ozonapi-async 0.7.2__tar.gz → 0.7.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.
Files changed (102) hide show
  1. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/PKG-INFO +2 -1
  2. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/pyproject.toml +1 -1
  3. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/readme.md +1 -0
  4. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/__init__.py +1 -1
  5. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/config.py +1 -1
  6. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/core.py +65 -27
  7. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/exceptions.py +4 -0
  8. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/method_rate_limiter.py +17 -10
  9. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/rate_limiter.py +15 -9
  10. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/sessions.py +11 -5
  11. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/fbs.py +2 -2
  12. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi_async.egg-info/PKG-INFO +2 -1
  13. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/LICENSE +0 -0
  14. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/setup.cfg +0 -0
  15. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/__init__.py +0 -0
  16. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/__init__.py +0 -0
  17. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/__init__.py +0 -0
  18. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/delivery.py +0 -0
  19. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/localization.py +0 -0
  20. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/postings.py +0 -0
  21. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/prices.py +0 -0
  22. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/products.py +0 -0
  23. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/requests.py +0 -0
  24. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/common/enumerations/warehouses.py +0 -0
  25. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/core/__init__.py +0 -0
  26. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/__init__.py +0 -0
  27. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/attributes_and_characteristics.py +0 -0
  28. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/barcodes.py +0 -0
  29. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/beta.py +0 -0
  30. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/prices_and_stocks.py +0 -0
  31. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/products.py +0 -0
  32. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/methods/warehouses.py +0 -0
  33. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/__init__.py +0 -0
  34. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/attributes_and_characteristics/__init__.py +0 -0
  35. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/attributes_and_characteristics/base.py +0 -0
  36. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute.py +0 -0
  37. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute_values.py +0 -0
  38. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute_values_search.py +0 -0
  39. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_tree.py +0 -0
  40. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/barcodes/__init__.py +0 -0
  41. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/barcodes/v1__barcode_add.py +0 -0
  42. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/barcodes/v1__barcode_generate.py +0 -0
  43. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/base.py +0 -0
  44. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/beta/__init__.py +0 -0
  45. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/beta/v1__analytics_stocks.py +0 -0
  46. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/__init__.py +0 -0
  47. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/__init__.py +0 -0
  48. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__addressee.py +0 -0
  49. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__analytics_data.py +0 -0
  50. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__barcodes.py +0 -0
  51. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__cancellation.py +0 -0
  52. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__customer.py +0 -0
  53. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__customer_address.py +0 -0
  54. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__delivery_method.py +0 -0
  55. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__filter_with.py +0 -0
  56. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__financial_data.py +0 -0
  57. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__financial_data_products.py +0 -0
  58. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__legal_info.py +0 -0
  59. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__optional.py +0 -0
  60. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__posting.py +0 -0
  61. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__product.py +0 -0
  62. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__requirements.py +0 -0
  63. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/entities/posting__tariffication.py +0 -0
  64. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v2__fbs_posting_delivering.py +0 -0
  65. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v2__posting_fbs_get_by_barcode.py +0 -0
  66. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v2__posting_fbs_product_change.py +0 -0
  67. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v3__posting_fbs_get.py +0 -0
  68. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v3__posting_fbs_list.py +0 -0
  69. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v3__posting_fbs_unfulfilled_list.py +0 -0
  70. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/fbs/v3__posting_multiboxqty_set.py +0 -0
  71. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/mixins.py +0 -0
  72. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/__init__.py +0 -0
  73. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/base.py +0 -0
  74. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/v1__product_import_prices.py +0 -0
  75. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/v1__product_info_stocks_by_warehouse_fbs.py +0 -0
  76. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/v2__products_stocks.py +0 -0
  77. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/v4__product_info_stocks.py +0 -0
  78. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/prices_and_stocks/v5__product_info_prices.py +0 -0
  79. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/__init__.py +0 -0
  80. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/base.py +0 -0
  81. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_archive.py +0 -0
  82. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_attributes_update.py +0 -0
  83. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_import_by_sku.py +0 -0
  84. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_import_info.py +0 -0
  85. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_info_subscription.py +0 -0
  86. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_rating_by_sku.py +0 -0
  87. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_related_sku_get.py +0 -0
  88. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_unarchive.py +0 -0
  89. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v1__product_update_offer_id.py +0 -0
  90. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v2__product_pictures_info.py +0 -0
  91. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v2__products_delete.py +0 -0
  92. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v3__product_import.py +0 -0
  93. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v3__product_info_list.py +0 -0
  94. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v3__product_list.py +0 -0
  95. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/products/v4__product_info_attributes.py +0 -0
  96. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/warehouses/__init__.py +0 -0
  97. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/warehouses/v1__delivery_method_list.py +0 -0
  98. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi/seller/schemas/warehouses/v1__warehouse_list.py +0 -0
  99. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi_async.egg-info/SOURCES.txt +0 -0
  100. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi_async.egg-info/dependency_links.txt +0 -0
  101. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/src/ozonapi_async.egg-info/requires.txt +0 -0
  102. {ozonapi_async-0.7.2 → ozonapi_async-0.7.4}/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.2
