etsy-python 1.2.2__tar.gz → 1.2.3__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.
Files changed (75) hide show
  1. {etsy_python-1.2.2/etsy_python.egg-info → etsy_python-1.2.3}/PKG-INFO +3 -3
  2. {etsy_python-1.2.2 → etsy_python-1.2.3}/README.md +2 -2
  3. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/_version.py +1 -1
  4. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/common/Request.py +2 -0
  5. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Listing.py +35 -2
  6. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingInventory.py +2 -0
  7. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingVideo.py +16 -7
  8. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Session.py +61 -23
  9. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/UserAddress.py +9 -0
  10. {etsy_python-1.2.2 → etsy_python-1.2.3/etsy_python.egg-info}/PKG-INFO +3 -3
  11. {etsy_python-1.2.2 → etsy_python-1.2.3}/LICENSE +0 -0
  12. {etsy_python-1.2.2 → etsy_python-1.2.3}/MANIFEST.in +0 -0
  13. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/__init__.py +0 -0
  14. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/__init__.py +0 -0
  15. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/auth/OAuth.py +0 -0
  16. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/auth/__init__.py +0 -0
  17. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/common/Env.py +0 -0
  18. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/common/Utils.py +0 -0
  19. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/common/__init__.py +0 -0
  20. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/HolidayPreferences.py +0 -0
  21. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/Language.py +0 -0
  22. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/Listing.py +0 -0
  23. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/ListingInventory.py +0 -0
  24. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/ProcessingProfile.py +0 -0
  25. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/ShippingProfile.py +0 -0
  26. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/ShopReceipt.py +0 -0
  27. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/enums/__init__.py +0 -0
  28. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/exceptions/BaseAPIException.py +0 -0
  29. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/exceptions/RequestException.py +0 -0
  30. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/exceptions/__init__.py +0 -0
  31. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/FileRequest.py +0 -0
  32. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/HolidayPreferences.py +0 -0
  33. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Miscellaneous.py +0 -0
  34. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/ProcessingProfile.py +0 -0
  35. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Product.py +0 -0
  36. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Receipt.py +0 -0
  37. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Request.py +0 -0
  38. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/ShippingProfile.py +0 -0
  39. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Shop.py +0 -0
  40. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/ShopReturnPolicy.py +0 -0
  41. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/Utils.py +0 -0
  42. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/models/__init__.py +0 -0
  43. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/HolidayPreferences.py +0 -0
  44. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Listing.py +0 -0
  45. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingFile.py +0 -0
  46. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingImage.py +0 -0
  47. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingOffering.py +0 -0
  48. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingProduct.py +0 -0
  49. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingTranslation.py +0 -0
  50. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ListingVariationImages.py +0 -0
  51. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Miscellaneous.py +0 -0
  52. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Payment.py +0 -0
  53. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/PaymentLedgerEntry.py +0 -0
  54. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ProcessingProfile.py +0 -0
  55. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Receipt.py +0 -0
  56. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ReceiptTransactions.py +0 -0
  57. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Response.py +0 -0
  58. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Review.py +0 -0
  59. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ShippingProfile.py +0 -0
  60. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Shop.py +0 -0
  61. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ShopProductionPartner.py +0 -0
  62. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ShopReturnPolicy.py +0 -0
  63. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/ShopSection.py +0 -0
  64. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/Taxonomy.py +0 -0
  65. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/User.py +0 -0
  66. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/__init__.py +0 -0
  67. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/enums/RateLimit.py +0 -0
  68. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/enums/Request.py +0 -0
  69. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python/v3/resources/enums/__init__.py +0 -0
  70. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python.egg-info/SOURCES.txt +0 -0
  71. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python.egg-info/dependency_links.txt +0 -0
  72. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python.egg-info/requires.txt +0 -0
  73. {etsy_python-1.2.2 → etsy_python-1.2.3}/etsy_python.egg-info/top_level.txt +0 -0
  74. {etsy_python-1.2.2 → etsy_python-1.2.3}/setup.cfg +0 -0
  75. {etsy_python-1.2.2 → etsy_python-1.2.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: etsy-python
3
- Version: 1.2.2
3
+ Version: 1.2.3
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.utcnow() + timedelta(seconds=token_data["expires_in"])
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.utcnow() + timedelta(seconds=token_data["expires_in"])
155
+ expiry=datetime.now(tz=timezone.utc) + timedelta(seconds=token_data["expires_in"])
156
156
  )
157
157
  ```
158
158
 
@@ -1,3 +1,3 @@
1
1
  """Version information for etsy-python package."""
2
2
 
3
- __version__ = "1.2.2"
3
+ __version__ = "1.2.3"
@@ -1,4 +1,6 @@
1
1
  NO_RESPONSE_CODES = {204}
2
+ # Kept for import compatibility only: EtsyClient treats every status >= 400 as
3
+ # an error, not just these codes.
2
4
  ERROR_CODES = {400, 401, 403, 404, 409, 500, 503}
3
5
 
4
6
  DEFAULT_RESPONSE_MESSAGES = {
@@ -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
- self.featured_rank = featured_rank
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"]
@@ -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
- Pass ``is_multi_video=True`` to keep existing videos on the listing.
41
- Omitting it preserves the former single-video behaviour, where the
42
- upload replaces any video already attached.
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
  )
@@ -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 ERROR_CODES, NO_RESPONSE_CODES
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
- response = self.session.post(environment.token_url, json=refresh_json)
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
- self.access_token = response_json.get("access_token")
44
- self.refresh_token = response_json.get("refresh_token")
45
- if not (self.access_token and self.refresh_token):
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, error=response_json.get("error", "Something went wrong!")
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
- updated_expiry = datetime.utcnow() + timedelta(
51
- seconds=response_json["expires_in"]
52
- )
53
- self.expiry = updated_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
- is_error = response.status_code in ERROR_CODES
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["X-Limit-Per-Day"],
141
- response.headers["X-Remaining-This-Second"],
142
- response.headers["X-Limit-Per-Day"],
143
- response.headers["X-Remaining-Today"],
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.2
3
+ Version: 1.2.3
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.utcnow() + timedelta(seconds=token_data["expires_in"])
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