ozonapi-async 0.7.3__tar.gz → 0.8.0__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 (103) hide show
  1. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/PKG-INFO +4 -3
  2. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/pyproject.toml +1 -1
  3. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/readme.md +3 -2
  4. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/__init__.py +1 -1
  5. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/config.py +1 -1
  6. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/core.py +48 -23
  7. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/method_rate_limiter.py +17 -10
  8. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/rate_limiter.py +15 -9
  9. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/sessions.py +11 -5
  10. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/products.py +64 -2
  11. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/__init__.py +3 -0
  12. ozonapi_async-0.8.0/src/ozonapi/seller/schemas/products/v1__product_pictures_import.py +85 -0
  13. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi_async.egg-info/PKG-INFO +4 -3
  14. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi_async.egg-info/SOURCES.txt +1 -0
  15. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/LICENSE +0 -0
  16. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/setup.cfg +0 -0
  17. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/__init__.py +0 -0
  18. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/__init__.py +0 -0
  19. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/__init__.py +0 -0
  20. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/delivery.py +0 -0
  21. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/localization.py +0 -0
  22. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/postings.py +0 -0
  23. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/prices.py +0 -0
  24. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/products.py +0 -0
  25. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/requests.py +0 -0
  26. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/common/enumerations/warehouses.py +0 -0
  27. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/__init__.py +0 -0
  28. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/core/exceptions.py +0 -0
  29. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/__init__.py +0 -0
  30. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/attributes_and_characteristics.py +0 -0
  31. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/barcodes.py +0 -0
  32. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/beta.py +0 -0
  33. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/fbs.py +0 -0
  34. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/prices_and_stocks.py +0 -0
  35. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/methods/warehouses.py +0 -0
  36. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/__init__.py +0 -0
  37. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/attributes_and_characteristics/__init__.py +0 -0
  38. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/attributes_and_characteristics/base.py +0 -0
  39. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute.py +0 -0
  40. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute_values.py +0 -0
  41. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute_values_search.py +0 -0
  42. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_tree.py +0 -0
  43. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/barcodes/__init__.py +0 -0
  44. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/barcodes/v1__barcode_add.py +0 -0
  45. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/barcodes/v1__barcode_generate.py +0 -0
  46. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/base.py +0 -0
  47. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/beta/__init__.py +0 -0
  48. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/beta/v1__analytics_stocks.py +0 -0
  49. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/__init__.py +0 -0
  50. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/__init__.py +0 -0
  51. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__addressee.py +0 -0
  52. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__analytics_data.py +0 -0
  53. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__barcodes.py +0 -0
  54. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__cancellation.py +0 -0
  55. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__customer.py +0 -0
  56. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__customer_address.py +0 -0
  57. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__delivery_method.py +0 -0
  58. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__filter_with.py +0 -0
  59. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__financial_data.py +0 -0
  60. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__financial_data_products.py +0 -0
  61. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__legal_info.py +0 -0
  62. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__optional.py +0 -0
  63. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__posting.py +0 -0
  64. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__product.py +0 -0
  65. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__requirements.py +0 -0
  66. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/entities/posting__tariffication.py +0 -0
  67. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v2__fbs_posting_delivering.py +0 -0
  68. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v2__posting_fbs_get_by_barcode.py +0 -0
  69. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v2__posting_fbs_product_change.py +0 -0
  70. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v3__posting_fbs_get.py +0 -0
  71. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v3__posting_fbs_list.py +0 -0
  72. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v3__posting_fbs_unfulfilled_list.py +0 -0
  73. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/fbs/v3__posting_multiboxqty_set.py +0 -0
  74. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/mixins.py +0 -0
  75. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/__init__.py +0 -0
  76. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/base.py +0 -0
  77. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/v1__product_import_prices.py +0 -0
  78. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/v1__product_info_stocks_by_warehouse_fbs.py +0 -0
  79. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/v2__products_stocks.py +0 -0
  80. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/v4__product_info_stocks.py +0 -0
  81. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/prices_and_stocks/v5__product_info_prices.py +0 -0
  82. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/base.py +0 -0
  83. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_archive.py +0 -0
  84. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_attributes_update.py +0 -0
  85. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_import_by_sku.py +0 -0
  86. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_import_info.py +0 -0
  87. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_info_subscription.py +0 -0
  88. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_rating_by_sku.py +0 -0
  89. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_related_sku_get.py +0 -0
  90. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_unarchive.py +0 -0
  91. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v1__product_update_offer_id.py +0 -0
  92. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v2__product_pictures_info.py +0 -0
  93. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v2__products_delete.py +0 -0
  94. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v3__product_import.py +0 -0
  95. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v3__product_info_list.py +0 -0
  96. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v3__product_list.py +0 -0
  97. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/products/v4__product_info_attributes.py +0 -0
  98. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/warehouses/__init__.py +0 -0
  99. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/warehouses/v1__delivery_method_list.py +0 -0
  100. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi/seller/schemas/warehouses/v1__warehouse_list.py +0 -0
  101. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi_async.egg-info/dependency_links.txt +0 -0
  102. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi_async.egg-info/requires.txt +0 -0
  103. {ozonapi_async-0.7.3 → ozonapi_async-0.8.0}/src/ozonapi_async.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ozonapi-async
3
- Version: 0.7.3
3
+ Version: 0.8.0
4
4
  Summary: Асинхронный клиент для работы с API маркетплейса Ozon
5
5
  Author: Alexander Ulianov
