shopware-api-client 1.0.100__py3-none-any.whl → 1.0.101__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shopware_api_client/base.py +46 -13
- shopware_api_client/client.py +4 -1
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.101.dist-info}/METADATA +7 -1
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.101.dist-info}/RECORD +6 -6
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.101.dist-info}/LICENSE +0 -0
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.101.dist-info}/WHEEL +0 -0
shopware_api_client/base.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
3
|
from datetime import UTC, datetime
|
|
4
|
+
from functools import cached_property
|
|
4
5
|
from typing import (
|
|
5
6
|
Any,
|
|
6
7
|
AsyncGenerator,
|
|
@@ -9,6 +10,7 @@ from typing import (
|
|
|
9
10
|
Self,
|
|
10
11
|
Type,
|
|
11
12
|
TypeVar,
|
|
13
|
+
cast,
|
|
12
14
|
get_origin,
|
|
13
15
|
overload,
|
|
14
16
|
)
|
|
@@ -61,11 +63,11 @@ class ClientBase:
|
|
|
61
63
|
self.raw = raw
|
|
62
64
|
|
|
63
65
|
async def __aenter__(self) -> "Self":
|
|
64
|
-
self.
|
|
66
|
+
self.http_client
|
|
65
67
|
return self
|
|
66
68
|
|
|
67
69
|
async def __aexit__(self, *args: Any) -> None:
|
|
68
|
-
await self.
|
|
70
|
+
await self.http_client.aclose()
|
|
69
71
|
|
|
70
72
|
async def log_request(self, request: httpx.Request) -> None:
|
|
71
73
|
if not hasattr(request, "_content"):
|
|
@@ -82,7 +84,13 @@ class ClientBase:
|
|
|
82
84
|
response.headers,
|
|
83
85
|
)
|
|
84
86
|
|
|
87
|
+
@cached_property
|
|
88
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
89
|
+
return self._get_client()
|
|
90
|
+
|
|
85
91
|
def _get_client(self) -> httpx.AsyncClient:
|
|
92
|
+
# FIXME: rename _get_client -> _get_http_client to avoid confusion with ApiModelBase._get_client
|
|
93
|
+
# (fix middleware usage of private method usage first)
|
|
86
94
|
raise NotImplementedError()
|
|
87
95
|
|
|
88
96
|
def _get_headers(self) -> dict[str, str]:
|
|
@@ -95,24 +103,30 @@ class ClientBase:
|
|
|
95
103
|
|
|
96
104
|
@property
|
|
97
105
|
def timeout(self) -> httpx.Timeout:
|
|
98
|
-
client = self.
|
|
106
|
+
client = self.http_client
|
|
99
107
|
return client.timeout
|
|
100
108
|
|
|
101
109
|
@timeout.setter
|
|
102
110
|
def timeout(self, timeout: httpx._types.TimeoutTypes) -> None:
|
|
103
|
-
client = self.
|
|
111
|
+
client = self.http_client
|
|
104
112
|
client.timeout = timeout # type: ignore
|
|
105
113
|
|
|
114
|
+
async def retry_sleep(self, retry_wait_base: int, retry_count: int) -> None:
|
|
115
|
+
retry_sleep = retry_wait_base**retry_count
|
|
116
|
+
logger.debug(f"Try failed, retrying in {retry_sleep} seconds.")
|
|
117
|
+
await asyncio.sleep(retry_sleep)
|
|
118
|
+
|
|
106
119
|
async def _make_request(self, method: str, relative_url: str, **kwargs: Any) -> httpx.Response:
|
|
107
120
|
if relative_url.startswith("http://") or relative_url.startswith("https://"):
|
|
108
121
|
url = relative_url
|
|
109
122
|
else:
|
|
110
123
|
url = f"{self.api_url}{relative_url}"
|
|
111
|
-
client = self.
|
|
124
|
+
client = self.http_client
|
|
112
125
|
|
|
113
126
|
headers = self._get_headers()
|
|
114
127
|
headers.update(kwargs.pop("headers", {}))
|
|
115
128
|
|
|
129
|
+
retry_wait_base = int(kwargs.pop("retriy_wait_base", 2))
|
|
116
130
|
retries = int(kwargs.pop("retries", 0))
|
|
117
131
|
retry_errors = tuple(
|
|
118
132
|
kwargs.pop("retry_errors", [SWAPIInternalServerError, SWAPIServiceUnavailable, SWAPIGatewayTimeout])
|
|
@@ -126,7 +140,7 @@ class ClientBase:
|
|
|
126
140
|
try:
|
|
127
141
|
response = await client.request(method, url, headers=headers, **kwargs)
|
|
128
142
|
except httpx.RequestError as exc:
|
|
129
|
-
if retry_count
|
|
143
|
+
if retry_count >= retries:
|
|
130
144
|
raise SWAPIException(f"HTTP client exception ({exc.__class__.__name__}). Details: {str(exc)}")
|
|
131
145
|
await asyncio.sleep(2**retry_count)
|
|
132
146
|
retry_count += 1
|
|
@@ -137,8 +151,8 @@ class ClientBase:
|
|
|
137
151
|
errors: list = response.json().get("errors")
|
|
138
152
|
# ensure `errors` attribute is a list/tuple, fallback to from_response if not
|
|
139
153
|
if not isinstance(errors, (list, tuple)):
|
|
140
|
-
|
|
141
|
-
|
|
154
|
+
raise ValueError("`errors` attribute in json not a list/tuple!")
|
|
155
|
+
|
|
142
156
|
error: SWAPIError | SWAPIErrorList = SWAPIError.from_errors(errors)
|
|
143
157
|
except (json.JSONDecodeError, ValueError):
|
|
144
158
|
error: SWAPIError | SWAPIErrorList = SWAPIError.from_response(response) # type: ignore
|
|
@@ -159,10 +173,28 @@ class ClientBase:
|
|
|
159
173
|
if retry_count == retries:
|
|
160
174
|
raise error
|
|
161
175
|
|
|
162
|
-
|
|
163
|
-
await asyncio.sleep(2**retry_count)
|
|
176
|
+
await self.retry_sleep(retry_wait_base, retry_count)
|
|
164
177
|
retry_count += 1
|
|
165
178
|
else:
|
|
179
|
+
# guard against "200 okay" responses with malformed json
|
|
180
|
+
try:
|
|
181
|
+
setattr(response, "json_cached", response.json())
|
|
182
|
+
except json.JSONDecodeError:
|
|
183
|
+
# retries exhausted?
|
|
184
|
+
if retry_count >= retries:
|
|
185
|
+
response.status_code = 500
|
|
186
|
+
exception = SWAPIError.from_response(response)
|
|
187
|
+
# prefix details with x-trace-header to
|
|
188
|
+
exception.detail = (
|
|
189
|
+
f"x-trace-id: {str(response.headers.get('x-trace-id', 'not-set'))}" + exception.detail
|
|
190
|
+
)
|
|
191
|
+
raise exception
|
|
192
|
+
|
|
193
|
+
# schedule retry
|
|
194
|
+
await self.retry_sleep(retry_wait_base, retry_count)
|
|
195
|
+
retry_count += 1
|
|
196
|
+
continue
|
|
197
|
+
|
|
166
198
|
return response
|
|
167
199
|
|
|
168
200
|
async def get(self, relative_url: str, **kwargs: Any) -> httpx.Response:
|
|
@@ -187,7 +219,7 @@ class ClientBase:
|
|
|
187
219
|
)
|
|
188
220
|
|
|
189
221
|
async def close(self) -> None:
|
|
190
|
-
await self.
|
|
222
|
+
await self.http_client.aclose()
|
|
191
223
|
|
|
192
224
|
async def bulk_upsert(
|
|
193
225
|
self,
|
|
@@ -289,7 +321,8 @@ class ApiModelBase(BaseModel, Generic[EndpointClass]):
|
|
|
289
321
|
|
|
290
322
|
def _get_endpoint(self) -> EndpointClass:
|
|
291
323
|
# we want a fresh endpoint
|
|
292
|
-
|
|
324
|
+
client = self._get_client()
|
|
325
|
+
endpoint: EndpointClass = getattr(client, self._identifier).__class__(client) # type: ignore
|
|
293
326
|
return endpoint
|
|
294
327
|
|
|
295
328
|
async def save(self, force_insert: bool = False, update_fields: IncEx | None = None) -> Self | dict | None:
|
|
@@ -372,7 +405,7 @@ class EndpointBase(Generic[ModelClass]):
|
|
|
372
405
|
field = self.model_class.model_fields[name]
|
|
373
406
|
|
|
374
407
|
if get_origin(field.annotation) in [ForeignRelation, ManyRelation]:
|
|
375
|
-
return to_camel(name)
|
|
408
|
+
return cast(str, to_camel(name))
|
|
376
409
|
else:
|
|
377
410
|
return self.model_class.model_fields[name].serialization_alias or name
|
|
378
411
|
|
shopware_api_client/client.py
CHANGED
|
@@ -164,7 +164,10 @@ class AdminClient(ClientBase, AdminEndpoints):
|
|
|
164
164
|
|
|
165
165
|
result = await self._retry_bulk_parts(action="delete", name=name, objs=objs, exception=e, **request_kwargs)
|
|
166
166
|
else:
|
|
167
|
-
|
|
167
|
+
if hasattr(response, "json_cached"):
|
|
168
|
+
result = response.json_cached
|
|
169
|
+
else:
|
|
170
|
+
result = response.json()
|
|
168
171
|
|
|
169
172
|
return result
|
|
170
173
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: shopware-api-client
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.101
|
|
4
4
|
Summary: An api client for the Shopware API
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: shopware,api,client
|
|
@@ -315,6 +315,12 @@ We have two classes `Base` and `Relations`. This way we can [reuse the Base-Mode
|
|
|
315
315
|
|
|
316
316
|
## Development
|
|
317
317
|
|
|
318
|
+
### Testing
|
|
319
|
+
|
|
320
|
+
You can use `poetry build` and `poetry run pip install -e .` to install the current src.
|
|
321
|
+
|
|
322
|
+
Then run `poetry run pytest .` to execute the tests.
|
|
323
|
+
|
|
318
324
|
### Model Creation
|
|
319
325
|
|
|
320
326
|
Shopware provides API-definitions for their whole API. You can download it from `<shopurl>/api/_info/openapi3.json`
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
shopware_api_client/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
shopware_api_client/base.py,sha256=
|
|
3
|
-
shopware_api_client/client.py,sha256=
|
|
2
|
+
shopware_api_client/base.py,sha256=x4VIfvpDbyIaqCY60LEVCIIRm_YQZ8CVR0EIKtGCwZg,24977
|
|
3
|
+
shopware_api_client/client.py,sha256=9b4Hvj6izHEMplus6--S2no0Sts61vg25HNIPEhJ7JQ,7573
|
|
4
4
|
shopware_api_client/config.py,sha256=HStgfQcClpo_aqaTRDrqdTUjqSGPFkIMjrPwSruVnM8,1565
|
|
5
5
|
shopware_api_client/endpoints/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
shopware_api_client/endpoints/admin/__init__.py,sha256=yfGXeIzGDoAIzVBp0zEGYMcWKoc-qrjMeot8HXOwfhA,17213
|
|
@@ -108,7 +108,7 @@ shopware_api_client/endpoints/store/core/cart.py,sha256=34eNwuv7H9WZUtJGf4TkTGHi
|
|
|
108
108
|
shopware_api_client/exceptions.py,sha256=AELVvzdjH0RABF0WgqQ-DbEuZB1k-5V8L_NkKZLV6tk,4459
|
|
109
109
|
shopware_api_client/logging.py,sha256=4QSTK1vcdBew4shvLG-fm-xDOlddhOZeyb5T9Og0fSA,251
|
|
110
110
|
shopware_api_client/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
111
|
-
shopware_api_client-1.0.
|
|
112
|
-
shopware_api_client-1.0.
|
|
113
|
-
shopware_api_client-1.0.
|
|
114
|
-
shopware_api_client-1.0.
|
|
111
|
+
shopware_api_client-1.0.101.dist-info/LICENSE,sha256=qTihFhbGE2ZJJ7Byc9hnEYBY33yDK2Jw87SpAm0IKUs,1107
|
|
112
|
+
shopware_api_client-1.0.101.dist-info/METADATA,sha256=UwnWQIm54nwFGcGafokW5eaIX8VrAQCGqxpOztwyvC8,22659
|
|
113
|
+
shopware_api_client-1.0.101.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
114
|
+
shopware_api_client-1.0.101.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|