3
+ Version: 0.7.4
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:
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "ozonapi-async"
7
- version = "0.7.2"
7
+ version = "0.7.4"
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:
@@ -2,7 +2,7 @@
2
2
  Асинхронный интерфейс для взаимодействия с API маркетплейса Ozon.
3
3
  """
4
4
 
5
- __version__ = "0.7.2"
5
+ __version__ = "0.7.4"
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="INFO",
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
@@ -24,14 +25,7 @@ from .exceptions import (
24
25
  APIError,
25
26
  APIForbiddenError,
26
27
  APINotFoundError,
27
- APIServerError,
28
- )
29
-
30
- logger.remove()
31
- logger.add(
32
- sys.stderr,
33
- level=APIConfig().log_level,
34
- enqueue=True,
28
+ APIServerError, APITooManyRequestsError,
35
29
  )
36
30
 
37
31
 
@@ -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,
@@ -64,12 +67,13 @@ class APIManager:
64
67
  api_key: Ключ API для аутентификации
65
68
  config: Конфигурация клиента
66
69
  """
67
- self._config = config or self.load_config()
70
+ self._config = self.load_config(config)
68
71
  self._client_id = client_id or self._config.client_id
69
72
  self._api_key = api_key or self._config.api_key
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,27 +82,53 @@ 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
- def load_config(cls) -> APIConfig:
105
+ def load_config(cls, user_config: APIConfig | None = None) -> APIConfig:
99
106
  """Создает конфигурацию с загрузкой из .env файла."""
100
107
  load_dotenv()
101
- return APIConfig()
108
+ base_config = APIConfig()
109
+
110
+ if user_config is None:
111
+ return base_config
112
+ else:
113
+ return base_config.model_copy(
114
+ update=user_config.model_dump(
115
+ exclude_unset=True,
116
+ exclude_defaults=True
117
+ )
118
+ )
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
102
132
 
103
133
  @classmethod
104
134
  async def initialize(cls) -> None:
@@ -109,7 +139,7 @@ class APIManager:
109
139
  if cls._method_rate_limiter_manager:
110
140
  await cls._method_rate_limiter_manager.start()
111
141
  cls._initialized = True
112
- logger.debug("Выполнена инициализация ресурсов API-менеджера")
142
+ cls._class_logger.debug("Выполнена инициализация ресурсов API-менеджера")
113
143
 
114
144
  @classmethod
115
145
  async def shutdown(cls) -> None:
@@ -122,7 +152,7 @@ class APIManager:
122
152
  if cls._session_manager:
123
153
  await cls._session_manager.close_all()
124
154
  cls._initialized = False
125
- logger.debug("Выполнена деинициализация ресурсов API-менеджера")
155
+ cls._class_logger.debug("Выполнена деинициализация ресурсов API-менеджера")
126
156
 
127
157
  def _validate_credentials(self) -> None:
128
158
  """Валидация учетных данных."""
@@ -132,7 +162,7 @@ class APIManager:
132
162
  raise ValueError("api_key не должен быть пустой строкой")
133
163
 
134
164
  if self._config.max_requests_per_second > 50:
135
- logger.warning(
165
+ self.logger.warning(
136
166
  f"Максимальное кол-во запросов в секунду согласно документации Ozon - 50. "
137
167
  f"Установлено: {self._config.max_requests_per_second}"
138
168
  )
@@ -184,7 +214,7 @@ class APIManager:
184
214
  if APIManager._session_manager:
185
215
  await APIManager._session_manager.close_all()
186
216
 
187
- logger.debug(f"Работа API-клиента для ClientID {self._client_id} завершена")
217
+ self.logger.debug(f"Работа API-клиента для ClientID {self._client_id} завершена")
188
218
 
189
219
  @property
190
220
  def client_id(self) -> str:
@@ -201,6 +231,11 @@ class APIManager:
201
231
  """Проверяет закрыт ли клиент."""
202
232
  return self._closed
203
233
 
234
+ @property
235
+ def logger(self):
236
+ """Возвращает логер экземпляра."""
237
+ return self._instance_logger
238
+
204
239
  @classmethod
205
240
  def get_instance_count(cls) -> int:
206
241
  """Получает количество активных экземпляров."""
@@ -210,7 +245,7 @@ class APIManager:
210
245
  """Создает декоратор повторов на основе конфигурации."""
211
246
 
212
247
  def log_retry(retry_state):
213
- logger.debug(
248
+ self.logger.debug(
214
249
  f"Попытка {retry_state.attempt_number} совершения запроса для ClientID {self._client_id}"
215
250
  f" завершилась исключением: {retry_state.outcome.exception()}"
216
251
  )
@@ -220,6 +255,7 @@ class APIManager:
220
255
  (
221
256
  # Обрабатываемые механизмом retry ошибки
222
257
  APIServerError,
258
+ APITooManyRequestsError,
223
259
  asyncio.TimeoutError
224
260
  )
225
261
  ),
@@ -229,7 +265,7 @@ class APIManager:
229
265
  min=self._config.retry_min_wait,
230
266
  max=self._config.retry_max_wait
231
267
  ),