6
6
  License: MIT
@@ -113,6 +113,7 @@ async def main():
113
113
  config = SellerAPIConfig(
114
114
  client_id="your_client_id",
115
115
  api_key="your_api_key",
116
+ log_level="DEBUG"
116
117
  )
117
118
 
118
119
  async with SellerAPI(config=config) as api:
@@ -259,7 +260,7 @@ pytest --cov=ozonapi --cov-report=html
259
260
  | ✓ | `/v1/description-category/attribute/values/search` | Поиск по справочным значениям характеристики | `description_category_attribute_values_search()` |
260
261
  </details>
261
262
  <details>
262
- <summary>Загрузка и обновление товаров (16)</summary>
263
+ <summary>Загрузка и обновление товаров (17)</summary>
263
264
 
264
265
  | ✓ | Адрес метода Ozon | Описание метода | Python-метод |
265
266
  |---|---|---|---|
@@ -267,7 +268,7 @@ pytest --cov=ozonapi --cov-report=html
267
268
  | ✓ | `/v1/product/import/info` | Узнать статус добавления или обновления товара | `product_import_info()` |
268
269
  | ✓ | `/v1/product/import-by-sku` | Создать товар по SKU | `product_import_by_sku()` |
269
270
  | ✓ | `/v1/product/attributes/update` | Обновить характеристики товара | `product_attributes_update()` |
270
- | | `/v1/product/pictures/import` | Загрузить или обновить изображения товара | `product_pictures_import()` |
271
+ | | `/v1/product/pictures/import` | Загрузить или обновить изображения товара | `product_pictures_import()` |
271
272
  | ✓ | `/v3/product/list` | Список товаров | `product_list()` |
272
273
  | ✓ | `/v1/product/rating-by-sku` | Получить контент-рейтинг товаров по SKU | `product_rating_by_sku()` |
273
274
  | ✓ | `/v3/product/info/list` | Получить информацию о товарах по идентификаторам | `product_info_list()` |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ozonapi-async"
7
- version = "0.7.3"
7
+ version = "0.8.0"
8
8
  description = "Асинхронный клиент для работы с API маркетплейса Ozon"
9
9
  readme = "readme.md"
10
10
  keywords = ["ozon", "api", "async", "ecommerce", "seller", "client"]
@@ -71,6 +71,7 @@ async def main():
71
71
  config = SellerAPIConfig(
72
72
  client_id="your_client_id",
73
73
  api_key="your_api_key",
74
+ log_level="DEBUG"
74
75
  )
75
76
 
76
77
  async with SellerAPI(config=config) as api:
@@ -217,7 +218,7 @@ pytest --cov=ozonapi --cov-report=html
217
218
  | ✓ | `/v1/description-category/attribute/values/search` | Поиск по справочным значениям характеристики | `description_category_attribute_values_search()` |
218
219
  </details>
219
220
  <details>
220
- <summary>Загрузка и обновление товаров (16)</summary>
221
+ <summary>Загрузка и обновление товаров (17)</summary>
221
222
 
222
223
  | ✓ | Адрес метода Ozon | Описание метода | Python-метод |
223
224
  |---|---|---|---|
@@ -225,7 +226,7 @@ pytest --cov=ozonapi --cov-report=html
225
226
  | ✓ | `/v1/product/import/info` | Узнать статус добавления или обновления товара | `product_import_info()` |
226
227
  | ✓ | `/v1/product/import-by-sku` | Создать товар по SKU | `product_import_by_sku()` |
227
228
  | ✓ | `/v1/product/attributes/update` | Обновить характеристики товара | `product_attributes_update()` |
228
- | | `/v1/product/pictures/import` | Загрузить или обновить изображения товара | `product_pictures_import()` |
229
+ | | `/v1/product/pictures/import` | Загрузить или обновить изображения товара | `product_pictures_import()` |
229
230
  | ✓ | `/v3/product/list` | Список товаров | `product_list()` |
230
231
  | ✓ | `/v1/product/rating-by-sku` | Получить контент-рейтинг товаров по SKU | `product_rating_by_sku()` |
231
232
  | ✓ | `/v3/product/info/list` | Получить информацию о товарах по идентификаторам | `product_info_list()` |
@@ -2,7 +2,7 @@
2
2
  Асинхронный интерфейс для взаимодействия с API маркетплейса Ozon.
3
3
  """
4
4
 
5
- __version__ = "0.7.3"
5
+ __version__ = "0.8.0"
6
6
  __author__ = "Alexander Ulianov"
7
7
  __repository__ = "https://github.com/a-ulianov/OzonAPI"
8
8
  __docs__ = "https://github.com/a-ulianov/OzonAPI#readme"
@@ -36,7 +36,7 @@ class APIConfig(BaseSettings):
36
36
  description="Авторизационный ключ Ozon Seller API",
37
37
  )
38
38
  log_level: Optional[str] = Field(
39
- default="DEBUG",
39
+ default="ERROR",
40
40
  description="Уровень логирования."
41
41
  )
42
42
  base_url: str = Field(
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ import logging
2
3
  import sys
3
4
  from types import TracebackType
4
5
  from typing import Any, Literal, Optional, ClassVar
@@ -27,13 +28,6 @@ from .exceptions import (
27
28
  APIServerError, APITooManyRequestsError,
28
29
  )
29
30
 
30
- logger.remove()
31
- logger.add(
32
- sys.stderr,
33
- level=APIConfig().log_level,
34
- enqueue=True,
35
- )
36
-
37
31
 
38
32
  class APIManager:
39
33
  """
