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.
@@ -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._get_client()
66
+ self.http_client
65
67
  return self
66
68
 
67
69
  async def __aexit__(self, *args: Any) -> None:
68
- await self._get_client().aclose()
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._get_client()
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._get_client()
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._get_client()
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 == retries:
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
- raise ValueError("`errors` attribute in json not a list/tuple!")
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
- logger.debug(f"Try failed, retrying in {2 ** retry_count} seconds.")
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._get_client().aclose()
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
- endpoint: EndpointClass = getattr(self._get_client(), self._identifier).__class__(self._get_client()) # type: ignore
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
 
@@ -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
- result = response.json()
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.100
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=dq9o45_1jtrBlkNNnQssNv7H3_WOWsdqLRphuu32Riw,23528
3
- shopware_api_client/client.py,sha256=Ii8oRpCkCVzR6unRIanMxxOHHbqlFedaYEEyrlZwlkI,7456
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.100.dist-info/LICENSE,sha256=qTihFhbGE2ZJJ7Byc9hnEYBY33yDK2Jw87SpAm0IKUs,1107
112
- shopware_api_client-1.0.100.dist-info/METADATA,sha256=bliK6_zoJTbegjL2cb-xVSQUWuGRaCC-9J7_X0FWgQA,22502
113
- shopware_api_client-1.0.100.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
114
- shopware_api_client-1.0.100.dist-info/RECORD,,
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,,