232
- before_sleep=before_sleep_log(logger, 30),
268
+ before_sleep=before_sleep_log(self.logger, 30),
233
269
  after=log_retry,
234
270
  reraise=True,
235
271
  )
@@ -257,13 +293,14 @@ class APIManager:
257
293
  "error_details": details,
258
294
  })
259
295
 
260
- logger.error(f"Ошибка API: {message}", extra=log_context)
296
+ APIManager._class_logger.error(f"Ошибка API: {message}", extra=log_context)
261
297
 
262
298
  error_map = {
263
299
  400: APIClientError,
264
300
  403: APIForbiddenError,
265
301
  404: APINotFoundError,
266
302
  409: APIConflictError,
303
+ 429: APITooManyRequestsError,
267
304
  500: APIServerError,
268
305
  }
269
306
 
@@ -298,6 +335,7 @@ class APIManager:
298
335
  APIForbiddenError: При ошибках доступа (403)
299
336
  APINotFoundError: При отсутствии ресурса (404)
300
337
  APIConflictError: При конфликте данных (409)
338
+ APITooManyRequestsError: При превышении кол-ва запросов (429)
301
339
  APIServerError: При ошибках сервера (500)
302
340
  APIError: При прочих ошибках
303
341
  """
@@ -319,7 +357,7 @@ class APIManager:
319
357
  "has_payload": json is not None,
320
358
  }
321
359
 
322
- logger.debug("Отправка запроса к API", extra=log_context)
360
+ self.logger.debug("Отправка запроса к API", extra=log_context)
323
361
 
324
362
  await self._ensure_registered()
325
363
 
@@ -354,21 +392,21 @@ class APIManager:
354
392
  if error:
355
393
  raise error
356
394
 
357
- logger.debug("Успешный ответ от API", extra=log_context)
395
+ self.logger.debug("Успешный ответ от API", extra=log_context)
358
396
  return data
359
397
 
360
398
  except asyncio.TimeoutError:
361
- logger.error("Таймаут запроса к API", extra=log_context)
399
+ self.logger.error("Таймаут запроса к API", extra=log_context)
362
400
  raise APIError(408, "Request timeout")
363
401
  except asyncio.CancelledError:
364
- logger.warning("Запрос к API отменен", extra=log_context)
402
+ self.logger.warning("Запрос к API отменен", extra=log_context)
365
403
  raise
366
404
  except (aiohttp.ClientError, ConnectionError, OSError) as e:
367
405
  log_context.update({
368
406
  "error_type": type(e).__name__,
369
407
  "error_message": str(e)
370
408
  })
371
- logger.error(
409
+ self.logger.error(
372
410
  f"Сетевая ошибка при выполнении запроса к API: {str(e)}",
373
411
  extra=log_context
374
412
  )
@@ -26,6 +26,10 @@ class APIConflictError(APIError):
26
26
  """Ошибка 409: Конфликт запроса."""
27
27
  pass
28
28
 
29
+ class APITooManyRequestsError(APIError):
30
+ """Ошибка 429: Слишком много запросов."""
31
+ pass
32
+
29
33
 
30
34
  class APIServerError(APIError):
31
35
  """Ошибка 500: Внутренняя ошибка сервера."""
@@ -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("Все сессии закрыты")
@@ -164,7 +164,7 @@ class SellerFBSAPI(APIManager):
164
164
  )
165
165
  return PostingFBSListResponse(**response)
166
166
 
167
- @method_rate_limit(limit_requests=1, interval_seconds=1)
167
+ @method_rate_limit(limit_requests=2, interval_seconds=1)
168
168
  async def posting_fbs_get(
169
169
  self: "SellerFBSAPI",
170
170
  request: PostingFBSGetRequest
@@ -172,7 +172,7 @@ class SellerFBSAPI(APIManager):
172
172
  """Метод для получения информации об отправлении FBS по его номеру.
173
173
 
174
174
  Notes:
175
- Установлено ограничение 1 запрос в секунду (вычислено экспериментально)
175
+ Метод часто возвращает 429 (TooManyRequestsError), поэтому установлено ограничение 2 запроса в секунду (экспериментальное значение).
176
176
  • Чтобы получать актуальную дату отгрузки, регулярно обновляйте информацию об отправлениях или подключите пуш-уведомления.
177
177
  • Для получения дополнительных данных используйте параметр `with_` в запросе.
178
178
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ozonapi-async
3
- Version: 0.7.2
3
+ Version: 0.7.4
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:
File without changes
File without changes