shopware-api-client 1.0.100__py3-none-any.whl → 1.0.102__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.
Potentially problematic release.
This version of shopware-api-client might be problematic. Click here for more details.
- shopware_api_client/base.py +50 -16
- shopware_api_client/client.py +6 -3
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.102.dist-info}/METADATA +7 -1
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.102.dist-info}/RECORD +6 -6
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.102.dist-info}/LICENSE +0 -0
- {shopware_api_client-1.0.100.dist-info → shopware_api_client-1.0.102.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
|
)
|
|
@@ -42,6 +44,8 @@ from .exceptions import (
|
|
|
42
44
|
)
|
|
43
45
|
from .logging import logger
|
|
44
46
|
|
|
47
|
+
APPLICATION_JSON = "application/json"
|
|
48
|
+
|
|
45
49
|
EndpointClass = TypeVar("EndpointClass", bound="EndpointBase[Any]")
|
|
46
50
|
ModelClass = TypeVar("ModelClass", bound="ApiModelBase[Any]")
|
|
47
51
|
|
|
@@ -61,11 +65,12 @@ class ClientBase:
|
|
|
61
65
|
self.raw = raw
|
|
62
66
|
|
|
63
67
|
async def __aenter__(self) -> "Self":
|
|
64
|
-
self.
|
|
68
|
+
client = self.http_client
|
|
69
|
+
assert isinstance(client, httpx.AsyncClient), "http_client must be an instance of httpx.AsyncClient"
|
|
65
70
|
return self
|
|
66
71
|
|
|
67
72
|
async def __aexit__(self, *args: Any) -> None:
|
|
68
|
-
await self.
|
|
73
|
+
await self.http_client.aclose()
|
|
69
74
|
|
|
70
75
|
async def log_request(self, request: httpx.Request) -> None:
|
|
71
76
|
if not hasattr(request, "_content"):
|
|
@@ -82,11 +87,15 @@ class ClientBase:
|
|
|
82
87
|
response.headers,
|
|
83
88
|
)
|
|
84
89
|
|
|
85
|
-
|
|
90
|
+
@cached_property
|
|
91
|
+
def http_client(self) -> httpx.AsyncClient:
|
|
92
|
+
return self._get_http_client()
|
|
93
|
+
|
|
94
|
+
def _get_http_client(self) -> httpx.AsyncClient:
|
|
86
95
|
raise NotImplementedError()
|
|
87
96
|
|
|
88
97
|
def _get_headers(self) -> dict[str, str]:
|
|
89
|
-
headers = {"Content-Type":
|
|
98
|
+
headers = {"Content-Type": APPLICATION_JSON, "Accept": APPLICATION_JSON}
|
|
90
99
|
|
|
91
100
|
if self.language_id is not None:
|
|
92
101
|
headers["sw-language-id"] = str(self.language_id)
|
|
@@ -95,24 +104,30 @@ class ClientBase:
|
|
|
95
104
|
|
|
96
105
|
@property
|
|
97
106
|
def timeout(self) -> httpx.Timeout:
|
|
98
|
-
client = self.
|
|
107
|
+
client = self.http_client
|
|
99
108
|
return client.timeout
|
|
100
109
|
|
|
101
110
|
@timeout.setter
|
|
102
111
|
def timeout(self, timeout: httpx._types.TimeoutTypes) -> None:
|
|
103
|
-
client = self.
|
|
112
|
+
client = self.http_client
|
|
104
113
|
client.timeout = timeout # type: ignore
|
|
105
114
|
|
|
115
|
+
async def retry_sleep(self, retry_wait_base: int, retry_count: int) -> None:
|
|
116
|
+
retry_sleep = retry_wait_base**retry_count
|
|
117
|
+
logger.debug(f"Try failed, retrying in {retry_sleep} seconds.")
|
|
118
|
+
await asyncio.sleep(retry_sleep)
|
|
119
|
+
|
|
106
120
|
async def _make_request(self, method: str, relative_url: str, **kwargs: Any) -> httpx.Response:
|
|
107
121
|
if relative_url.startswith("http://") or relative_url.startswith("https://"):
|
|
108
122
|
url = relative_url
|
|
109
123
|
else:
|
|
110
124
|
url = f"{self.api_url}{relative_url}"
|
|
111
|
-
client = self.
|
|
125
|
+
client = self.http_client
|
|
112
126
|
|
|
113
127
|
headers = self._get_headers()
|
|
114
128
|
headers.update(kwargs.pop("headers", {}))
|
|
115
129
|
|
|
130
|
+
retry_wait_base = int(kwargs.pop("retriy_wait_base", 2))
|
|
116
131
|
retries = int(kwargs.pop("retries", 0))
|
|
117
132
|
retry_errors = tuple(
|
|
118
133
|
kwargs.pop("retry_errors", [SWAPIInternalServerError, SWAPIServiceUnavailable, SWAPIGatewayTimeout])
|
|
@@ -126,7 +141,7 @@ class ClientBase:
|
|
|
126
141
|
try:
|
|
127
142
|
response = await client.request(method, url, headers=headers, **kwargs)
|
|
128
143
|
except httpx.RequestError as exc:
|
|
129
|
-
if retry_count
|
|
144
|
+
if retry_count >= retries:
|
|
130
145
|
raise SWAPIException(f"HTTP client exception ({exc.__class__.__name__}). Details: {str(exc)}")
|
|
131
146
|
await asyncio.sleep(2**retry_count)
|
|
132
147
|
retry_count += 1
|
|
@@ -137,10 +152,10 @@ class ClientBase:
|
|
|
137
152
|
errors: list = response.json().get("errors")
|
|
138
153
|
# ensure `errors` attribute is a list/tuple, fallback to from_response if not
|
|
139
154
|
if not isinstance(errors, (list, tuple)):
|
|
140
|
-
|
|
141
|
-
|
|
155
|
+
raise ValueError("`errors` attribute in json not a list/tuple!")
|
|
156
|
+
|
|
142
157
|
error: SWAPIError | SWAPIErrorList = SWAPIError.from_errors(errors)
|
|
143
|
-
except
|
|
158
|
+
except ValueError:
|
|
144
159
|
error: SWAPIError | SWAPIErrorList = SWAPIError.from_response(response) # type: ignore
|
|
145
160
|
|
|
146
161
|
if isinstance(error, SWAPIErrorList) and len(error.errors) == 1:
|
|
@@ -159,9 +174,27 @@ class ClientBase:
|
|
|
159
174
|
if retry_count == retries:
|
|
160
175
|
raise error
|
|
161
176
|
|
|
162
|
-
|
|
163
|
-
await asyncio.sleep(2**retry_count)
|
|
177
|
+
await self.retry_sleep(retry_wait_base, retry_count)
|
|
164
178
|
retry_count += 1
|
|
179
|
+
elif response.status_code == 200 and response.headers.get("Content-Type", "").startswith(APPLICATION_JSON):
|
|
180
|
+
# guard against "200 okay" responses with malformed json
|
|
181
|
+
try:
|
|
182
|
+
setattr(response, "json_cached", response.json())
|
|
183
|
+
return response
|
|
184
|
+
except json.JSONDecodeError:
|
|
185
|
+
# retries exhausted?
|
|
186
|
+
if retry_count >= retries:
|
|
187
|
+
response.status_code = 500
|
|
188
|
+
exception = SWAPIError.from_response(response)
|
|
189
|
+
# prefix details with x-trace-header to
|
|
190
|
+
exception.detail = (
|
|
191
|
+
f"x-trace-id: {str(response.headers.get('x-trace-id', 'not-set'))}" + exception.detail
|
|
192
|
+
)
|
|
193
|
+
raise exception
|
|
194
|
+
|
|
195
|
+
# schedule retry
|
|
196
|
+
await self.retry_sleep(retry_wait_base, retry_count)
|
|
197
|
+
retry_count += 1
|
|
165
198
|
else:
|
|
166
199
|
return response
|
|
167
200
|
|
|
@@ -187,7 +220,7 @@ class ClientBase:
|
|
|
187
220
|
)
|
|
188
221
|
|
|
189
222
|
async def close(self) -> None:
|
|
190
|
-
await self.
|
|
223
|
+
await self.http_client.aclose()
|
|
191
224
|
|
|
192
225
|
async def bulk_upsert(
|
|
193
226
|
self,
|
|
@@ -289,7 +322,8 @@ class ApiModelBase(BaseModel, Generic[EndpointClass]):
|
|
|
289
322
|
|
|
290
323
|
def _get_endpoint(self) -> EndpointClass:
|
|
291
324
|
# we want a fresh endpoint
|
|
292
|
-
|
|
325
|
+
client = self._get_client()
|
|
326
|
+
endpoint: EndpointClass = getattr(client, self._identifier).__class__(client) # type: ignore
|
|
293
327
|
return endpoint
|
|
294
328
|
|
|
295
329
|
async def save(self, force_insert: bool = False, update_fields: IncEx | None = None) -> Self | dict | None:
|
|
@@ -372,7 +406,7 @@ class EndpointBase(Generic[ModelClass]):
|
|
|
372
406
|
field = self.model_class.model_fields[name]
|
|
373
407
|
|
|
374
408
|
if get_origin(field.annotation) in [ForeignRelation, ManyRelation]:
|
|
375
|
-
return to_camel(name)
|
|
409
|
+
return cast(str, to_camel(name))
|
|
376
410
|
else:
|
|
377
411
|
return self.model_class.model_fields[name].serialization_alias or name
|
|
378
412
|
|
shopware_api_client/client.py
CHANGED
|
@@ -19,7 +19,7 @@ class AdminClient(ClientBase, AdminEndpoints):
|
|
|
19
19
|
self._client: httpx.AsyncClient | None = None
|
|
20
20
|
self.init_endpoints(self)
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def _get_http_client(self) -> httpx.AsyncClient:
|
|
23
23
|
if self._client is None:
|
|
24
24
|
self._client = httpx.AsyncClient(
|
|
25
25
|
event_hooks={"request": [self.log_request], "response": [self.log_response]}
|
|
@@ -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
|
|
|
@@ -177,7 +180,7 @@ class StoreClient(ClientBase, StoreEndpoints):
|
|
|
177
180
|
self._client: httpx.AsyncClient | None = None
|
|
178
181
|
self.init_endpoints(self)
|
|
179
182
|
|
|
180
|
-
def
|
|
183
|
+
def _get_http_client(self) -> httpx.AsyncClient:
|
|
181
184
|
if self._client is None:
|
|
182
185
|
self._client = httpx.AsyncClient(
|
|
183
186
|
event_hooks={"request": [self.log_request], "response": [self.log_response]},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: shopware-api-client
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.102
|
|
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=8iBg3_OvcsEETVGmW-QsSy2HU-6JLnnogA6NaupuE6k,25067
|
|
3
|
+
shopware_api_client/client.py,sha256=vkTZ8WLIu0Q3_73gXvidkTSpMJlWsJUjFyhDY2f2ISM,7583
|
|
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.102.dist-info/LICENSE,sha256=qTihFhbGE2ZJJ7Byc9hnEYBY33yDK2Jw87SpAm0IKUs,1107
|
|
112
|
+
shopware_api_client-1.0.102.dist-info/METADATA,sha256=FQYM1P-ZsAL0904pyeXAA9rBwPM11Tpz4NH1vj2L1oI,22659
|
|
113
|
+
shopware_api_client-1.0.102.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
|
|
114
|
+
shopware_api_client-1.0.102.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|