@@ -50,6 +44,15 @@ class APIManager:
50
44
  _initialized: ClassVar[bool] = False
51
45
  _instance_count: ClassVar[int] = 0
52
46
 
47
+ _class_logger: ClassVar = logger
48
+ _class_logger.remove()
49
+ _class_logger.add(
50
+ sys.stderr,
51
+ level=APIConfig().log_level,
52
+ enqueue=True,
53
+ )
54
+
55
+
53
56
  def __init__(
54
57
  self,
55
58
  client_id: Optional[str] = None,
@@ -70,6 +73,7 @@ class APIManager:
70
73
  self._instance_id = id(self)
71
74
  self._registered = False
72
75
  self._closed = False
76
+ self._instance_logger = self._get_instance_logger()
73
77
 
74
78
  if self._client_id is None or self._api_key is None:
75
79
  raise ValueError(
@@ -78,21 +82,24 @@ class APIManager:
78
82
 
79
83
  if APIManager._rate_limiter_manager is None:
80
84
  APIManager._rate_limiter_manager = RateLimiterManager(
81
- cleanup_interval=self._config.cleanup_interval
85
+ cleanup_interval=self._config.cleanup_interval,
86
+ instance_logger=self.logger
82
87
  )
83
88
  if APIManager._session_manager is None:
84
89
  APIManager._session_manager = SessionManager(
85
90
  timeout=self._config.request_timeout,
86
- connector_limit=self._config.connector_limit
91
+ connector_limit=self._config.connector_limit,
92
+ instance_logger=self.logger
87
93
  )
88
94
  if APIManager._method_rate_limiter_manager is None:
89
95
  APIManager._method_rate_limiter_manager = MethodRateLimiterManager(
90
- cleanup_interval=self._config.cleanup_interval
96
+ cleanup_interval=self._config.cleanup_interval,
97
+ instance_logger=self.logger
91
98
  )
92
99
 
93
100
  APIManager._instance_count += 1
94
101
  self._validate_credentials()
95
- logger.debug(f"API-клиент инициализирован для ClientID {self._client_id}")
102
+ self.logger.debug(f"API-клиент инициализирован для ClientID {self._client_id}")
96
103
 
97
104
  @classmethod
98
105
  def load_config(cls, user_config: APIConfig | None = None) -> APIConfig:
@@ -110,6 +117,19 @@ class APIManager:
110
117
  )
111
118
  )
112
119
 
120
+ def _get_instance_logger(self) -> logging.Logger:
121
+ """Инициализирует и возвращает настроенный логер для экземпляра."""
122
+ instance_logger = logger.bind(client_id=self._client_id)
123
+
124
+ instance_logger.remove()
125
+ instance_logger.add(
126
+ sys.stderr,
127
+ level=self._config.log_level,
128
+ enqueue=True,
129
+ )
130
+
131
+ return instance_logger
132
+
113
133
  @classmethod
114
134
  async def initialize(cls) -> None:
115
135
  """Инициализация ресурсов."""
@@ -119,7 +139,7 @@ class APIManager:
119
139
  if cls._method_rate_limiter_manager:
120
140
  await cls._method_rate_limiter_manager.start()
121
141
  cls._initialized = True
122
- logger.debug("Выполнена инициализация ресурсов API-менеджера")
142
+ cls._class_logger.debug("Выполнена инициализация ресурсов API-менеджера")
123
143
 
124
144
  @classmethod
125
145
  async def shutdown(cls) -> None:
@@ -132,7 +152,7 @@ class APIManager:
132
152
  if cls._session_manager:
133
153
  await cls._session_manager.close_all()
134
154
  cls._initialized = False
135
- logger.debug("Выполнена деинициализация ресурсов API-менеджера")
155
+ cls._class_logger.debug("Выполнена деинициализация ресурсов API-менеджера")
136
156
 
137
157
  def _validate_credentials(self) -> None:
138
158
  """Валидация учетных данных."""
@@ -142,7 +162,7 @@ class APIManager:
142
162
  raise ValueError("api_key не должен быть пустой строкой")
143
163
 
144
164
  if self._config.max_requests_per_second > 50:
145
- logger.warning(
165
+ self.logger.warning(
146
166
  f"Максимальное кол-во запросов в секунду согласно документации Ozon - 50. "
147
167
  f"Установлено: {self._config.max_requests_per_second}"
148
168
  )
@@ -194,7 +214,7 @@ class APIManager:
194
214
  if APIManager._session_manager:
195
215
  await APIManager._session_manager.close_all()
196
216
 
197
- logger.debug(f"Работа API-клиента для ClientID {self._client_id} завершена")
217
+ self.logger.debug(f"Работа API-клиента для ClientID {self._client_id} завершена")
198
218
 
199
219
  @property
200
220
  def client_id(self) -> str:
@@ -211,6 +231,11 @@ class APIManager:
211
231
  """Проверяет закрыт ли клиент."""
212
232
  return self._closed
213
233
 
234
+ @property
235
+ def logger(self):
236
+ """Возвращает логер экземпляра."""
237
+ return self._instance_logger
238
+
214
239
  @classmethod
215
240
  def get_instance_count(cls) -> int:
216
241
  """Получает количество активных экземпляров."""
@@ -220,7 +245,7 @@ class APIManager:
220
245
  """Создает декоратор повторов на основе конфигурации."""
221
246
 
222
247
  def log_retry(retry_state):
223
- logger.debug(
248
+ self.logger.debug(
224
249
  f"Попытка {retry_state.attempt_number} совершения запроса для ClientID {self._client_id}"
225
250
  f" завершилась исключением: {retry_state.outcome.exception()}"
226
251
  )
@@ -240,7 +265,7 @@ class APIManager:
240
265
  min=self._config.retry_min_wait,
241
266
  max=self._config.retry_max_wait
242
267
  ),
243
- before_sleep=before_sleep_log(logger, 30),
268
+ before_sleep=before_sleep_log(self.logger, 30),
244
269
  after=log_retry,
245
270
  reraise=True,
246
271
  )
