shopware-api-client 1.0.107__tar.gz → 1.0.109__tar.gz
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-1.0.107 → shopware_api_client-1.0.109}/PKG-INFO +42 -2
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/README.md +39 -1
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/pyproject.toml +12 -1
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/base.py +133 -21
- shopware_api_client-1.0.109/src/shopware_api_client/cache.py +162 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/config.py +4 -3
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/exceptions.py +11 -1
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/LICENSE +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/client.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/commercial/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/commercial/b2b_components_role.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/commercial/b2b_employee.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/commercial/dynamic_access.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/acl_role.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/api_info.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/app.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/app_script_condition.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/category.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/cms_block.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/cms_page.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/cms_section.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/cms_slot.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/country.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/country_state.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/currency.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/currency_country_rounding.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/custom_entity.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/custom_field.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/customer.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/customer_address.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/customer_group.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/customer_recovery.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/customer_wishlist.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/customer_wishlist_product.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/delivery_time.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/document.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/document_base_config.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/document_base_config_sales_channel.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/document_type.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/integration.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/landing_page.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/language.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/locale.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/main_category.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/media.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/media_default_folder.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/media_folder.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/media_folder_configuration.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/media_thumbnail.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/media_thumbnail_size.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_address.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_customer.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_delivery.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_delivery_position.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_line_item.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_line_item_download.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_transaction.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_transaction_capture.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_transaction_capture_refund.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/order_transaction_capture_refund_position.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/payment_method.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_configurator_setting.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_cross_selling.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_cross_selling_assigned_products.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_download.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_export.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_feature_set.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_manufacturer.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_media.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_price.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_review.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_search_keyword.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_stream.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_visibility.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/product_warehouse.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/promotion.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/promotion_discount.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/promotion_discount_prices.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/property_group.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/property_group_option.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/rule.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/rule_condition.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/sales_channel.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/sales_channel_domain.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/salutation.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/seo_url.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/shipping_method.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/state_machine.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/state_machine_history.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/state_machine_state.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/state_machine_transition.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/system_config.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/tag.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/tax.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/tax_rule.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/tax_rule_type.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/unit.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/user.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/warehouse.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/warehouse_group.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/admin/core/warehouse_group_warehouse.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/base_fields.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/relations.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/store/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/store/core/__init__.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/store/core/address.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/endpoints/store/core/cart.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/logging.py +0 -0
- {shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: shopware-api-client
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.109
|
|
4
4
|
Summary: An api client for the Shopware API
|
|
5
5
|
License: MIT
|
|
6
6
|
Keywords: shopware,api,client
|
|
@@ -12,10 +12,12 @@ Classifier: Operating System :: OS Independent
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Provides-Extra: redis
|
|
15
16
|
Requires-Dist: httpx (>=0.26,<0.27)
|
|
16
17
|
Requires-Dist: httpx-auth (>=0.21,<0.22)
|
|
17
18
|
Requires-Dist: pydantic (>=2.6,<3.0)
|
|
18
19
|
Requires-Dist: pytest-random-order (>=1.1.1,<2.0.0)
|
|
20
|
+
Requires-Dist: redis (>5.0,<7.0) ; extra == "redis"
|
|
19
21
|
Project-URL: Bugtracker, https://github.com/GWS-mbH/shopware-api-client/issues
|
|
20
22
|
Project-URL: Changelog, https://github.com/GWS-mbH/shopware-api-client
|
|
21
23
|
Project-URL: Documentation, https://github.com/GWS-mbH/shopware-api-client/wiki
|
|
@@ -29,8 +31,13 @@ A Django-ORM like, Python 3.12, async Shopware 6 admin and store-front API clien
|
|
|
29
31
|
|
|
30
32
|
## Installation
|
|
31
33
|
|
|
34
|
+
```sh
|
|
32
35
|
pip install shopware-api-client
|
|
33
36
|
|
|
37
|
+
# If you want to use the redis cache
|
|
38
|
+
pip install shopware-api-client[redis]
|
|
39
|
+
```
|
|
40
|
+
|
|
34
41
|
## Usage
|
|
35
42
|
|
|
36
43
|
There are two kinds of clients provided by this library. The `client.AdminClient` for the Admin API and the
|
|
@@ -151,7 +158,7 @@ await client.ce_blog.all()
|
|
|
151
158
|
|
|
152
159
|
# Pydantic Model for the custom entity ce_blog
|
|
153
160
|
CeBlog = client.ce_blog.model_class
|
|
154
|
-
```
|
|
161
|
+
```
|
|
155
162
|
Since custom entities are completely dynamic no autocompletion in IDE is available. However there are some pydantic validations added for the field-types of the custom entity. Relations are currently not supported, but everything else should work as expected.
|
|
156
163
|
|
|
157
164
|
### client.StoreClient
|
|
@@ -175,6 +182,39 @@ config = StoreConfig(url=SHOP_URL, access_key=STORE_API_ACCESS_KEY, context_toke
|
|
|
175
182
|
|
|
176
183
|
This config can be used with the `StoreClient`, which works exactly like the `AdminClient`.
|
|
177
184
|
|
|
185
|
+
### Redis Caching for Rate Limits
|
|
186
|
+
|
|
187
|
+
Both the AdminClient and the StoreClient use a built-in rate limiter. Shopware's rate limits differ based on the endpoints, both for the [SaaS-](https://docs.shopware.com/en/en/shopware-6-en/saas/rate-limits) and the [on-premise-solution](https://developer.shopware.com/docs/guides/hosting/infrastructure/rate-limiter.html).
|
|
188
|
+
|
|
189
|
+
To be able to respect the rate limit when sending requests from multiple clients, it is possible to use redis as a cache-backend for route-based rate-limit data. If redis is not used, each Client independently keeps track of the rate limit. Please note that the non-Redis cache is not thread-safe.
|
|
190
|
+
|
|
191
|
+
To use redis, simply hand over a redis-client to the client config:
|
|
192
|
+
```py
|
|
193
|
+
import redis
|
|
194
|
+
from shopware_api_client.config import AdminConfig, StoreConfig
|
|
195
|
+
from shopware_api_client.client import AdminClient, StoreClient
|
|
196
|
+
|
|
197
|
+
redis_client = redis.Redis()
|
|
198
|
+
|
|
199
|
+
admin_config = AdminConfig(
|
|
200
|
+
url='',
|
|
201
|
+
client_id='...',
|
|
202
|
+
client_secre='...',
|
|
203
|
+
redis_client=redis_client,
|
|
204
|
+
)
|
|
205
|
+
admin_client = AdminClient(config=config) # <- This client uses the redis client now
|
|
206
|
+
|
|
207
|
+
store_config = StoreConfig(
|
|
208
|
+
url='',
|
|
209
|
+
access_key='',
|
|
210
|
+
context_token=''
|
|
211
|
+
redis_client=redis_client,
|
|
212
|
+
)
|
|
213
|
+
store_client = StoreClient(config=config) # <- Works for store client as well (Only do this in safe environments)
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
__Note:__ Shopware currently enforces rate limits on a per–public‑IP basis. As a result, you should only share Redis‑backed rate‑limit caching among clients that originate from the same public IP address.
|
|
217
|
+
|
|
178
218
|
## EndpointBase
|
|
179
219
|
The `base.EndpointBase` class should be used for creating new Endpoints. It provides some usefull functions to call
|
|
180
220
|
the Shopware-API.
|
|
@@ -4,8 +4,13 @@ A Django-ORM like, Python 3.12, async Shopware 6 admin and store-front API clien
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
```sh
|
|
7
8
|
pip install shopware-api-client
|
|
8
9
|
|
|
10
|
+
# If you want to use the redis cache
|
|
11
|
+
pip install shopware-api-client[redis]
|
|
12
|
+
```
|
|
13
|
+
|
|
9
14
|
## Usage
|
|
10
15
|
|
|
11
16
|
There are two kinds of clients provided by this library. The `client.AdminClient` for the Admin API and the
|
|
@@ -126,7 +131,7 @@ await client.ce_blog.all()
|
|
|
126
131
|
|
|
127
132
|
# Pydantic Model for the custom entity ce_blog
|
|
128
133
|
CeBlog = client.ce_blog.model_class
|
|
129
|
-
```
|
|
134
|
+
```
|
|
130
135
|
Since custom entities are completely dynamic no autocompletion in IDE is available. However there are some pydantic validations added for the field-types of the custom entity. Relations are currently not supported, but everything else should work as expected.
|
|
131
136
|
|
|
132
137
|
### client.StoreClient
|
|
@@ -150,6 +155,39 @@ config = StoreConfig(url=SHOP_URL, access_key=STORE_API_ACCESS_KEY, context_toke
|
|
|
150
155
|
|
|
151
156
|
This config can be used with the `StoreClient`, which works exactly like the `AdminClient`.
|
|
152
157
|
|
|
158
|
+
### Redis Caching for Rate Limits
|
|
159
|
+
|
|
160
|
+
Both the AdminClient and the StoreClient use a built-in rate limiter. Shopware's rate limits differ based on the endpoints, both for the [SaaS-](https://docs.shopware.com/en/en/shopware-6-en/saas/rate-limits) and the [on-premise-solution](https://developer.shopware.com/docs/guides/hosting/infrastructure/rate-limiter.html).
|
|
161
|
+
|
|
162
|
+
To be able to respect the rate limit when sending requests from multiple clients, it is possible to use redis as a cache-backend for route-based rate-limit data. If redis is not used, each Client independently keeps track of the rate limit. Please note that the non-Redis cache is not thread-safe.
|
|
163
|
+
|
|
164
|
+
To use redis, simply hand over a redis-client to the client config:
|
|
165
|
+
```py
|
|
166
|
+
import redis
|
|
167
|
+
from shopware_api_client.config import AdminConfig, StoreConfig
|
|
168
|
+
from shopware_api_client.client import AdminClient, StoreClient
|
|
169
|
+
|
|
170
|
+
redis_client = redis.Redis()
|
|
171
|
+
|
|
172
|
+
admin_config = AdminConfig(
|
|
173
|
+
url='',
|
|
174
|
+
client_id='...',
|
|
175
|
+
client_secre='...',
|
|
176
|
+
redis_client=redis_client,
|
|
177
|
+
)
|
|
178
|
+
admin_client = AdminClient(config=config) # <- This client uses the redis client now
|
|
179
|
+
|
|
180
|
+
store_config = StoreConfig(
|
|
181
|
+
url='',
|
|
182
|
+
access_key='',
|
|
183
|
+
context_token=''
|
|
184
|
+
redis_client=redis_client,
|
|
185
|
+
)
|
|
186
|
+
store_client = StoreClient(config=config) # <- Works for store client as well (Only do this in safe environments)
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
__Note:__ Shopware currently enforces rate limits on a per–public‑IP basis. As a result, you should only share Redis‑backed rate‑limit caching among clients that originate from the same public IP address.
|
|
190
|
+
|
|
153
191
|
## EndpointBase
|
|
154
192
|
The `base.EndpointBase` class should be used for creating new Endpoints. It provides some usefull functions to call
|
|
155
193
|
the Shopware-API.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "shopware-api-client"
|
|
3
3
|
description = " An api client for the Shopware API"
|
|
4
|
-
version = "1.0.
|
|
4
|
+
version = "1.0.109"
|
|
5
5
|
license = "MIT"
|
|
6
6
|
authors = ["GWS Gesellschaft für Warenwirtschafts-Systeme mbH <ebusiness@gws.ms>"]
|
|
7
7
|
readme = "README.md"
|
|
@@ -25,6 +25,10 @@ httpx = "^0.26"
|
|
|
25
25
|
httpx-auth = "^0.21"
|
|
26
26
|
pydantic = "^2.6"
|
|
27
27
|
pytest-random-order = "^1.1.1"
|
|
28
|
+
redis = {version = ">5.0,<7.0", optional = true}
|
|
29
|
+
|
|
30
|
+
[tool.poetry.extras]
|
|
31
|
+
redis = ["redis"]
|
|
28
32
|
|
|
29
33
|
[tool.poetry.group.dev.dependencies]
|
|
30
34
|
mypy = "^1.8.0"
|
|
@@ -35,6 +39,8 @@ ruff = ">=0.5.0,<0.10"
|
|
|
35
39
|
pytest-asyncio = ">=0.23.3,<0.26.0"
|
|
36
40
|
pytest-recording = "^0.13.1"
|
|
37
41
|
pytest-mock = "^3.14.0"
|
|
42
|
+
redis = ">5.0,<7.0"
|
|
43
|
+
fakeredis = {extras = ["lua"], version = "^2.30.1"}
|
|
38
44
|
|
|
39
45
|
[build-system]
|
|
40
46
|
requires = ["poetry-core"]
|
|
@@ -47,6 +53,11 @@ warn_unused_configs = true
|
|
|
47
53
|
disallow_untyped_defs = true
|
|
48
54
|
strict_optional = true
|
|
49
55
|
|
|
56
|
+
[[tool.mypy.overrides]]
|
|
57
|
+
# Redis stubs don't exist for redis >5.0
|
|
58
|
+
module = "redis.asyncio"
|
|
59
|
+
ignore_missing_imports = true
|
|
60
|
+
|
|
50
61
|
[tool.pytest.ini_options]
|
|
51
62
|
python_files = ["test_*.py", "*_test.py"]
|
|
52
63
|
filterwarnings = ["ignore::DeprecationWarning"]
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
-
from datetime import UTC, datetime
|
|
3
|
+
from datetime import UTC, datetime, timezone
|
|
4
|
+
from email.utils import parsedate_to_datetime
|
|
4
5
|
from functools import cached_property
|
|
6
|
+
from math import ceil
|
|
7
|
+
from time import time
|
|
5
8
|
from typing import (
|
|
9
|
+
TYPE_CHECKING,
|
|
6
10
|
Any,
|
|
7
11
|
AsyncGenerator,
|
|
8
12
|
Callable,
|
|
@@ -29,6 +33,7 @@ from pydantic import (
|
|
|
29
33
|
from pydantic.alias_generators import to_camel
|
|
30
34
|
from pydantic.main import IncEx
|
|
31
35
|
|
|
36
|
+
from .cache import DictCache, RedisCache
|
|
32
37
|
from .endpoints.base_fields import IdField
|
|
33
38
|
from .exceptions import (
|
|
34
39
|
SWAPIDataValidationError,
|
|
@@ -37,22 +42,41 @@ from .exceptions import (
|
|
|
37
42
|
SWAPIException,
|
|
38
43
|
SWAPIGatewayTimeout,
|
|
39
44
|
SWAPIInternalServerError,
|
|
45
|
+
SWAPIRetryException,
|
|
40
46
|
SWAPIServiceUnavailable,
|
|
41
|
-
SWAPITooManyRequests,
|
|
42
47
|
SWFilterException,
|
|
43
48
|
SWNoClientProvided,
|
|
44
49
|
)
|
|
45
50
|
from .logging import logger
|
|
46
51
|
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from redis.asyncio import Redis
|
|
54
|
+
|
|
47
55
|
APPLICATION_JSON = "application/json"
|
|
48
56
|
|
|
49
57
|
EndpointClass = TypeVar("EndpointClass", bound="EndpointBase[Any]")
|
|
50
58
|
ModelClass = TypeVar("ModelClass", bound="ApiModelBase[Any]")
|
|
59
|
+
RETRY_CACHE_KEY = "shopware-api-client:retry:{url}:{method}"
|
|
60
|
+
HEADER_X_RATE_LIMIT_LIMIT = "X-Rate-Limit-Limit"
|
|
61
|
+
HEADER_X_RATE_LIMIT_REMAINING = "X-Rate-Limit-Remaining"
|
|
62
|
+
HEADER_X_RATE_LIMIT_RESET = "X-Rate-Limit-Reset"
|
|
51
63
|
|
|
52
64
|
|
|
53
65
|
class ConfigBase:
|
|
54
|
-
def __init__(
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
url: str,
|
|
69
|
+
retry_after_threshold: int = 60,
|
|
70
|
+
redis_client: "Redis | None" = None,
|
|
71
|
+
local_cache_cleanup_cycle_seconds: int = 10,
|
|
72
|
+
) -> None:
|
|
55
73
|
self.url = url.rstrip("/")
|
|
74
|
+
self.retry_after_threshold = retry_after_threshold
|
|
75
|
+
self.cache = (
|
|
76
|
+
RedisCache(redis_client)
|
|
77
|
+
if redis_client
|
|
78
|
+
else DictCache(cleanup_cycle_seconds=local_cache_cleanup_cycle_seconds)
|
|
79
|
+
)
|
|
56
80
|
|
|
57
81
|
|
|
58
82
|
class ClientBase:
|
|
@@ -60,8 +84,10 @@ class ClientBase:
|
|
|
60
84
|
raw: bool
|
|
61
85
|
language_id: IdField | None = None
|
|
62
86
|
|
|
63
|
-
def __init__(self, config: ConfigBase, raw: bool = False):
|
|
87
|
+
def __init__(self, config: ConfigBase, raw: bool = False) -> None:
|
|
64
88
|
self.api_url = config.url
|
|
89
|
+
self.retry_after_threshold = config.retry_after_threshold
|
|
90
|
+
self.cache = config.cache
|
|
65
91
|
self.raw = raw
|
|
66
92
|
|
|
67
93
|
async def __aenter__(self) -> "Self":
|
|
@@ -112,10 +138,46 @@ class ClientBase:
|
|
|
112
138
|
client = self.http_client
|
|
113
139
|
client.timeout = timeout # type: ignore
|
|
114
140
|
|
|
115
|
-
async def
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
141
|
+
async def sleep_and_increment(self, retry_wait_base: int, retry_count: int) -> int:
|
|
142
|
+
retry_count += 1
|
|
143
|
+
sleep_and_increment = retry_wait_base**retry_count
|
|
144
|
+
logger.debug(f"Try failed, retrying in {sleep_and_increment} seconds.")
|
|
145
|
+
await asyncio.sleep(sleep_and_increment)
|
|
146
|
+
return retry_count
|
|
147
|
+
|
|
148
|
+
def get_header_ts(self, header: str | None, fallback_time: float) -> float:
|
|
149
|
+
if header is None:
|
|
150
|
+
return fallback_time
|
|
151
|
+
|
|
152
|
+
server_dt = parsedate_to_datetime(header)
|
|
153
|
+
# ensure timezone-aware UTC
|
|
154
|
+
if server_dt.tzinfo is None:
|
|
155
|
+
server_dt = server_dt.replace(tzinfo=timezone.utc)
|
|
156
|
+
|
|
157
|
+
return server_dt.timestamp()
|
|
158
|
+
|
|
159
|
+
def parse_reset_time(self, headers: httpx.Headers) -> int:
|
|
160
|
+
"""Determine reset wait time based on server time"""
|
|
161
|
+
server_ts = self.get_header_ts(headers.get("Date"), time())
|
|
162
|
+
reset_ts = float(headers.get(HEADER_X_RATE_LIMIT_RESET, "0"))
|
|
163
|
+
adjusted_time = reset_ts - server_ts
|
|
164
|
+
|
|
165
|
+
return max(0, ceil(adjusted_time))
|
|
166
|
+
|
|
167
|
+
def parse_retry_after(self, headers: httpx.Headers) -> int:
|
|
168
|
+
retry_header: str | None = headers.get("Retry-After")
|
|
169
|
+
if retry_header is None:
|
|
170
|
+
return 0
|
|
171
|
+
|
|
172
|
+
if retry_header.isdigit():
|
|
173
|
+
return int(retry_header)
|
|
174
|
+
|
|
175
|
+
current_time = time()
|
|
176
|
+
server_ts = self.get_header_ts(headers.get("Date"), current_time)
|
|
177
|
+
retry_ts = self.get_header_ts(retry_header, current_time)
|
|
178
|
+
adjusted_time = retry_ts - server_ts
|
|
179
|
+
|
|
180
|
+
return max(0, ceil(adjusted_time))
|
|
119
181
|
|
|
120
182
|
async def _make_request(self, method: str, relative_url: str, **kwargs: Any) -> httpx.Response:
|
|
121
183
|
if relative_url.startswith("http://") or relative_url.startswith("https://"):
|
|
@@ -127,29 +189,82 @@ class ClientBase:
|
|
|
127
189
|
headers = self._get_headers()
|
|
128
190
|
headers.update(kwargs.pop("headers", {}))
|
|
129
191
|
|
|
192
|
+
retry_after_threshold = int(kwargs.pop("retry_after_threshold", self.retry_after_threshold))
|
|
130
193
|
retry_wait_base = int(kwargs.pop("retry_wait_base", 2))
|
|
131
194
|
retries = int(kwargs.pop("retries", 0))
|
|
132
195
|
retry_errors = tuple(
|
|
133
196
|
kwargs.pop("retry_errors", [SWAPIInternalServerError, SWAPIServiceUnavailable, SWAPIGatewayTimeout])
|
|
134
197
|
)
|
|
135
|
-
no_retry_errors = tuple(kwargs.pop("no_retry_errors", [
|
|
198
|
+
no_retry_errors = tuple(kwargs.pop("no_retry_errors", []))
|
|
136
199
|
|
|
137
200
|
kwargs.setdefault("follow_redirects", True)
|
|
138
201
|
|
|
202
|
+
key_base = RETRY_CACHE_KEY.format(
|
|
203
|
+
url=url.removeprefix("https://").removeprefix("http://"),
|
|
204
|
+
method=method,
|
|
205
|
+
)
|
|
206
|
+
x_retry_limit_cache_key = key_base + ":limit"
|
|
207
|
+
x_retry_remaining_cache_key = key_base + ":remaining"
|
|
208
|
+
x_retry_reset_cache_key = key_base + ":reset"
|
|
209
|
+
x_retry_lock_cache_key = key_base + ":lock"
|
|
210
|
+
got_lock = False
|
|
211
|
+
|
|
139
212
|
retry_count = 0
|
|
140
213
|
while True:
|
|
214
|
+
x_retry_remaining = await self.cache.get_and_decrement(x_retry_remaining_cache_key)
|
|
215
|
+
if x_retry_remaining is not None and x_retry_remaining <= 0 and not got_lock:
|
|
216
|
+
current_time = int(time())
|
|
217
|
+
reset_time = cast(int, await self.cache.get(x_retry_reset_cache_key)) or 0
|
|
218
|
+
wait_time = max(1, reset_time - current_time)
|
|
219
|
+
|
|
220
|
+
if wait_time > retry_after_threshold:
|
|
221
|
+
raise SWAPIRetryException(
|
|
222
|
+
f"Retry threshold exceeded for endpoint {url!r}. Threshold: {retry_after_threshold}s, Retry-After: {wait_time}s"
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
await asyncio.sleep(wait_time)
|
|
226
|
+
|
|
227
|
+
got_lock = await self.cache.has_lock(x_retry_lock_cache_key, wait_time)
|
|
228
|
+
continue
|
|
229
|
+
|
|
141
230
|
try:
|
|
142
231
|
response = await client.request(method, url, headers=headers, **kwargs)
|
|
143
232
|
except httpx.RequestError as exc:
|
|
144
233
|
if retry_count >= retries:
|
|
145
234
|
raise SWAPIException(f"HTTP client exception ({exc.__class__.__name__}). Details: {str(exc)}")
|
|
146
|
-
await
|
|
147
|
-
retry_count += 1
|
|
235
|
+
retry_count = await self.sleep_and_increment(retry_wait_base, retry_count)
|
|
148
236
|
continue
|
|
237
|
+
|
|
238
|
+
# Set retry-cache if headers are present
|
|
239
|
+
if rl_limit := response.headers.get(HEADER_X_RATE_LIMIT_LIMIT):
|
|
240
|
+
x_retry_limit = int(rl_limit)
|
|
241
|
+
wait_time = self.parse_reset_time(response.headers)
|
|
242
|
+
remaining_requests = int(response.headers.get(HEADER_X_RATE_LIMIT_REMAINING))
|
|
243
|
+
|
|
244
|
+
await asyncio.gather(
|
|
245
|
+
self.cache.set(x_retry_limit_cache_key, x_retry_limit),
|
|
246
|
+
self.cache.set(x_retry_reset_cache_key, int(time()) + wait_time, wait_time),
|
|
247
|
+
self.cache.set(x_retry_remaining_cache_key, remaining_requests),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if got_lock:
|
|
251
|
+
await self.cache.delete(x_retry_lock_cache_key)
|
|
252
|
+
got_lock = False
|
|
253
|
+
|
|
149
254
|
if response.status_code == 429:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
255
|
+
retry_wait_time = self.parse_retry_after(response.headers)
|
|
256
|
+
if retry_wait_time > retry_after_threshold:
|
|
257
|
+
error = SWAPIError.from_response(response)
|
|
258
|
+
raise SWAPIRetryException(
|
|
259
|
+
f"Retry threshold exceeded for endpoint {url!r}. Threshold: {retry_after_threshold}s, Retry-After: {retry_wait_time}s"
|
|
260
|
+
) from error
|
|
261
|
+
|
|
262
|
+
# If 429 is thrown, Retry-After == X-Rate-Limit-Reset
|
|
263
|
+
await asyncio.gather(
|
|
264
|
+
self.cache.set(x_retry_reset_cache_key, int(time()) + retry_wait_time, retry_wait_time),
|
|
265
|
+
asyncio.sleep(retry_wait_time),
|
|
266
|
+
)
|
|
267
|
+
|
|
153
268
|
elif response.status_code >= 400:
|
|
154
269
|
# retry other failure codes
|
|
155
270
|
try:
|
|
@@ -159,7 +274,7 @@ class ClientBase:
|
|
|
159
274
|
raise ValueError("`errors` attribute in json not a list/tuple!")
|
|
160
275
|
|
|
161
276
|
error: SWAPIError | SWAPIErrorList = SWAPIError.from_errors(errors) # type: ignore
|
|
162
|
-
except
|
|
277
|
+
except ValueError:
|
|
163
278
|
error: SWAPIError | SWAPIErrorList = SWAPIError.from_response(response) # type: ignore
|
|
164
279
|
|
|
165
280
|
if isinstance(error, SWAPIErrorList) and len(error.errors) == 1:
|
|
@@ -175,11 +290,10 @@ class ClientBase:
|
|
|
175
290
|
elif isinstance(error, no_retry_errors) or not isinstance(error, retry_errors):
|
|
176
291
|
raise error
|
|
177
292
|
|
|
178
|
-
if retry_count
|
|
293
|
+
if retry_count >= retries:
|
|
179
294
|
raise error
|
|
180
295
|
|
|
181
|
-
await self.
|
|
182
|
-
retry_count += 1
|
|
296
|
+
retry_count = await self.sleep_and_increment(retry_wait_base, retry_count)
|
|
183
297
|
elif response.status_code == 200 and response.headers.get("Content-Type", "").startswith(APPLICATION_JSON):
|
|
184
298
|
# guard against "200 okay" responses with malformed json
|
|
185
299
|
try:
|
|
@@ -196,9 +310,7 @@ class ClientBase:
|
|
|
196
310
|
)
|
|
197
311
|
raise exception
|
|
198
312
|
|
|
199
|
-
|
|
200
|
-
await self.retry_sleep(retry_wait_base, retry_count)
|
|
201
|
-
retry_count += 1
|
|
313
|
+
retry_count = await self.sleep_and_increment(retry_wait_base, retry_count)
|
|
202
314
|
else:
|
|
203
315
|
return response
|
|
204
316
|
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from collections import OrderedDict
|
|
4
|
+
from time import time
|
|
5
|
+
from typing import Any, Awaitable, NamedTuple, cast
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from redis.asyncio import Redis
|
|
9
|
+
|
|
10
|
+
_has_redis = True
|
|
11
|
+
except ModuleNotFoundError:
|
|
12
|
+
_has_redis = False
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class CacheBase(ABC):
|
|
16
|
+
@abstractmethod
|
|
17
|
+
async def get(self, key: str) -> Any | None:
|
|
18
|
+
...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
async def get_and_decrement(self, key: str) -> int | None:
|
|
22
|
+
...
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
async def has_lock(self, key: str, ttl: int) -> bool:
|
|
26
|
+
...
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
30
|
+
...
|
|
31
|
+
|
|
32
|
+
@abstractmethod
|
|
33
|
+
async def delete(self, key: str) -> None:
|
|
34
|
+
...
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def _json_encode(key: str, value: Any) -> str:
|
|
38
|
+
try:
|
|
39
|
+
return json.dumps(value)
|
|
40
|
+
except (TypeError, ValueError) as e:
|
|
41
|
+
raise ValueError(f"Value {value!r} for cache key {key!r} must be JSON-serializable: {e}") from e
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def _json_decode(raw_value: Any) -> Any | None:
|
|
45
|
+
if raw_value is None:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(raw_value)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
# If write was broken, mimic missing key
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class RedisCache(CacheBase):
|
|
56
|
+
_DECR_IF_EXISTS = """
|
|
57
|
+
if redis.call('EXISTS', KEYS[1]) == 1 then
|
|
58
|
+
return redis.call('DECR', KEYS[1])
|
|
59
|
+
else
|
|
60
|
+
return nil
|
|
61
|
+
end
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(self, redis_client: "Redis") -> None:
|
|
65
|
+
if not _has_redis:
|
|
66
|
+
raise RuntimeError("Redis needs to be installed to use it as a cache.")
|
|
67
|
+
|
|
68
|
+
self.client = redis_client
|
|
69
|
+
|
|
70
|
+
async def get(self, key: str) -> Any | None:
|
|
71
|
+
value = await self.client.get(key)
|
|
72
|
+
return self._json_decode(value)
|
|
73
|
+
|
|
74
|
+
async def get_and_decrement(self, key: str) -> int | None:
|
|
75
|
+
return_value = await cast(Awaitable[int | None], self.client.eval(self._DECR_IF_EXISTS, 1, key))
|
|
76
|
+
return None if return_value is None else return_value + 1
|
|
77
|
+
|
|
78
|
+
async def has_lock(self, key: str, ttl: int) -> bool:
|
|
79
|
+
return await self.client.set(key, 1, ex=ttl, nx=True) or False
|
|
80
|
+
|
|
81
|
+
async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
82
|
+
await self.client.set(name=key, value=self._json_encode(key, value), ex=ttl)
|
|
83
|
+
|
|
84
|
+
async def delete(self, key: str) -> None:
|
|
85
|
+
await self.client.delete(key)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class DictCache(CacheBase):
|
|
89
|
+
class CacheValue(NamedTuple):
|
|
90
|
+
value: Any
|
|
91
|
+
expire_at: int | None
|
|
92
|
+
|
|
93
|
+
def __init__(self, cleanup_cycle_seconds: int) -> None:
|
|
94
|
+
self._cache: OrderedDict[str, DictCache.CacheValue] = OrderedDict()
|
|
95
|
+
self.cleanup_cycle_seconds = cleanup_cycle_seconds
|
|
96
|
+
self.next_cleanup: int = 0
|
|
97
|
+
|
|
98
|
+
async def get(self, key: str) -> Any | None:
|
|
99
|
+
self._cleanup_by_expiry()
|
|
100
|
+
entry = self._cache.get(key)
|
|
101
|
+
|
|
102
|
+
return self._json_decode(entry and entry.value)
|
|
103
|
+
|
|
104
|
+
async def get_and_decrement(self, key: str) -> int | None:
|
|
105
|
+
self._cleanup_by_expiry()
|
|
106
|
+
entry = self._cache.get(key)
|
|
107
|
+
if entry is None:
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
decoded_value = self._json_decode(entry.value)
|
|
111
|
+
if not isinstance(decoded_value, int):
|
|
112
|
+
raise ValueError(f"Trying to decrement key {key!r}, but value {decoded_value!r} is not an int")
|
|
113
|
+
|
|
114
|
+
new_value = decoded_value - 1
|
|
115
|
+
self._cache[key] = DictCache.CacheValue(self._json_encode(key, new_value), entry.expire_at)
|
|
116
|
+
|
|
117
|
+
return decoded_value
|
|
118
|
+
|
|
119
|
+
async def has_lock(self, key: str, ttl: int) -> bool:
|
|
120
|
+
self._cleanup_by_expiry(False)
|
|
121
|
+
entry = self._cache.get(key)
|
|
122
|
+
if unlocked := entry is None:
|
|
123
|
+
self._cache[key] = DictCache.CacheValue(True, self.get_expiry(ttl))
|
|
124
|
+
|
|
125
|
+
return unlocked
|
|
126
|
+
|
|
127
|
+
async def set(self, key: str, value: Any, ttl: int | None = None) -> None:
|
|
128
|
+
self._cleanup_by_expiry()
|
|
129
|
+
cache_value = self._json_encode(key, value)
|
|
130
|
+
|
|
131
|
+
self._cache[key] = DictCache.CacheValue(cache_value, self.get_expiry(ttl))
|
|
132
|
+
|
|
133
|
+
async def delete(self, key: str) -> None:
|
|
134
|
+
self._cleanup_by_expiry()
|
|
135
|
+
self._cache.pop(key, None)
|
|
136
|
+
|
|
137
|
+
def get_expiry(self, ttl: int | None) -> int | None:
|
|
138
|
+
return int(time()) + ttl if ttl is not None else None
|
|
139
|
+
|
|
140
|
+
def _cleanup_by_expiry(self, do_wait: bool = True) -> None:
|
|
141
|
+
now = int(time())
|
|
142
|
+
|
|
143
|
+
if now < self.next_cleanup and do_wait:
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
initial_size = len(self._cache)
|
|
147
|
+
for _ in range(initial_size):
|
|
148
|
+
if not self._cache:
|
|
149
|
+
break
|
|
150
|
+
|
|
151
|
+
first_key, first_value = next(iter(self._cache.items()))
|
|
152
|
+
|
|
153
|
+
if first_value.expire_at is None:
|
|
154
|
+
self._cache.move_to_end(first_key)
|
|
155
|
+
continue
|
|
156
|
+
|
|
157
|
+
if first_value.expire_at > now:
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
self._cache.popitem(last=False)
|
|
161
|
+
|
|
162
|
+
self.next_cleanup = now + self.cleanup_cycle_seconds
|
{shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/config.py
RENAMED
|
@@ -14,6 +14,7 @@ class AdminConfig(ConfigBase):
|
|
|
14
14
|
client_secret: str | None = None,
|
|
15
15
|
grant_type: str = "client_credentials",
|
|
16
16
|
extra: dict[str, Any] | None = None,
|
|
17
|
+
**kwargs: Any,
|
|
17
18
|
) -> None:
|
|
18
19
|
match grant_type:
|
|
19
20
|
case "client_credentials":
|
|
@@ -27,7 +28,7 @@ class AdminConfig(ConfigBase):
|
|
|
27
28
|
case _:
|
|
28
29
|
raise SWAPIConfigException("Invalid 'grant_type'. Must be one of: 'client_credentials', 'password'")
|
|
29
30
|
|
|
30
|
-
super().__init__(url=url)
|
|
31
|
+
super().__init__(url=url, **kwargs)
|
|
31
32
|
self.username = username
|
|
32
33
|
self.password = password
|
|
33
34
|
self.client_id = client_id
|
|
@@ -37,7 +38,7 @@ class AdminConfig(ConfigBase):
|
|
|
37
38
|
|
|
38
39
|
|
|
39
40
|
class StoreConfig(ConfigBase):
|
|
40
|
-
def __init__(self, url: str, access_key: str, context_token: str | None = None):
|
|
41
|
-
super().__init__(url=url)
|
|
41
|
+
def __init__(self, url: str, access_key: str, context_token: str | None = None, **kwargs: Any):
|
|
42
|
+
super().__init__(url=url, **kwargs)
|
|
42
43
|
self.access_key = access_key
|
|
43
44
|
self.context_token = context_token
|
{shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/exceptions.py
RENAMED
|
@@ -24,6 +24,10 @@ class SWAPIConfigException(SWAPIException):
|
|
|
24
24
|
pass
|
|
25
25
|
|
|
26
26
|
|
|
27
|
+
class SWAPIRetryException(SWAPIException):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
|
|
27
31
|
class SWAPIMethodNotAvailable(SWAPIConfigException):
|
|
28
32
|
def __init__(self, msg: str | None = None, *args: list[Any], **kwargs: dict[Any, Any]) -> None:
|
|
29
33
|
if not msg:
|
|
@@ -95,7 +99,13 @@ class SWAPIError(SWAPIException):
|
|
|
95
99
|
@classmethod
|
|
96
100
|
def from_response(cls, response: Response) -> "SWAPIError":
|
|
97
101
|
exception_class = cls.get_exception_class(response.status_code)
|
|
98
|
-
|
|
102
|
+
|
|
103
|
+
try:
|
|
104
|
+
response.headers["requested-url"] = str(response.request.url)
|
|
105
|
+
except RuntimeError:
|
|
106
|
+
# If the request URL is not available, we can ignore it.
|
|
107
|
+
pass
|
|
108
|
+
|
|
99
109
|
return exception_class(
|
|
100
110
|
status=response.status_code,
|
|
101
111
|
title=response.reason_phrase,
|
|
File without changes
|
{shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/__init__.py
RENAMED
|
File without changes
|
{shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/client.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/logging.py
RENAMED
|
File without changes
|
{shopware_api_client-1.0.107 → shopware_api_client-1.0.109}/src/shopware_api_client/py.typed
RENAMED
|
File without changes
|