Skip to content

Commit 238c079

Browse files
authored
Merge pull request #22 from cloudblue/async_client
New AsyncConnectClient for asyncio.
2 parents bc3a971 + 8ee7dd1 commit 238c079

33 files changed

+2997
-529
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ coverage/
1717
/htmlcov/
1818
docs/_build
1919
temp/
20-
converage.xml
20+
coverage.xml
21+
*.DS_Store

cnct/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
from connect.client.exceptions import ClientError # noqa
27
from connect.client.fluent import ConnectClient # noqa
38
from connect.client.rql import R # noqa

cnct/rql.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
from connect.client.rql import R # noqa

connect/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
import pkgutil
27

38
__path__ = pkgutil.extend_path(__path__, __name__)

connect/client/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
from connect.client.exceptions import ClientError # noqa
2-
from connect.client.fluent import ConnectClient # noqa
7+
from connect.client.fluent import AsyncConnectClient, ConnectClient # noqa
38
from connect.client.rql import R # noqa

connect/client/constants.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,7 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
CONNECT_SPECS_URL = 'https://apispec.connect.cloudblue.com/connect-openapi30.yml' # noqa
27
CONNECT_ENDPOINT_URL = 'https://api.connect.cloudblue.com/public/v1' # noqa

connect/client/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
from http import HTTPStatus
27

38

connect/client/fluent.py

Lines changed: 33 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
import threading
2-
import time
37
from json.decoder import JSONDecodeError
48

5-
import requests
6-
from requests.exceptions import RequestException
7-
89
from connect.client.constants import CONNECT_ENDPOINT_URL, CONNECT_SPECS_URL
9-
from connect.client.exceptions import ClientError
10-
from connect.client.models import Collection, NS
10+
from connect.client.mixins import AsyncClientMixin, SyncClientMixin
11+
from connect.client.models import AsyncCollection, AsyncNS, Collection, NS
1112
from connect.client.utils import get_headers
1213
from connect.client.help_formatter import DefaultFormatter
1314
from connect.client.openapi import OpenAPISpecs
1415

1516

16-
class ConnectClient(threading.local):
17+
class _ConnectClientBase(threading.local):
18+
1719
"""
1820
Connect ReST API client.
1921
"""
@@ -89,7 +91,7 @@ def ns(self, name):
8991
if not name:
9092
raise ValueError('`name` must not be blank.')
9193

92-
return NS(self, name)
94+
return self._get_namespace_class()(self, name)
9395