@@ -268,7 +293,7 @@ class APIManager:
268
293
  "error_details": details,
269
294
  })
270
295
 
271
- logger.error(f"Ошибка API: {message}", extra=log_context)
296
+ APIManager._class_logger.error(f"Ошибка API: {message}", extra=log_context)
272
297
 
273
298
  error_map = {
274
299
  400: APIClientError,
@@ -332,7 +357,7 @@ class APIManager:
332
357
  "has_payload": json is not None,
333
358
  }
334
359
 
335
- logger.debug("Отправка запроса к API", extra=log_context)
360
+ self.logger.debug("Отправка запроса к API", extra=log_context)
336
361
 
337
362
  await self._ensure_registered()
338
363
 
@@ -367,21 +392,21 @@ class APIManager:
367
392
  if error:
368
393
  raise error
369
394
 
370
- logger.debug("Успешный ответ от API", extra=log_context)
395
+ self.logger.debug("Успешный ответ от API", extra=log_context)
371
396
  return data
372
397
 
373
398
  except asyncio.TimeoutError:
374
- logger.error("Таймаут запроса к API", extra=log_context)
399
+ self.logger.error("Таймаут запроса к API", extra=log_context)
375
400
  raise APIError(408, "Request timeout")
376
401
  except asyncio.CancelledError:
377
- logger.warning("Запрос к API отменен", extra=log_context)
402
+ self.logger.warning("Запрос к API отменен", extra=log_context)
378
403
  raise
379
404
  except (aiohttp.ClientError, ConnectionError, OSError) as e:
380
405
  log_context.update({
381
406
  "error_type": type(e).__name__,
382
407
  "error_message": str(e)
383
408
  })
