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.

@@ -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._get_client()
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._get_client().aclose()
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
- def _get_client(self) -> httpx.AsyncClient:
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": "application/json", "Accept": "application/json"}
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._get_client()
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._get_client()
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._get_client()
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 == retries:
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
- raise ValueError("`errors` attribute in json not a list/tuple!")
141
-
155
+ raise ValueError("`errors` attribute in json not a list/tuple!")
156
+
142
157
  error: SWAPIError | SWAPIErrorList = SWAPIError.from_errors(errors)
143
- except (json.JSONDecodeError, ValueError):
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
- logger.debug(f"Try failed, retrying in {2 ** retry_count} seconds.")
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._get_client().aclose()
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
- endpoint: EndpointClass = getattr(self._get_client(), self._identifier).__class__(self._get_client()) # type: ignore
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
 
@@ -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 _get_client(self) -> httpx.AsyncClient:
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
- 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
 
@@ -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 _get_client(self) -> httpx.AsyncClient:
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.100
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=dq9o45_1jtrBlkNNnQssNv7H3_WOWsdqLRphuu32Riw,23528
3
- shopware_api_client/client.py,sha256=Ii8oRpCkCVzR6unRIanMxxOHHbqlFedaYEEyrlZwlkI,7456
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.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.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,,