9496
def collection(self, name):
9597
"""
@@ -106,62 +108,11 @@ def collection(self, name):
106108
if not name:
107109
raise ValueError('`name` must not be blank.')
108110

109-
return Collection(
111+
return self._get_collection_class()(
110112
self,
111113
name,
112114
)
113115

114-
def get(self, url, **kwargs):
115-
return self.execute('get', url, **kwargs)
116-
117-
def create(self, url, payload=None, **kwargs):
118-
kwargs = kwargs or {}
119-
120-
if payload:
121-
kwargs['json'] = payload
122-
123-
return self.execute('post', url, **kwargs)
124-
125-
def update(self, url, payload=None, **kwargs):
126-
kwargs = kwargs or {}
127-
128-
if payload:
129-
kwargs['json'] = payload
130-
131-
return self.execute('put', url, **kwargs)
132-
133-
def delete(self, url, **kwargs):
134-
return self.execute('delete', url, **kwargs)
135-
136-
def execute(self, method, path, **kwargs):
137-
if (
138-
self._use_specs
139-
and self._validate_using_specs
140-
and not self.specs.exists(method, path)
141-
):
142-
# TODO more info, specs version, method etc
143-
raise ClientError(f'The path `{path}` does not exist.')
144-
145-
url = f'{self.endpoint}/{path}'
146-
147-
kwargs = self._prepare_call_kwargs(kwargs)
148-
149-
self.response = None
150-
151-
try:
152-
self._execute_http_call(method, url, kwargs)
153-
if self.response.status_code == 204:
154-
return None
155-
if self.response.headers['Content-Type'] == 'application/json':
156-
return self.response.json()
157-
else:
158-
return self.response.content
159-
160-
except RequestException as re:
161-
api_error = self._get_api_error_details() or {}
162-
status_code = self.response.status_code if self.response is not None else None
163-
raise ClientError(status_code=status_code, **api_error) from re
164-
165116
def print_help(self, obj):
166117
print()
167118
print(self._help_formatter.format(obj))
@@ -170,6 +121,12 @@ def help(self):
170121
self.print_help(None)
171122
return self
172123

124+
def _get_collection_class(self):
125+
raise NotImplementedError()
126+
127+
def _get_namespace_class(self):
128+
raise NotImplementedError()
129+
173130
def _prepare_call_kwargs(self, kwargs):
174131
kwargs = kwargs or {}
175132
if 'headers' in kwargs:
@@ -181,21 +138,6 @@ def _prepare_call_kwargs(self, kwargs):
181138
kwargs['headers'].update(self.default_headers)
182139
return kwargs
183140

184-
def _execute_http_call(self, method, url, kwargs):
185-
retry_count = 0
186-
while True:
187-
self.response = requests.request(method, url, **kwargs)
188-
if ( # pragma: no branch
189-
self.response.status_code == 502
190-
and retry_count < self.max_retries
191-
):
192-
retry_count += 1
193-
time.sleep(1)
194-
continue
195-
break # pragma: no cover
196-
if self.response.status_code >= 400:
197-
self.response.raise_for_status()
198-
199141
def _get_api_error_details(self):
200142
if self.response is not None:
201143
try:
@@ -204,3 +146,19 @@ def _get_api_error_details(self):
204146
return error
205147
except JSONDecodeError:
206148
pass
149+
150+
151+
class ConnectClient(_ConnectClientBase, SyncClientMixin):
152+
def _get_collection_class(self):
153+
return Collection
154+
155+
def _get_namespace_class(self):
156+
return NS
157+
158+
159+
class AsyncConnectClient(_ConnectClientBase, AsyncClientMixin):
160+
def _get_collection_class(self):
161+
return AsyncCollection
162+
163+
def _get_namespace_class(self):
164+
return AsyncNS

connect/client/help_formatter.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
16
import inflect
27

38
from cmr import render

connect/client/mixins.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#
2+
# This file is part of the Ingram Micro CloudBlue Connect Python OpenAPI Client.
3+
#
4+
# Copyright (c) 2021 Ingram Micro. All Rights Reserved.
5+
#
6+
import time
7+
8+
import httpx
9+
10+
import requests
11+
12+
from httpx import HTTPError
13+
14+
from requests.exceptions import RequestException
15+
16+
from connect.client.exceptions import ClientError
17+
18+
19+
class SyncClientMixin:
20+
21+
def get(self, url, **kwargs):
22+
return self.execute('get', url, **kwargs)
23+
24+
def create(self, url, payload=None, **kwargs):
25+
kwargs = kwargs or {}
26+
27+
if payload:
28+
kwargs['json'] = payload
29+
30+
return self.execute('post', url, **kwargs)
31+
32+
def update(self, url, payload=None, **kwargs):
33+
kwargs = kwargs or {}
34+
35+
if payload:
36+
kwargs['json'] = payload
37+
38+
return self.execute('put', url, **kwargs)
39+
40+
def delete(self, url, **kwargs):
41+
return self.execute('delete', url, **kwargs)
42+
43+
def execute(self, method, path, **kwargs):
44+
if (
45+
self._use_specs
46+
and self._validate_using_specs
47+
and not self.specs.exists(method, path)
48+
):
49+
# TODO more info, specs version, method etc
50+
raise ClientError(f'The path `{path}` does not exist.')
51+
52+
url = f'{self.endpoint}/{path}'
53+
54+
kwargs = self._prepare_call_kwargs(kwargs)
55+
56+
self.response = None
57+
58+
try:
59+
self._execute_http_call(method, url, kwargs)
60+
if self.response.status_code == 204:
61+
return None
62+
if self.response.headers['Content-Type'] == 'application/json':
63+
return self.response.json()
64+
else:
65+
return self.response.content
66+
67+
except RequestException as re:
68+
api_error = self._get_api_error_details() or {}
69+
status_code = self.response.status_code if self.response is not None else None
70+
raise ClientError(status_code=status_code, **api_error) from re
71+
72+
def _execute_http_call(self, method, url, kwargs):
73+
retry_count = 0
74+
while True:
75+
self.response = requests.request(method, url, **kwargs)
76+
if ( # pragma: no branch
77+
self.response.status_code == 502
78+
and retry_count < self.max_retries
79+
):
80+
retry_count += 1
81+
time.sleep(1)
82+
continue
83+
break # pragma: no cover
84+
if self.response.status_code >= 400:
85+
self.response.raise_for_status()
86+
87+
88+
class AsyncClientMixin:
89+
90+
async def get(self, url, **kwargs):
91+
return await self.execute('get', url, **kwargs)
92+
93+
async def create(self, url, payload=None, **kwargs):
94+
kwargs = kwargs or {}
95+
96+
if payload:
97+
kwargs['json'] = payload
98+
99+
return await self.execute('post', url, **kwargs)
100+
101+
async def update(self, url, payload=None, **kwargs):
102+
kwargs = kwargs or {}
103+
104+
if payload:
105+
kwargs['json'] = payload
106+
107+
return await self.execute('put', url, **kwargs)
108+
109+
async def delete(self, url, **kwargs):
110+
return await self.execute('delete', url, **kwargs)
111+
112+
async def execute(self, method, path, **kwargs):
113+
if (
114+
self._use_specs
115+
and self._validate_using_specs
116+
and not self.specs.exists(method, path)
117+
):
118+
# TODO more info, specs version, method etc
119+
raise ClientError(f'The path `{path}` does not exist.')
120+
121+
url = f'{self.endpoint}/{path}'
122+
123+
kwargs = self._prepare_call_kwargs(kwargs)
124+
125+
self.response = None
126+
127+
try:
128+
await self._execute_http_call(method, url, kwargs)
129+
if self.response.status_code == 204:
130+
return None
131+
if self.response.headers.get('Content-Type') == 'application/json':
132+
return self.response.json()
133+
else:
134+
return self.response.content
135+
136+
except HTTPError as re:
137+
api_error = self._get_api_error_details() or {}
138+
status_code = self.response.status_code if self.response is not None else None
139+
raise ClientError(status_code=status_code, **api_error) from re
140+
141+
async def _execute_http_call(self, method, url, kwargs):
142+
retry_count = 0
143+
while True:
144+
async with httpx.AsyncClient() as client:
145+
self.response = await client.request(method, url, **kwargs)
146+
if ( # pragma: no branch
147+
self.response.status_code == 502
148+
and retry_count < self.max_retries
149+
):
150+
retry_count += 1
151+
time.sleep(1)
152+
continue
153+
break # pragma: no cover
154+
if self.response.status_code >= 400:
155+
self.response.raise_for_status()

0 commit comments

Comments
 (0)