384
- logger.error(
409
+ self.logger.error(
385
410
  f"Сетевая ошибка при выполнении запроса к API: {str(e)}",
386
411
  extra=log_context
387
412
  )
@@ -22,7 +22,12 @@ class MethodRateLimiterManager:
22
22
  Обеспечивает раздельные лимиты для каждого метода и client_id.
23
23
  """
24
24
 
25
- def __init__(self, cleanup_interval: float = 300.0, min_instance_ttl: float = 300.0) -> None:
25
+ def __init__(
26
+ self,
27
+ cleanup_interval: float = 300.0,
28
+ min_instance_ttl: float = 300.0,
29
+ instance_logger = logger
30
+ ) -> None:
26
31
  self._rate_limiters: dict[str, AsyncLimiter] = {}
27
32
  self._limiter_configs: dict[str, MethodRateLimitConfig] = {}
28
33
  self._last_used: dict[str, float] = {}
@@ -32,12 +37,13 @@ class MethodRateLimiterManager:
32
37
  self._shutdown = False
33
38
  self._cleanup_interval = cleanup_interval
34
39
  self._min_instance_ttl = min_instance_ttl
40
+ self._logger = instance_logger
35
41
 
36
42
  async def start(self) -> None:
37
43
  """Запуск фоновых задач менеджера."""
38
44
  if self._cleanup_task is None:
39
45
  self._cleanup_task = asyncio.create_task(self._cleanup_loop())
40
- logger.debug("Менеджер ограничителей методов запущен")
46
+ self._logger.debug("Менеджер ограничителей методов запущен")
41
47
 
42
48
  async def shutdown(self) -> None:
43
49
  """Корректное завершение работы менеджера."""
@@ -47,9 +53,9 @@ class MethodRateLimiterManager:
47
53
  try:
48
54
  await self._cleanup_task
49
55
  except asyncio.CancelledError:
50
- logger.debug("Задача очистки ограничителей методов отменена")
56
+ self._logger.debug("Задача очистки ограничителей методов отменена")
51
57
  self._cleanup_task = None
52
- logger.debug("Менеджер ограничителей методов остановлен")
58
+ self._logger.debug("Менеджер ограничителей методов остановлен")
53
59
 
54
60
  @staticmethod
55
61
  def _generate_limiter_key(client_id: str, method_identifier: str) -> str:
@@ -77,7 +83,7 @@ class MethodRateLimiterManager:
77
83
  self._rate_limiters[limiter_key] = limiter
78
84
  self._limiter_configs[limiter_key] = config
79
85
  self._last_instance_creation[limiter_key] = current_time
80
- logger.debug(
86
+ self._logger.debug(
81
87
  f"Инициализирован ограничитель запросов для метода {config.method_identifier} "
82
88
  f"ClientID {client_id}: {config.limit_requests} запросов в {config.interval_seconds} сек"
83
89
  )
@@ -107,7 +113,7 @@ class MethodRateLimiterManager:
107
113
  self._last_used.pop(limiter_key, None)
108
114
  self._last_instance_creation.pop(limiter_key, None)
109
115
  if config:
110
- logger.debug(f"Очищен ограничитель для метода {config.method_identifier}")
116
+ self._logger.debug(f"Очищен ограничитель для метода {config.method_identifier}")
111
117
 
112
118
  async def _cleanup_loop(self) -> None:
113
119
  """Фоновая задача для очистки неиспользуемых ограничителей."""
@@ -118,7 +124,7 @@ class MethodRateLimiterManager:
118
124
  except asyncio.CancelledError:
119
125
  break
120
126
  except Exception as e:
121
- logger.error(f"Ошибка в cleanup loop методов: {e}")
127
+ self._logger.error(f"Ошибка в cleanup loop методов: {e}")
122
128
  await asyncio.sleep(60)
123
129
 
124
130
  async def get_limiter_stats(self) -> dict[str, dict[str, Any]]:
@@ -164,9 +170,10 @@ def method_rate_limit(limit_requests: int, interval_seconds: float):
164
170
 
165
171
  @wraps(method)
166
172
  async def wrapper(self, *args, **kwargs):
173
+ _logger = self._logger if hasattr(self, '_logger') else logger
167
174
  # Проверяем, что экземпляр имеет необходимые атрибуты
168
175
  if not hasattr(self, '_client_id') or not hasattr(self, '_method_rate_limiter_manager'):
169
- logger.warning(
176
+ _logger.warning(
170
177
  f"Метод {method_identifier} вызван без инициализации ограничителей. "
171
178
  "Ограничения не применяются."
172
179
  )
@@ -174,7 +181,7 @@ def method_rate_limit(limit_requests: int, interval_seconds: float):
174
181
 
175
182
  # Дополнительная проверка, что менеджер не None
176
183
  if self._method_rate_limiter_manager is None:
177
- logger.warning(
184
+ _logger.warning(
178
185
  f"Менеджер ограничителей методов не инициализирован для {method_identifier}. "
179
186
  "Ограничения не применяются."
180
187
  )
@@ -187,7 +194,7 @@ def method_rate_limit(limit_requests: int, interval_seconds: float):
187
194
 
188
195
  # Применяем ограничитель запросов
189
196
  async with method_limiter:
190
- logger.debug(
197
+ _logger.debug(
191
198
  f"Применен ограничитель метода {method_identifier} для ClientID {self._client_id}: "
192
199
  f"{limit_requests} запросов в {interval_seconds} сек"
193
200
  )
@@ -27,7 +27,12 @@ class RateLimiterManager:
27
27
  Обеспечивает общий лимит запросов для всех экземпляров с одинаковым client_id.
28
28
  """
29
29
 
30
- def __init__(self, cleanup_interval: float = 300.0, min_instance_ttl: float = 300.0) -> None:
30
+ def __init__(
31
+ self,
32
+ cleanup_interval: float = 300.0,
33
+ min_instance_ttl: float = 300.0,
34
+ instance_logger = logger,
35
+ ) -> None:
31
36
  self._rate_limiters: dict[str, AsyncLimiter] = {}
32
37
  self._instance_refs: dict[str, set[int]] = {}
33
38
  self._configs: dict[str, RateLimiterConfig] = {}
@@ -37,12 +42,13 @@ class RateLimiterManager:
37
42
  self._shutdown = False
38
43
  self._cleanup_interval = cleanup_interval
39
44
  self._min_instance_ttl = min_instance_ttl
45
+ self._logger = instance_logger
40
46
 
41
47
  async def start(self) -> None:
42
48
  """Запуск фоновых задач менеджера."""
43
49
  if self._cleanup_task is None:
44
50
  self._cleanup_task = asyncio.create_task(self._cleanup_loop())
45
- logger.debug(f"Менеджер ограничителей общих клиентских запросов запущен")
51
+ self._logger.debug(f"Менеджер ограничителей общих клиентских запросов запущен")
46
52
 
47
53
  async def shutdown(self) -> None:
48
54
  """Корректное завершение работы менеджера."""
@@ -52,10 +58,10 @@ class RateLimiterManager:
52
58
  try:
53
59
  await self._cleanup_task
54
60
  except asyncio.CancelledError:
55
- logger.debug("Задача очистки общих ограничителей клиентских запросов отменена")
61
+ self._logger.debug("Задача очистки общих ограничителей клиентских запросов отменена")
56
62
  self._cleanup_task = None
57
63
 
58
- logger.debug("Менеджер общих ограничителей клиентских запросов остановлен")
64
+ self._logger.debug("Менеджер общих ограничителей клиентских запросов остановлен")
59
65
 
60
66
  async def get_limiter(self, client_id: str, config: RateLimiterConfig) -> AsyncLimiter:
61
67
  """
@@ -77,7 +83,7 @@ class RateLimiterManager:
77
83
  self._instance_refs[client_id] = set()
78
84
  if client_id not in self._last_instance_creation:
79
85
  self._last_instance_creation[client_id] = time.monotonic()
80
- logger.debug(f"Инициализирован новый общий ограничитель запросов для ClientID {client_id}: {config}")
86
+ self._logger.debug(f"Инициализирован новый общий ограничитель запросов для ClientID {client_id}: {config}")
81
87
 
82
88
  return self._rate_limiters[client_id]
83
89
 
@@ -95,7 +101,7 @@ class RateLimiterManager:
95
101
  self._instance_refs[client_id] = set()
96
102
  self._last_instance_creation[client_id] = current_time
97
103
  self._instance_refs[client_id].add(instance_id)
98
- logger.debug(f"Зарегистрировано подключение к API {instance_id} для ClientID {client_id}")
104
+ self._logger.debug(f"Зарегистрировано подключение к API {instance_id} для ClientID {client_id}")
99
105
 
100
106
  async def unregister_instance(self, client_id: str, instance_id: int) -> None:
101
107
  """
@@ -108,7 +114,7 @@ class RateLimiterManager:
108
114
  async with self._lock:
109
115
  if client_id in self._instance_refs:
110
116
  self._instance_refs[client_id].discard(instance_id)
111
- logger.debug(f"Отменена регистрация подключения к API {instance_id} для ClientID {client_id}")
117
+ self._logger.debug(f"Отменена регистрация подключения к API {instance_id} для ClientID {client_id}")
112
118
 
113
119
  async def _cleanup_unused_limiters(self) -> None:
114
120
  """Очистка неиспользуемых ограничителей кол-ва запросов с учетом минимального времени жизни."""
@@ -130,7 +136,7 @@ class RateLimiterManager:
130
136
  self._configs.pop(client_id, None)
131
137
  self._instance_refs.pop(client_id, None)
132
138
  self._last_instance_creation.pop(client_id, None)
133
- logger.debug(f"Очищены ресурсы общего ограничителя запросов для ClientID {client_id}")
139
+ self._logger.debug(f"Очищены ресурсы общего ограничителя запросов для ClientID {client_id}")
134
140
 
135
141
  async def _cleanup_loop(self) -> None:
136
142
  """Фоновая задача для очистки неиспользуемых ограничителей кол-ва запросов."""
@@ -141,7 +147,7 @@ class RateLimiterManager:
141
147
  except asyncio.CancelledError:
142
148
  break
143
149
  except Exception as e:
144
- logger.error(f"Ошибка в cleanup loop: {e}")
150
+ self._logger.error(f"Ошибка в cleanup loop: {e}")
145
151
  await asyncio.sleep(60) # Пауза при ошибках
146
152
 
147
153
  async def get_active_client_ids(self) -> list[str]:
@@ -9,12 +9,18 @@ from loguru import logger
9
9
  class SessionManager:
10
10
  """Менеджер для управления HTTP-сессиями."""
11
11
 
12
- def __init__(self, timeout: float = 30.0, connector_limit: int = 100) -> None:
12
+ def __init__(
13
+ self,
14
+ timeout: float = 30.0,
15
+ connector_limit: int = 100,
16
+ instance_logger = logger
17
+ ) -> None:
13
18
  self._sessions: dict[str, ClientSession] = {}
14
19
  self._session_refs: dict[str, set[int]] = {}
15
20
  self._lock = asyncio.Lock()
16
21
  self._timeout = ClientTimeout(total=timeout)
17
22
  self._connector_limit = connector_limit
23
+ self._logger = instance_logger
18
24
 
19
25
  @asynccontextmanager
20
26
  async def get_session(self, client_id: str, api_key: str, instance_id: int) -> AsyncIterator[ClientSession]:
@@ -40,7 +46,7 @@ class SessionManager:
40
46
  connector=TCPConnector(limit=self._connector_limit)
41
47
  )
