etsy-python 1.2.2__tar.gz → 1.2.4__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.
- {etsy_python-1.2.2/etsy_python.egg-info → etsy_python-1.2.4}/PKG-INFO +3 -3
- {etsy_python-1.2.2 → etsy_python-1.2.4}/README.md +2 -2
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/_version.py +1 -1
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/common/Request.py +2 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/common/Utils.py +16 -1
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Listing.py +35 -2
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Listing.py +11 -8
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingInventory.py +2 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingVideo.py +16 -7
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Payment.py +3 -2
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Session.py +61 -23
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/UserAddress.py +9 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4/etsy_python.egg-info}/PKG-INFO +3 -3
- {etsy_python-1.2.2 → etsy_python-1.2.4}/LICENSE +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/MANIFEST.in +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/auth/OAuth.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/auth/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/common/Env.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/common/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/HolidayPreferences.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/Language.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/Listing.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/ListingInventory.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/ProcessingProfile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/ShippingProfile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/ShopReceipt.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/enums/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/exceptions/BaseAPIException.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/exceptions/RequestException.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/exceptions/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/FileRequest.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/HolidayPreferences.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Miscellaneous.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/ProcessingProfile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Product.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Receipt.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Request.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/ShippingProfile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Shop.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/ShopReturnPolicy.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/Utils.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/models/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/HolidayPreferences.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingFile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingImage.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingOffering.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingProduct.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingTranslation.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ListingVariationImages.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Miscellaneous.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/PaymentLedgerEntry.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ProcessingProfile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Receipt.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ReceiptTransactions.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Response.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Review.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ShippingProfile.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Shop.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ShopProductionPartner.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ShopReturnPolicy.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/ShopSection.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/Taxonomy.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/User.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/enums/RateLimit.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/enums/Request.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python/v3/resources/enums/__init__.py +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python.egg-info/SOURCES.txt +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python.egg-info/dependency_links.txt +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python.egg-info/requires.txt +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/etsy_python.egg-info/top_level.txt +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/setup.cfg +0 -0
- {etsy_python-1.2.2 → etsy_python-1.2.4}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: etsy-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Etsy API Client Library for Python
|
|
5
5
|
Author: Amit Ray
|
|
6
6
|
Author-email: mail@amitray.dev
|
|
@@ -181,7 +181,7 @@ token_data = oauth.get_access_token()
|
|
|
181
181
|
### 2. Initialize Client Session
|
|
182
182
|
|
|
183
183
|
```python
|
|
184
|
-
from datetime import datetime
|
|
184
|
+
from datetime import datetime, timedelta, timezone
|
|
185
185
|
from etsy_python.v3.resources.Session import EtsyClient
|
|
186
186
|
|
|
187
187
|
# Create client with tokens
|
|
@@ -189,7 +189,7 @@ client = EtsyClient(
|
|
|
189
189
|
keystring="your_api_key",
|
|
190
190
|
access_token=token_data["access_token"],
|
|
191
191
|
refresh_token=token_data["refresh_token"],
|
|
192
|
-
expiry=datetime.
|
|
192
|
+
expiry=datetime.now(tz=timezone.utc) + timedelta(seconds=token_data["expires_in"])
|
|
193
193
|
)
|
|
194
194
|
```
|
|
195
195
|
|
|
@@ -144,7 +144,7 @@ token_data = oauth.get_access_token()
|
|
|
144
144
|
### 2. Initialize Client Session
|
|
145
145
|
|
|
146
146
|
```python
|
|
147
|
-
from datetime import datetime
|
|
147
|
+
from datetime import datetime, timedelta, timezone
|
|
148
148
|
from etsy_python.v3.resources.Session import EtsyClient
|
|
149
149
|
|
|
150
150
|
# Create client with tokens
|
|
@@ -152,7 +152,7 @@ client = EtsyClient(
|
|
|
152
152
|
keystring="your_api_key",
|
|
153
153
|
access_token=token_data["access_token"],
|
|
154
154
|
refresh_token=token_data["refresh_token"],
|
|
155
|
-
expiry=datetime.
|
|
155
|
+
expiry=datetime.now(tz=timezone.utc) + timedelta(seconds=token_data["expires_in"])
|
|
156
156
|
)
|
|
157
157
|
```
|
|
158
158
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import warnings
|
|
2
2
|
from enum import Enum
|
|
3
|
-
from typing import Any, Dict, List, Optional
|
|
3
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
def warn_removed_legacy_param(operation_id: str) -> None:
|
|
@@ -25,6 +25,21 @@ def warn_removed_legacy_param(operation_id: str) -> None:
|
|
|
25
25
|
)
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def join_ids(name: str, ids: Iterable[Any]) -> str:
|
|
29
|
+
"""Join a required list of IDs into Etsy's comma-separated query value.
|
|
30
|
+
|
|
31
|
+
Raises ``TypeError`` for a string (iterating it would split the ID into
|
|
32
|
+
characters) and ``ValueError`` when no IDs are given, so a request Etsy
|
|
33
|
+
would reject is caught before it is sent.
|
|
34
|
+
"""
|
|
35
|
+
if isinstance(ids, (str, bytes)):
|
|
36
|
+
raise TypeError(f"{name} must be a list of IDs, not a string")
|
|
37
|
+
joined = ",".join(map(str, ids)) if ids is not None else ""
|
|
38
|
+
if not joined:
|
|
39
|
+
raise ValueError(f"{name} is required and must contain at least one ID")
|
|
40
|
+
return joined
|
|
41
|
+
|
|
42
|
+
|
|
28
43
|
def generate_get_uri(uri: str, params: Optional[Dict[str, Any]] = None) -> str:
|
|
29
44
|
if not params:
|
|
30
45
|
return uri
|
|
@@ -57,6 +57,28 @@ def _warn_if_personalization_used(
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
_FEATURED_RANK_DEPRECATION_MSG = (
|
|
61
|
+
"featured_rank was removed from updateListing by the Etsy API and is no "
|
|
62
|
+
"longer sent; it will be dropped from UpdateListingRequest in a future "
|
|
63
|
+
"major version."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _warn_if_featured_rank_used(featured_rank: Optional[int]) -> None:
|
|
68
|
+
"""Emit DeprecationWarning when the removed featured_rank field is set.
|
|
69
|
+
|
|
70
|
+
``0`` does not warn: it is the default a fetched listing reports, so
|
|
71
|
+
callers that round-trip a listing into an update would otherwise warn on
|
|
72
|
+
every call. This matches the personalization fields, which skip falsy
|
|
73
|
+
values. ``stacklevel=3`` points at the caller of the model ``__init__`` or
|
|
74
|
+
the ``featured_rank`` setter.
|
|
75
|
+
"""
|
|
76
|
+
if featured_rank:
|
|
77
|
+
warnings.warn(
|
|
78
|
+
_FEATURED_RANK_DEPRECATION_MSG, DeprecationWarning, stacklevel=3
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
|
|
60
82
|
class _PersonalizationFieldsMixin:
|
|
61
83
|
"""Accessors for the personalization fields Etsy removed from listings.
|
|
62
84
|
|
|
@@ -310,7 +332,6 @@ class UpdateListingRequest(_PersonalizationFieldsMixin, _ECGTFieldsMixin, Reques
|
|
|
310
332
|
"item_weight_unit",
|
|
311
333
|
"item_dimensions_unit",
|
|
312
334
|
"tags",
|
|
313
|
-
"featured_rank",
|
|
314
335
|
"production_partner_ids",
|
|
315
336
|
"_type",
|
|
316
337
|
*ECGT_FIELDS,
|
|
@@ -375,7 +396,10 @@ class UpdateListingRequest(_PersonalizationFieldsMixin, _ECGTFieldsMixin, Reques
|
|
|
375
396
|
self.tags = tags
|
|
376
397
|
self.who_made = who_made
|
|
377
398
|
self.when_made = when_made
|
|
378
|
-
|
|
399
|
+
# Removed from the updateListing body by Etsy: stored under an
|
|
400
|
+
# underscore name so todict never sends it.
|
|
401
|
+
self._featured_rank = featured_rank
|
|
402
|
+
_warn_if_featured_rank_used(featured_rank)
|
|
379
403
|
self._store_personalization(
|
|
380
404
|
is_personalizable,
|
|
381
405
|
personalization_is_required,
|
|
@@ -400,6 +424,15 @@ class UpdateListingRequest(_PersonalizationFieldsMixin, _ECGTFieldsMixin, Reques
|
|
|
400
424
|
mandatory=UpdateListingRequest.mandatory,
|
|
401
425
|
)
|
|
402
426
|
|
|
427
|
+
@property
|
|
428
|
+
def featured_rank(self) -> Optional[int]:
|
|
429
|
+
return self._featured_rank
|
|
430
|
+
|
|
431
|
+
@featured_rank.setter
|
|
432
|
+
def featured_rank(self, value: Optional[int]) -> None:
|
|
433
|
+
self._featured_rank = value
|
|
434
|
+
_warn_if_featured_rank_used(value)
|
|
435
|
+
|
|
403
436
|
|
|
404
437
|
class UpdateListingInventoryRequest(Request):
|
|
405
438
|
nullable: List[str] = ["readiness_state_on_property"]
|
|
@@ -2,7 +2,7 @@ import warnings
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Optional, List, Dict, Any, Union
|
|
4
4
|
|
|
5
|
-
from etsy_python.v3.common.Utils import warn_removed_legacy_param
|
|
5
|
+
from etsy_python.v3.common.Utils import join_ids, warn_removed_legacy_param
|
|
6
6
|
from etsy_python.v3.enums.Listing import Includes, State, SortOn, SortOrder
|
|
7
7
|
from etsy_python.v3.exceptions.RequestException import RequestException
|
|
8
8
|
from etsy_python.v3.models.Listing import (
|
|
@@ -150,7 +150,7 @@ class ListingResource:
|
|
|
150
150
|
) -> Union[Response, RequestException]:
|
|
151
151
|
endpoint = "/listings/batch"
|
|
152
152
|
query_params: Dict[str, Any] = {
|
|
153
|
-
"listing_ids": "
|
|
153
|
+
"listing_ids": join_ids("listing_ids", listing_ids),
|
|
154
154
|
"includes": ",".join(list(map(lambda inc: inc.value, includes)))
|
|
155
155
|
if includes is not None
|
|
156
156
|
else None,
|
|
@@ -179,7 +179,7 @@ class ListingResource:
|
|
|
179
179
|
) -> Union[Response, RequestException]:
|
|
180
180
|
endpoint = "/listings/batch/inventory"
|
|
181
181
|
query_params: Dict[str, Any] = {
|
|
182
|
-
"listing_ids": "
|
|
182
|
+
"listing_ids": join_ids("listing_ids", listing_ids),
|
|
183
183
|
}
|
|
184
184
|
return self.session.make_request(endpoint, query_params=query_params)
|
|
185
185
|
|
|
@@ -188,7 +188,7 @@ class ListingResource:
|
|
|
188
188
|
) -> Union[Response, RequestException]:
|
|
189
189
|
endpoint = "/listings/batch/shipping"
|
|
190
190
|
query_params: Dict[str, Any] = {
|
|
191
|
-
"listing_ids": "
|
|
191
|
+
"listing_ids": join_ids("listing_ids", listing_ids),
|
|
192
192
|
}
|
|
193
193
|
return self.session.make_request(endpoint, query_params=query_params)
|
|
194
194
|
|
|
@@ -260,18 +260,21 @@ class ListingResource:
|
|
|
260
260
|
def get_listings_by_shop_section_id(
|
|
261
261
|
self,
|
|
262
262
|
shop_id: int,
|
|
263
|
-
shop_section_ids:
|
|
263
|
+
shop_section_ids: List[int],
|
|
264
264
|
limit: int = 25,
|
|
265
265
|
offset: int = 0,
|
|
266
266
|
sort_on: SortOn = SortOn.CREATED,
|
|
267
267
|
sort_order: SortOrder = SortOrder.DESC,
|
|
268
268
|
legacy: Optional[bool] = None,
|
|
269
269
|
) -> Union[Response, RequestException]:
|
|
270
|
+
"""Get listings in the given shop sections.
|
|
271
|
+
|
|
272
|
+
Raises ``ValueError`` if ``shop_section_ids`` is empty and
|
|
273
|
+
``TypeError`` if it is a string, before any request is sent.
|
|
274
|
+
"""
|
|
270
275
|
endpoint = f"/shops/{shop_id}/shop-sections/listings"
|
|
271
276
|
query_params: Dict[str, Any] = {
|
|
272
|
-
"shop_section_ids": "
|
|
273
|
-
if shop_section_ids is not None
|
|
274
|
-
else None,
|
|
277
|
+
"shop_section_ids": join_ids("shop_section_ids", shop_section_ids),
|
|
275
278
|
"limit": limit,
|
|
276
279
|
"offset": offset,
|
|
277
280
|
"sort_on": sort_on.value,
|
|
@@ -35,6 +35,7 @@ class ListingInventoryResource:
|
|
|
35
35
|
self, listing_id: int, listing_inventory: UpdateListingInventoryRequest,
|
|
36
36
|
legacy: Optional[bool] = None,
|
|
37
37
|
max_variations_supported: Optional[MaxVariationsSupported] = None,
|
|
38
|
+
enable_scale_validation: Optional[bool] = None,
|
|
38
39
|
) -> Union[Response, RequestException]:
|
|
39
40
|
if legacy is not None:
|
|
40
41
|
warn_removed_legacy_param("updateListingInventory")
|
|
@@ -43,6 +44,7 @@ class ListingInventoryResource:
|
|
|
43
44
|
"max_variations_supported": max_variations_supported.value
|
|
44
45
|
if max_variations_supported is not None
|
|
45
46
|
else None,
|
|
47
|
+
"enable_scale_validation": enable_scale_validation,
|
|
46
48
|
}
|
|
47
49
|
return self.session.make_request(
|
|
48
50
|
endpoint, method=Method.PUT, payload=listing_inventory,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from typing import Optional, Union
|
|
3
4
|
|
|
@@ -37,14 +38,22 @@ class ListingVideoResource:
|
|
|
37
38
|
) -> Union[Response, RequestException]:
|
|
38
39
|
"""Upload a video to a listing.
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
upload replaces
|
|
41
|
+
``is_multi_video`` is deprecated: Etsy removed it from
|
|
42
|
+
uploadListingVideo, so any value is discarded and not sent, and Etsy
|
|
43
|
+
no longer documents whether an upload replaces an existing video. To
|
|
44
|
+
replace one, remove it with ``delete_listing_video`` before uploading
|
|
45
|
+
the new video. Etsy limits uploads to 10 per listing per day.
|
|
43
46
|
"""
|
|
47
|
+
if is_multi_video is not None:
|
|
48
|
+
warnings.warn(
|
|
49
|
+
"'is_multi_video' was removed from uploadListingVideo by Etsy "
|
|
50
|
+
"and is no longer sent; to replace a video, remove it with "
|
|
51
|
+
"delete_listing_video first. The argument will be dropped from "
|
|
52
|
+
"this method in a future major version.",
|
|
53
|
+
DeprecationWarning,
|
|
54
|
+
stacklevel=2,
|
|
55
|
+
)
|
|
44
56
|
endpoint = f"/shops/{shop_id}/listings/{listing_id}/videos"
|
|
45
57
|
return self.session.make_request(
|
|
46
|
-
endpoint,
|
|
47
|
-
method=Method.POST,
|
|
48
|
-
payload=listing_video,
|
|
49
|
-
query_params={"is_multi_video": is_multi_video},
|
|
58
|
+
endpoint, method=Method.POST, payload=listing_video
|
|
50
59
|
)
|
|
@@ -2,6 +2,7 @@ import warnings
|
|
|
2
2
|
from dataclasses import dataclass
|
|
3
3
|
from typing import Union, Dict, Any, List
|
|
4
4
|
|
|
5
|
+
from etsy_python.v3.common.Utils import join_ids
|
|
5
6
|
from etsy_python.v3.exceptions.RequestException import RequestException
|
|
6
7
|
from etsy_python.v3.resources.Session import EtsyClient
|
|
7
8
|
from etsy_python.v3.resources.Response import Response
|
|
@@ -16,7 +17,7 @@ class PaymentResource:
|
|
|
16
17
|
) -> Union[Response, RequestException]:
|
|
17
18
|
endpoint = f"/shops/{shop_id}/payment-account/ledger-entries/payments"
|
|
18
19
|
query_params: Dict[str, Any] = {
|
|
19
|
-
"ledger_entry_ids": "
|
|
20
|
+
"ledger_entry_ids": join_ids("ledger_entry_ids", ledger_entry_ids)
|
|
20
21
|
}
|
|
21
22
|
return self.session.make_request(endpoint, query_params=query_params)
|
|
22
23
|
|
|
@@ -42,5 +43,5 @@ class PaymentResource:
|
|
|
42
43
|
self, shop_id: int, payment_ids: List[int]
|
|
43
44
|
) -> Union[Response, RequestException]:
|
|
44
45
|
endpoint = f"/shops/{shop_id}/payments"
|
|
45
|
-
query_params: Dict[str, Any] = {"payment_ids": "
|
|
46
|
+
query_params: Dict[str, Any] = {"payment_ids": join_ids("payment_ids", payment_ids)}
|
|
46
47
|
return self.session.make_request(endpoint, query_params=query_params)
|
|
@@ -3,7 +3,7 @@ from typing import Any, Callable, Dict, Optional
|
|
|
3
3
|
|
|
4
4
|
from requests import Session
|
|
5
5
|
|
|
6
|
-
from etsy_python.v3.common.Request import
|
|
6
|
+
from etsy_python.v3.common.Request import NO_RESPONSE_CODES
|
|
7
7
|
from etsy_python.v3.common.Env import environment
|
|
8
8
|
from etsy_python.v3.common.Utils import generate_get_uri
|
|
9
9
|
from etsy_python.v3.resources.enums.Request import Method
|
|
@@ -35,22 +35,51 @@ class EtsyClient:
|
|
|
35
35
|
self.session.headers = self._get_resource_headers(keystring, access_token)
|
|
36
36
|
|
|
37
37
|
def update_token(self) -> tuple:
|
|
38
|
-
self._remove_authorization_header()
|
|
39
38
|
refresh_json = self._get_refresh_json(self.keystring, self.refresh_token)
|
|
40
|
-
|
|
39
|
+
# Drop Authorization for this call only. The shared session header is
|
|
40
|
+
# never touched, so a failed refresh cannot leave later requests
|
|
41
|
+
# unauthenticated.
|
|
42
|
+
response = self.session.post(
|
|
43
|
+
environment.token_url, json=refresh_json, headers={"Authorization": None}
|
|
44
|
+
)
|
|
41
45
|
response_json = self._process_request(response).message
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
if not isinstance(response_json, dict):
|
|
47
|
+
raise RequestException(
|
|
48
|
+
code=response.status_code,
|
|
49
|
+
error="Token refresh returned a non-JSON body",
|
|
50
|
+
)
|
|
51
|
+
access_token = response_json.get("access_token")
|
|
52
|
+
refresh_token = response_json.get("refresh_token")
|
|
53
|
+
expires_in = response_json.get("expires_in")
|
|
54
|
+
# expires_in must be a positive int; bool is an int subclass, and
|
|
55
|
+
# floats allow NaN/inf, which timedelta cannot handle.
|
|
56
|
+
invalid = [
|
|
57
|
+
name
|
|
58
|
+
for name, ok in (
|
|
59
|
+
("access_token", bool(access_token)),
|
|
60
|
+
("refresh_token", bool(refresh_token)),
|
|
61
|
+
(
|
|
62
|
+
"expires_in",
|
|
63
|
+
isinstance(expires_in, int)
|
|
64
|
+
and not isinstance(expires_in, bool)
|
|
65
|
+
and 0 < expires_in <= 10**9,
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
if not ok
|
|
69
|
+
]
|
|
70
|
+
if invalid:
|
|
46
71
|
raise RequestException(
|
|
47
|
-
code=401,
|
|
72
|
+
code=401,
|
|
73
|
+
error=response_json.get("error", "Something went wrong!"),
|
|
74
|
+
error_description=response_json.get("error_description")
|
|
75
|
+
or "Token refresh response has missing or invalid: "
|
|
76
|
+
+ ", ".join(invalid),
|
|
48
77
|
)
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
self.expiry =
|
|
79
|
+
# Store the new tokens only once the whole response is valid.
|
|
80
|
+
self.access_token = access_token
|
|
81
|
+
self.refresh_token = refresh_token
|
|
82
|
+
self.expiry = datetime.now(tz=timezone.utc) + timedelta(seconds=expires_in)
|
|
54
83
|
|
|
55
84
|
self.session.headers.update(
|
|
56
85
|
self._prepare_authorization_token(self.access_token)
|
|
@@ -70,9 +99,6 @@ class EtsyClient:
|
|
|
70
99
|
def _prepare_authorization_token(access_token: str) -> dict:
|
|
71
100
|
return {"Authorization": f"Bearer {access_token}"}
|
|
72
101
|
|
|
73
|
-
def _remove_authorization_header(self) -> None:
|
|
74
|
-
self.session.headers.pop("Authorization", None)
|
|
75
|
-
|
|
76
102
|
@staticmethod
|
|
77
103
|
def _get_request_headers(keystring: str) -> dict:
|
|
78
104
|
return {"Accept": "application/json", "x-api-key": keystring}
|
|
@@ -132,21 +158,26 @@ class EtsyClient:
|
|
|
132
158
|
return self._process_request(response)
|
|
133
159
|
|
|
134
160
|
def _process_request(self, response: Any) -> Any:
|
|
135
|
-
|
|
161
|
+
# Any 4xx/5xx is an error, including codes Etsy documents later
|
|
162
|
+
# (e.g. 429 for the uploadListingVideo daily limit).
|
|
163
|
+
is_error = response.status_code >= 400
|
|
136
164
|
|
|
137
165
|
rate_limits = None
|
|
138
166
|
if "X-Limit-Per-Day" in response.headers:
|
|
139
167
|
rate_limits = RateLimit(
|
|
140
|
-
response.headers
|
|
141
|
-
response.headers
|
|
142
|
-
response.headers
|
|
143
|
-
response.headers
|
|
168
|
+
response.headers.get("X-Limit-Per-Second"),
|
|
169
|
+
response.headers.get("X-Remaining-This-Second"),
|
|
170
|
+
response.headers.get("X-Limit-Per-Day"),
|
|
171
|
+
response.headers.get("X-Remaining-Today"),
|
|
144
172
|
)
|
|
145
173
|
|
|
146
|
-
response_json = (
|
|
147
|
-
response.json() if response.status_code not in NO_RESPONSE_CODES else None
|
|
148
|
-
)
|
|
149
174
|
if is_error:
|
|
175
|
+
try:
|
|
176
|
+
response_json = response.json()
|
|
177
|
+
except ValueError:
|
|
178
|
+
# Gateway and method errors (405, 502, 504) can return a
|
|
179
|
+
# non-JSON or empty body; keep the text, or the reason phrase.
|
|
180
|
+
response_json = response.text or response.reason
|
|
150
181
|
error_response = (
|
|
151
182
|
response_json
|
|
152
183
|
if isinstance(response_json, dict)
|
|
@@ -160,6 +191,13 @@ class EtsyClient:
|
|
|
160
191
|
error_description,
|
|
161
192
|
rate_limits=rate_limits,
|
|
162
193
|
)
|
|
194
|
+
response_json = None
|
|
195
|
+
if response.status_code not in NO_RESPONSE_CODES:
|
|
196
|
+
try:
|
|
197
|
+
response_json = response.json()
|
|
198
|
+
except ValueError:
|
|
199
|
+
# A success with an empty or non-JSON body (202, 304, ...).
|
|
200
|
+
response_json = response.text
|
|
163
201
|
return Response(
|
|
164
202
|
response.status_code, response_json or "OK", rate_limits=rate_limits
|
|
165
203
|
)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import warnings
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from typing import Union, Dict, Any
|
|
3
4
|
|
|
@@ -14,6 +15,14 @@ class UserAddressResource:
|
|
|
14
15
|
def delete_user_address(
|
|
15
16
|
self, user_address_id: int
|
|
16
17
|
) -> Union[Response, RequestException]:
|
|
18
|
+
"""Deprecated: Etsy removed deleteUserAddress from the Open API v3 spec."""
|
|
19
|
+
warnings.warn(
|
|
20
|
+
"delete_user_address is deprecated: Etsy removed deleteUserAddress "
|
|
21
|
+
"from the Open API v3 spec, so the request may fail. It will be "
|
|
22
|
+
"removed in a future major version.",
|
|
23
|
+
DeprecationWarning,
|
|
24
|
+
stacklevel=2,
|
|
25
|
+
)
|
|
17
26
|
endpoint = f"/user/addresses/{user_address_id}"
|
|
18
27
|
return self.session.make_request(endpoint, method=Method.DELETE)
|
|
19
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: etsy-python
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.4
|
|
4
4
|
Summary: Etsy API Client Library for Python
|
|
5
5
|
Author: Amit Ray
|
|
6
6
|
Author-email: mail@amitray.dev
|
|
@@ -181,7 +181,7 @@ token_data = oauth.get_access_token()
|
|
|
181
181
|
### 2. Initialize Client Session
|
|
182
182
|
|
|
183
183
|
```python
|
|
184
|
-
from datetime import datetime
|
|
184
|
+
from datetime import datetime, timedelta, timezone
|
|
185
185
|
from etsy_python.v3.resources.Session import EtsyClient
|
|
186
186
|
|
|
187
187
|
# Create client with tokens
|
|
@@ -189,7 +189,7 @@ client = EtsyClient(
|
|
|
189
189
|
keystring="your_api_key",
|
|
190
190
|
access_token=token_data["access_token"],
|
|
191
191
|
refresh_token=token_data["refresh_token"],
|
|
192
|
-
expiry=datetime.
|
|
192
|
+
expiry=datetime.now(tz=timezone.utc) + timedelta(seconds=token_data["expires_in"])
|
|
193
193
|
)
|
|
194
194
|
```
|
|
195
195
|
|
|
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
|