42
48
  self._session_refs[client_id] = set()
43
- logger.debug(f"Создана новая сессия для ClientID {client_id}")
49
+ self._logger.debug(f"Создана новая сессия для ClientID {client_id}")
44
50
 
45
51
  self._session_refs[client_id].add(instance_id)
46
52
 
@@ -61,7 +67,7 @@ class SessionManager:
61
67
  self._session_refs.pop(client_id, None)
62
68
  if not session.closed:
63
69
  await session.close()
64
- logger.debug(f"Сессия для ClientID {client_id} закрыта")
70
+ self._logger.debug(f"Сессия для ClientID {client_id} закрыта")
65
71
 
66
72
  async def close_all(self) -> None:
67
73
  """Закрывает все сессии."""
@@ -69,7 +75,7 @@ class SessionManager:
69
75
  for client_id, session in list(self._sessions.items()):
70
76
  if not session.closed:
71
77
  await session.close()
72
- logger.debug(f"Сессия для ClientID {client_id} закрыта")
78
+ self._logger.debug(f"Сессия для ClientID {client_id} закрыта")
73
79
  self._sessions.clear()
74
80
  self._session_refs.clear()
75
- logger.debug("Все сессии закрыты")
81
+ self._logger.debug("Все сессии закрыты")
@@ -6,7 +6,8 @@ from ..schemas.products import ProductArchiveResponse, ProductArchiveRequest, Pr
6
6
  ProductInfoAttributesResponse, ProductInfoListRequest, ProductInfoListResponse, ProductInfoSubscriptionRequest, \
7
7
  ProductInfoSubscriptionResponse, ProductListRequest, ProductListResponse, ProductPicturesInfoRequest, \
8
8
  ProductPicturesInfoResponse, ProductRatingBySkuRequest, ProductRatingBySkuResponse, ProductRelatedSkuGetRequest, \
9
- ProductRelatedSkuGetResponse, ProductUpdateOfferIdRequest, ProductUpdateOfferIdResponse
9
+ ProductRelatedSkuGetResponse, ProductUpdateOfferIdRequest, ProductUpdateOfferIdResponse, \
10
+ ProductPicturesImportResponse, ProductPicturesImportRequest
10
11
 
11
12
 
12
13
  class SellerProductAPI(APIManager):
@@ -670,4 +671,65 @@ class SellerProductAPI(APIManager):
670
671
  endpoint="product/update/offer-id",
671
672
  json=request.model_dump(),
672
673
  )
673
- return ProductUpdateOfferIdResponse(**response)
674
+ return ProductUpdateOfferIdResponse(**response)
675
+
676
+ async def product_pictures_import(
677
+ self: "SellerProductAPI", request: ProductPicturesImportRequest
678
+ ) -> ProductPicturesImportResponse:
679
+ """Загружает или обновляет изображения товара.
680
+
681
+ Notes:
682
+ • При каждом вызове метода передавайте все изображения, которые должны быть на карточке товара.
683
+ Например, если вы вызвали метод и загрузили 10 изображений, а затем вызвали метод второй раз
684
+ и загрузили ещё одно, то все 10 предыдущих сотрутся.
685
+ • Для загрузки передайте адрес ссылки на изображение в общедоступном облачном хранилище.
686
+ Формат изображения по ссылке — JPG или PNG.
687
+ • Изображения в массиве `images` располагайте в соответствии с желаемым порядком на сайте.
688
+ Главным будет первое изображение в массиве.
689
+ • Для каждого товара вы можете загрузить до `30` изображений.
690
+ • Для загрузки изображений 360 используйте поле `images360`, для загрузки маркетингового цвета — `color_image`.
691
+ • Если вы хотите изменить состав или порядок изображений, получите информацию с помощью метода
692
+ `product_info_list()` — в нём отображается текущий порядок и состав изображений. Скопируйте
693
+ данные полей `images`, `images360`, `color_image`, измените и дополните состав или порядок
694
+ в соответствии с необходимостью.
695
+ • В ответе метода всегда будет статус `imported` — картинка не обработана. Чтобы посмотреть
696
+ финальный статус, примерно через 10 секунд вызовите метод `product_pictures_info()`.
697
+ `* Примечание: Видимо, артефакт в документации, т.к. по факту метод product_pictures_info() не возвращает статусы.`
698
+ • Финальные статусы загрузки изображений:
699
+ - `uploaded` — изображение загружено;
700
+ - `pending` — при загрузке изображения возникла ошибка. Повторите попытку позже.
701
+
702
+ References:
703
+ https://docs.ozon.ru/api/seller/?__rr=1#operation/ProductAPI_ProductImportPictures
704
+
705
+ Args:
706
+ request: Данные для загрузки изображений товара по схеме `ProductPicturesImportRequest`.
707
+
708
+ Returns:
709
+ Результат загрузки изображений с временными статусами по схеме `ProductPicturesImportResponse`.
710
+
711
+ Example:
712
+ async with SellerAPI(client_id, api_key) as api:
713
+ result = await api.product_pictures_import(
714
+ ProductPicturesImportRequest(
715
+ product_id=123456789,
716
+ color_image="https://example.com/color.jpg",
717
+ images=[
718
+ "https://example.com/image1.jpg",
719
+ "https://example.com/image2.jpg",
720
+ "https://example.com/image3.jpg",
721
+ ],
722
+ images360=[
723
+ "https://example.com/360_1.jpg",
724
+ "https://example.com/360_2.jpg",
725
+ ]
726
+ )
727
+ )
728
+ """
729
+ response = await self._request(
730
+ method="post",
731
+ api_version="v1",
732
+ endpoint="product/pictures/import",
733
+ json=request.model_dump(),
734
+ )
735
+ return ProductPicturesImportResponse(**response)
@@ -57,6 +57,8 @@ __all__ = [
57
57
  "ProductListResponseItem",
58
58
  "ProductListResponseResult",
59
59
  "ProductListQuants",
60
+ "ProductPicturesImportRequest",
61
+ "ProductPicturesImportResponse",
60
62
  "ProductPicturesInfoRequest",
61
63
  "ProductPicturesInfoResponse",
62
64
  "ProductPicturesInfoItem",
@@ -104,6 +106,7 @@ from .v1__product_info_subscription import (
104
106
  ProductInfoSubscriptionRequest,
105
107
  ProductInfoSubscriptionItem,
106
108
  )
109
+ from .v1__product_pictures_import import ProductPicturesImportResponse, ProductPicturesImportRequest
107
110
  from .v1__product_rating_by_sku import (
108
111
  ProductRatingBySkuResponse,
109
112
  ProductRatingBySkuRequest,
@@ -0,0 +1,85 @@
1
+ """https://docs.ozon.ru/api/seller/?__rr=1#operation/ProductAPI_ProductImportPictures"""
2
+ from typing import Optional
3
+
4
+ from pydantic import BaseModel, Field
5
+
6
+
7
+ class ProductPicturesImportRequest(BaseModel):
8
+ """Описывает схему запроса на добавление или обновление изображений в товарной карточке.
9
+
10
+ Attributes:
11
+ color_image: Маркетинговый цвет (опционально)
12
+ images: Массив ссылок на изображения (опционально, до 30 штук; изображения в массиве расположены в порядке их расположения на сайте; первое изображение в массиве будет главным)
13
+ images360: Массив изображений 360 (опционально, до 70 штук)
14
+ product_id: Идентификатор товара в системе продавца — product_id
15
+ """
16
+ color_image: Optional[str] = Field(
17
+ None, description="Маркетинговый цвет."
18
+ )
19
+ images: Optional[list[str]] = Field(
20
+ default_factory=list, description="""
21
+ Массив ссылок на изображения (до 30 штук; изображения в массиве расположены в порядке их
22
+ расположения на сайте; первое изображение в массиве будет главным).
23
+ """,
24
+ max_length=30,
25
+ )
26
+ images360: Optional[list[str]] = Field(
27
+ default_factory=list, description="Массив изображений 360 (до 70 штук)",
28
+ max_length=70,
29
+ )
30
+ product_id: int = Field(
31
+ ..., description="Идентификатор товара в системе продавца."
32
+ )
33
+
34
+
35
+ class ProductPicturesImportPicture(BaseModel):
36
+ """Описывает схему результата добавления изображения.
37
+
38
+ Attributes:
39
+ is_360: Признак, что картинка — изображение 360
40
+ is_color: Признак, что картинка — образец цвета
41
+ is_primary: Признак, что картинка — главное изображение
42
+ product_id: Идентификатор товара в системе продавца
43
+ state: Статус загрузки изображения
44
+ url: Адрес ссылки на изображение в общедоступном облачном хранилище (формат изображения по ссылке — JPG или PNG)
45
+ """
46
+ is_360: Optional[bool] = Field(
47
+ None, description="Признак, что картинка — изображение 360."
48
+ )
49
+ is_color: Optional[bool] = Field(
50
+ None, description="Признак, что картинка — образец цвета."
51
+ )
52
+ is_primary: Optional[bool] = Field(
53
+ None, description="Признак, что картинка — главное изображение."
54
+ )
55
+ product_id: int = Field(
56
+ ..., description="Идентификатор товара в системе продавца."
57
+ )
58
+ state: str = Field(
59
+ ..., description="Статус загрузки изображения."
60
+ )
61
+ url: Optional[str] = Field(
62
+ None, description="Адрес ссылки на изображение в общедоступном облачном хранилище (формат изображения по ссылке — JPG или PNG)."
63
+ )
64
+
65
+
66
+ class ProductPicturesImportResult(BaseModel):
67
+ """Описывает схему результата добавление изображений.
68
+
69
+ Attributes:
70
+ pictures: Массив признаков загруженного контента, статусов и ссылок на изображения
71
+ """
72
+ pictures: list[ProductPicturesImportPicture] = Field(
73
+ default_factory=list, description="Массив признаков загруженного контента, статусов и ссылок на изображения."
74
+ )
75
+
76
+
77
+ class ProductPicturesImportResponse(BaseModel):
78
+ """Описывает схему ответа на запрос на добавление или обновление изображений в товарной карточке.
79
+
80
+ Attributes:
81
+ result: Результат работы метода
82
+ """
83
+ result: ProductPicturesImportResult = Field(
84
+ ..., description="Результат работы метода."
85
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ozonapi-async
3
- Version: 0.7.3
3
+ Version: 0.8.0
4
4
  Summary: Асинхронный клиент для работы с API маркетплейса Ozon
5
5
  Author: Alexander Ulianov
6
6
  License: MIT
@@ -113,6 +113,7 @@ async def main():
113
113
  config = SellerAPIConfig(
114
114
  client_id="your_client_id",
115
115
  api_key="your_api_key",
116
+ log_level="DEBUG"
116
117
  )
117
118
 
118
119
  async with SellerAPI(config=config) as api:
@@ -259,7 +260,7 @@ pytest --cov=ozonapi --cov-report=html
259
260
  | ✓ | `/v1/description-category/attribute/values/search` | Поиск по справочным значениям характеристики | `description_category_attribute_values_search()` |
260
261
  </details>
261
262
  <details>
262
- <summary>Загрузка и обновление товаров (16)</summary>
263
+ <summary>Загрузка и обновление товаров (17)</summary>
263
264
 
264
265
  | ✓ | Адрес метода Ozon | Описание метода | Python-метод |
265
266
  |---|---|---|---|
@@ -267,7 +268,7 @@ pytest --cov=ozonapi --cov-report=html
267
268
  | ✓ | `/v1/product/import/info` | Узнать статус добавления или обновления товара | `product_import_info()` |
268
269
  | ✓ | `/v1/product/import-by-sku` | Создать товар по SKU | `product_import_by_sku()` |
269
270
  | ✓ | `/v1/product/attributes/update` | Обновить характеристики товара | `product_attributes_update()` |
270
- | | `/v1/product/pictures/import` | Загрузить или обновить изображения товара | `product_pictures_import()` |
271
+ | | `/v1/product/pictures/import` | Загрузить или обновить изображения товара | `product_pictures_import()` |
271
272
  | ✓ | `/v3/product/list` | Список товаров | `product_list()` |
272
273
  | ✓ | `/v1/product/rating-by-sku` | Получить контент-рейтинг товаров по SKU | `product_rating_by_sku()` |
273
274
  | ✓ | `/v3/product/info/list` | Получить информацию о товарах по идентификаторам | `product_info_list()` |
@@ -80,6 +80,7 @@ src/ozonapi/seller/schemas/products/v1__product_attributes_update.py
80
80
  src/ozonapi/seller/schemas/products/v1__product_import_by_sku.py
81
81
  src/ozonapi/seller/schemas/products/v1__product_import_info.py
82
82
  src/ozonapi/seller/schemas/products/v1__product_info_subscription.py
83
+ src/ozonapi/seller/schemas/products/v1__product_pictures_import.py
83
84
  src/ozonapi/seller/schemas/products/v1__product_rating_by_sku.py
84
85
  src/ozonapi/seller/schemas/products/v1__product_related_sku_get.py
85
86
  src/ozonapi/seller/schemas/products/v1__product_unarchive.py
File without changes
File without changes