ozonapi-async 0.1.0__py3-none-any.whl

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 (67) hide show
  1. ozonapi/__init__.py +9 -0
  2. ozonapi/seller/__init__.py +29 -0
  3. ozonapi/seller/common/__init__.py +0 -0
  4. ozonapi/seller/common/enumerations/__init__.py +0 -0
  5. ozonapi/seller/common/enumerations/delivery.py +16 -0
  6. ozonapi/seller/common/enumerations/localization.py +39 -0
  7. ozonapi/seller/common/enumerations/postings.py +205 -0
  8. ozonapi/seller/common/enumerations/products.py +225 -0
  9. ozonapi/seller/common/enumerations/requests.py +26 -0
  10. ozonapi/seller/common/enumerations/warehouses.py +54 -0
  11. ozonapi/seller/core/__init__.py +14 -0
  12. ozonapi/seller/core/config.py +88 -0
  13. ozonapi/seller/core/core.py +404 -0
  14. ozonapi/seller/core/exceptions.py +32 -0
  15. ozonapi/seller/core/method_rate_limiter.py +199 -0
  16. ozonapi/seller/core/rate_limiter.py +174 -0
  17. ozonapi/seller/core/sessions.py +75 -0
  18. ozonapi/seller/methods/__init__.py +15 -0
  19. ozonapi/seller/methods/attributes_and_characteristics.py +177 -0
  20. ozonapi/seller/methods/barcodes.py +84 -0
  21. ozonapi/seller/methods/fbs.py +69 -0
  22. ozonapi/seller/methods/prices_and_stocks.py +147 -0
  23. ozonapi/seller/methods/products.py +673 -0
  24. ozonapi/seller/methods/warehouses.py +80 -0
  25. ozonapi/seller/schemas/__init__.py +0 -0
  26. ozonapi/seller/schemas/attributes_and_characteristics/__init__.py +21 -0
  27. ozonapi/seller/schemas/attributes_and_characteristics/base.py +52 -0
  28. ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute.py +108 -0
  29. ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute_values.py +38 -0
  30. ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_attribute_values_search.py +36 -0
  31. ozonapi/seller/schemas/attributes_and_characteristics/v1__description_category_tree.py +61 -0
  32. ozonapi/seller/schemas/barcodes/__init__.py +12 -0
  33. ozonapi/seller/schemas/barcodes/v1__barcode_add.py +66 -0
  34. ozonapi/seller/schemas/barcodes/v1__barcode_generate.py +46 -0
  35. ozonapi/seller/schemas/base.py +94 -0
  36. ozonapi/seller/schemas/fbs/__init__.py +9 -0
  37. ozonapi/seller/schemas/fbs/v3__posting_fbs_unfulfilled_list.py +764 -0
  38. ozonapi/seller/schemas/prices_and_stocks/__init__.py +16 -0
  39. ozonapi/seller/schemas/prices_and_stocks/base.py +26 -0
  40. ozonapi/seller/schemas/prices_and_stocks/v1__product_info_stocks_by_warehouse_fbs.py +55 -0
  41. ozonapi/seller/schemas/prices_and_stocks/v4__product_info_stocks.py +113 -0
  42. ozonapi/seller/schemas/prices_and_stocks/v5__product_info_prices.py +292 -0
  43. ozonapi/seller/schemas/products/__init__.py +51 -0
  44. ozonapi/seller/schemas/products/base.py +162 -0
  45. ozonapi/seller/schemas/products/v1__product_archive.py +20 -0
  46. ozonapi/seller/schemas/products/v1__product_attributes_update.py +79 -0
  47. ozonapi/seller/schemas/products/v1__product_import_by_sku.py +71 -0
  48. ozonapi/seller/schemas/products/v1__product_import_info.py +103 -0
  49. ozonapi/seller/schemas/products/v1__product_info_subscription.py +39 -0
  50. ozonapi/seller/schemas/products/v1__product_rating_by_sku.py +116 -0
  51. ozonapi/seller/schemas/products/v1__product_related_sku_get.py +81 -0
  52. ozonapi/seller/schemas/products/v1__product_unarchive.py +20 -0
  53. ozonapi/seller/schemas/products/v1__product_update_offer_id.py +52 -0
  54. ozonapi/seller/schemas/products/v2__product_pictures_info.py +73 -0
  55. ozonapi/seller/schemas/products/v2__products_delete.py +57 -0
  56. ozonapi/seller/schemas/products/v3__product_import.py +142 -0
  57. ozonapi/seller/schemas/products/v3__product_info_list.py +482 -0
  58. ozonapi/seller/schemas/products/v3__product_list.py +107 -0
  59. ozonapi/seller/schemas/products/v4__product_info_attributes.py +137 -0
  60. ozonapi/seller/schemas/warehouses/__init__.py +11 -0
  61. ozonapi/seller/schemas/warehouses/v1__delivery_method_list.py +95 -0
  62. ozonapi/seller/schemas/warehouses/v1__warehouse_list.py +109 -0
  63. ozonapi_async-0.1.0.dist-info/METADATA +648 -0
  64. ozonapi_async-0.1.0.dist-info/RECORD +67 -0
  65. ozonapi_async-0.1.0.dist-info/WHEEL +5 -0
  66. ozonapi_async-0.1.0.dist-info/licenses/LICENSE +21 -0
  67. ozonapi_async-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,199 @@
1
+ import asyncio
2
+ import hashlib
3
+ import time
4
+ from functools import wraps
5
+ from typing import Optional, Any
6
+
7
+ from aiolimiter import AsyncLimiter
8
+ from loguru import logger
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class MethodRateLimitConfig(BaseModel):
13
+ """Конфигурация ограничений для конкретного метода API."""
14
+ limit_requests: int = Field(..., ge=1, description="Максимальное количество запросов в интервал времени")
15
+ interval_seconds: float = Field(..., gt=0, description="Интервал ограничения количества запросов в секундах")
16
+ method_identifier: str = Field(..., description="Уникальный идентификатор вызываемого метода")
17
+
18
+
19
+ class MethodRateLimiterManager:
20
+ """
21
+ Менеджер для управления ограничителями запросов по методам API.
22
+ Обеспечивает раздельные лимиты для каждого метода и client_id.
23
+ """
24
+
25
+ def __init__(self, cleanup_interval: float = 300.0, min_instance_ttl: float = 300.0) -> None:
26
+ self._rate_limiters: dict[str, AsyncLimiter] = {}
27
+ self._limiter_configs: dict[str, MethodRateLimitConfig] = {}
28
+ self._last_used: dict[str, float] = {}
29
+ self._last_instance_creation: dict[str, float] = {}
30
+ self._lock = asyncio.Lock()
31
+ self._cleanup_task: Optional[asyncio.Task] = None
32
+ self._shutdown = False
33
+ self._cleanup_interval = cleanup_interval
34
+ self._min_instance_ttl = min_instance_ttl
35
+
36
+ async def start(self) -> None:
37
+ """Запуск фоновых задач менеджера."""
38
+ if self._cleanup_task is None:
39
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
40
+ logger.debug("Менеджер ограничителей методов запущен")
41
+
42
+ async def shutdown(self) -> None:
43
+ """Корректное завершение работы менеджера."""
44
+ self._shutdown = True
45
+ if self._cleanup_task:
46
+ self._cleanup_task.cancel()
47
+ try:
48
+ await self._cleanup_task
49
+ except asyncio.CancelledError:
50
+ logger.debug("Задача очистки ограничителей методов отменена")
51
+ self._cleanup_task = None
52
+ logger.debug("Менеджер ограничителей методов остановлен")
53
+
54
+ @staticmethod
55
+ def _generate_limiter_key(client_id: str, method_identifier: str) -> str:
56
+ """Генерирует уникальный ключ для ограничителя метода."""
57
+ key_data = f"{client_id}:{method_identifier}"
58
+ return hashlib.md5(key_data.encode()).hexdigest()
59
+
60
+ async def get_limiter(self, client_id: str, config: MethodRateLimitConfig) -> AsyncLimiter:
61
+ """
62
+ Получает ограничитель для указанного метода и client_id.
63
+
64
+ Args:
65
+ client_id: Идентификатор клиента
66
+ config: Конфигурация ограничителя метода
67
+
68
+ Returns:
69
+ AsyncLimiter: Ограничитель для метода
70
+ """
71
+ limiter_key = self._generate_limiter_key(client_id, config.method_identifier)
72
+ current_time = time.monotonic()
73
+
74
+ async with self._lock:
75
+ if limiter_key not in self._rate_limiters:
76
+ limiter = AsyncLimiter(config.limit_requests, config.interval_seconds)
77
+ self._rate_limiters[limiter_key] = limiter
78
+ self._limiter_configs[limiter_key] = config
79
+ self._last_instance_creation[limiter_key] = current_time
80
+ logger.debug(
81
+ f"Инициализирован ограничитель запросов для метода {config.method_identifier} "
82
+ f"ClientID {client_id}: {config.limit_requests} запросов в {config.interval_seconds} сек"
83
+ )
84
+
85
+ self._last_used[limiter_key] = current_time
86
+ return self._rate_limiters[limiter_key]
87
+
88
+ async def _cleanup_unused_limiters(self) -> None:
89
+ """Очистка неиспользуемых ограничителей методов с учетом минимального времени жизни."""
90
+ async with self._lock:
91
+ current_time = time.monotonic()
92
+ limiters_to_remove = []
93
+
94
+ for limiter_key in list(self._last_used.keys()):
95
+ last_used = self._last_used[limiter_key]
96
+ last_creation = self._last_instance_creation.get(limiter_key, last_used)
97
+ time_since_creation = current_time - last_creation
98
+ time_since_usage = current_time - last_used
99
+
100
+ if (time_since_usage > self._cleanup_interval and
101
+ time_since_creation > self._min_instance_ttl):
102
+ limiters_to_remove.append(limiter_key)
103
+
104
+ for limiter_key in limiters_to_remove:
105
+ config = self._limiter_configs.pop(limiter_key, None)
106
+ self._rate_limiters.pop(limiter_key, None)
107
+ self._last_used.pop(limiter_key, None)
108
+ self._last_instance_creation.pop(limiter_key, None)
109
+ if config:
110
+ logger.debug(f"Очищен ограничитель для метода {config.method_identifier}")
111
+
112
+ async def _cleanup_loop(self) -> None:
113
+ """Фоновая задача для очистки неиспользуемых ограничителей."""
114
+ while not self._shutdown:
115
+ try:
116
+ await asyncio.sleep(self._cleanup_interval)
117
+ await self._cleanup_unused_limiters()
118
+ except asyncio.CancelledError:
119
+ break
120
+ except Exception as e:
121
+ logger.error(f"Ошибка в cleanup loop методов: {e}")
122
+ await asyncio.sleep(60)
123
+
124
+ async def get_limiter_stats(self) -> dict[str, dict[str, Any]]:
125
+ """Формирует статистику по ограничителям методов."""
126
+ current_time = time.monotonic()
127
+ async with self._lock:
128
+ stats = {}
129
+ for limiter_key in self._rate_limiters:
130
+ config = self._limiter_configs.get(limiter_key)
131
+ last_used = self._last_used.get(limiter_key, current_time)
132
+ last_creation = self._last_instance_creation.get(limiter_key, current_time)
133
+
134
+ if config:
135
+ stats[limiter_key] = {
136
+ "config": config,
137
+ "last_used": last_used,
138
+ "last_instance_creation": last_creation,
139
+ "time_since_creation": current_time - last_creation,
140
+ "time_since_usage": current_time - last_used,
141
+ }
142
+ return stats
143
+
144
+ def method_rate_limit(limit_requests: int, interval_seconds: float):
145
+ """
146
+ Декоратор для применения дополнительных ограничений частоты запросов к методам API.
147
+
148
+ Args:
149
+ limit_requests: Максимальное количество запросов в указанный интервал
150
+ interval_seconds: Временной интервал в секундах
151
+
152
+ Returns:
153
+ Декоратор метода
154
+ """
155
+
156
+ def decorator(method):
157
+ # Генерируем уникальный идентификатор метода
158
+ method_identifier = f"{method.__module__}.{method.__qualname__}"
159
+ config = MethodRateLimitConfig(
160
+ limit_requests=limit_requests,
161
+ interval_seconds=interval_seconds,
162
+ method_identifier=method_identifier
163
+ )
164
+
165
+ @wraps(method)
166
+ async def wrapper(self, *args, **kwargs):
167
+ # Проверяем, что экземпляр имеет необходимые атрибуты
168
+ if not hasattr(self, '_client_id') or not hasattr(self, '_method_rate_limiter_manager'):
169
+ logger.warning(
170
+ f"Метод {method_identifier} вызван без инициализации ограничителей. "
171
+ "Ограничения не применяются."
172
+ )
173
+ return await method(self, *args, **kwargs)
174
+
175
+ # Дополнительная проверка, что менеджер не None
176
+ if self._method_rate_limiter_manager is None:
177
+ logger.warning(
178
+ f"Менеджер ограничителей методов не инициализирован для {method_identifier}. "
179
+ "Ограничения не применяются."
180
+ )
181
+ return await method(self, *args, **kwargs)
182
+
183
+ # Получаем ограничитель запросов для этого метода
184
+ method_limiter = await self._method_rate_limiter_manager.get_limiter(
185
+ self._client_id, config
186
+ )
187
+
188
+ # Применяем ограничитель запросов
189
+ async with method_limiter:
190
+ logger.debug(
191
+ f"Применен ограничитель метода {method_identifier} для ClientID {self._client_id}: "
192
+ f"{limit_requests} запросов в {interval_seconds} сек"
193
+ )
194
+ return await method(self, *args, **kwargs)
195
+ # Добавляем метаданные
196
+ wrapper._rate_limit_config = config
197
+ return wrapper
198
+
199
+ return decorator
@@ -0,0 +1,174 @@
1
+ import asyncio
2
+ import time
3
+ from typing import Optional, Any
4
+
5
+ from aiolimiter import AsyncLimiter
6
+ from loguru import logger
7
+
8
+
9
+ class RateLimiterConfig:
10
+ """Конфигурация ограничителя кол-ва запросов."""
11
+
12
+ def __init__(
13
+ self,
14
+ max_requests: int = 50,
15
+ time_window: float = 1.0,
16
+ ) -> None:
17
+ self.max_requests = max_requests
18
+ self.time_window = time_window
19
+
20
+ def __repr__(self) -> str:
21
+ return f"RateLimiterConfig(max_requests={self.max_requests}, time_window={self.time_window})"
22
+
23
+
24
+ class RateLimiterManager:
25
+ """
26
+ Менеджер для управления ограничителями запросов по client_id.
27
+ Обеспечивает общий лимит запросов для всех экземпляров с одинаковым client_id.
28
+ """
29
+
30
+ def __init__(self, cleanup_interval: float = 300.0, min_instance_ttl: float = 300.0) -> None:
31
+ self._rate_limiters: dict[str, AsyncLimiter] = {}
32
+ self._instance_refs: dict[str, set[int]] = {}
33
+ self._configs: dict[str, RateLimiterConfig] = {}
34
+ self._last_instance_creation: dict[str, float] = {}
35
+ self._lock = asyncio.Lock()
36
+ self._cleanup_task: Optional[asyncio.Task] = None
37
+ self._shutdown = False
38
+ self._cleanup_interval = cleanup_interval
39
+ self._min_instance_ttl = min_instance_ttl
40
+
41
+ async def start(self) -> None:
42
+ """Запуск фоновых задач менеджера."""
43
+ if self._cleanup_task is None:
44
+ self._cleanup_task = asyncio.create_task(self._cleanup_loop())
45
+ logger.debug(f"Менеджер ограничителей общих клиентских запросов запущен")
46
+
47
+ async def shutdown(self) -> None:
48
+ """Корректное завершение работы менеджера."""
49
+ self._shutdown = True
50
+ if self._cleanup_task:
51
+ self._cleanup_task.cancel()
52
+ try:
53
+ await self._cleanup_task
54
+ except asyncio.CancelledError:
55
+ logger.debug("Задача очистки общих ограничителей клиентских запросов отменена")
56
+ self._cleanup_task = None
57
+
58
+ logger.debug("Менеджер общих ограничителей клиентских запросов остановлен")
59
+
60
+ async def get_limiter(self, client_id: str, config: RateLimiterConfig) -> AsyncLimiter:
61
+ """
62
+ Получает ограничитель кол-ва запросов для указанного client_id.
63
+
64
+ Args:
65
+ client_id: Идентификатор клиента
66
+ config: Конфигурация ограничителя
67
+
68
+ Returns:
69
+ AsyncLimiter: Общий ограничитель для client_id
70
+ """
71
+ async with self._lock:
72
+ if client_id not in self._rate_limiters:
73
+ limiter = AsyncLimiter(config.max_requests, config.time_window)
74
+ self._rate_limiters[client_id] = limiter
75
+ self._configs[client_id] = config
76
+ if client_id not in self._instance_refs:
77
+ self._instance_refs[client_id] = set()
78
+ if client_id not in self._last_instance_creation:
79
+ self._last_instance_creation[client_id] = time.monotonic()
80
+ logger.debug(f"Инициализирован новый общий ограничитель запросов для ClientID {client_id}: {config}")
81
+
82
+ return self._rate_limiters[client_id]
83
+
84
+ async def register_instance(self, client_id: str, instance_id: int) -> None:
85
+ """
86
+ Регистрирует экземпляр класса.
87
+
88
+ Args:
89
+ client_id: Идентификатор клиента
90
+ instance_id: Уникальный идентификатор экземпляра
91
+ """
92
+ current_time = time.monotonic()
93
+ async with self._lock:
94
+ if client_id not in self._instance_refs:
95
+ self._instance_refs[client_id] = set()
96
+ self._last_instance_creation[client_id] = current_time
97
+ self._instance_refs[client_id].add(instance_id)
98
+ logger.debug(f"Зарегистрировано подключение к API {instance_id} для ClientID {client_id}")
99
+
100
+ async def unregister_instance(self, client_id: str, instance_id: int) -> None:
101
+ """
102
+ Удаляет регистрацию экземпляра.
103
+
104
+ Args:
105
+ client_id: Идентификатор клиента
106
+ instance_id: Уникальный идентификатор экземпляра
107
+ """
108
+ async with self._lock:
109
+ if client_id in self._instance_refs:
110
+ self._instance_refs[client_id].discard(instance_id)
111
+ logger.debug(f"Отменена регистрация подключения к API {instance_id} для ClientID {client_id}")
112
+
113
+ async def _cleanup_unused_limiters(self) -> None:
114
+ """Очистка неиспользуемых ограничителей кол-ва запросов с учетом минимального времени жизни."""
115
+ async with self._lock:
116
+ current_time = time.monotonic()
117
+ clients_to_remove = []
118
+
119
+ for client_id, instances in self._instance_refs.items():
120
+ if not instances:
121
+ last_creation = self._last_instance_creation.get(client_id, 0)
122
+ time_since_creation = current_time - last_creation
123
+
124
+ # Планируем очистку, если прошло достаточно времени с создания последнего экземпляра
125
+ if time_since_creation > self._min_instance_ttl:
126
+ clients_to_remove.append(client_id)
127
+
128
+ for client_id in clients_to_remove:
129
+ self._rate_limiters.pop(client_id, None)
130
+ self._configs.pop(client_id, None)
131
+ self._instance_refs.pop(client_id, None)
132
+ self._last_instance_creation.pop(client_id, None)
133
+ logger.debug(f"Очищены ресурсы общего ограничителя запросов для ClientID {client_id}")
134
+
135
+ async def _cleanup_loop(self) -> None:
136
+ """Фоновая задача для очистки неиспользуемых ограничителей кол-ва запросов."""
137
+ while not self._shutdown:
138
+ try:
139
+ await asyncio.sleep(self._cleanup_interval)
140
+ await self._cleanup_unused_limiters()
141
+ except asyncio.CancelledError:
142
+ break
143
+ except Exception as e:
144
+ logger.error(f"Ошибка в cleanup loop: {e}")
145
+ await asyncio.sleep(60) # Пауза при ошибках
146
+
147
+ async def get_active_client_ids(self) -> list[str]:
148
+ """Формирует список активных client_id."""
149
+ async with self._lock:
150
+ return [cid for cid, instances in self._instance_refs.items() if instances]
151
+
152
+ async def get_instance_stats(self) -> dict[str, int]:
153
+ """Формирует статистику по экземплярам."""
154
+ async with self._lock:
155
+ return {
156
+ client_id: len(instances)
157
+ for client_id, instances in self._instance_refs.items()
158
+ if instances
159
+ }
160
+
161
+ async def get_limiter_stats(self) -> dict[str, dict[str, Any]]:
162
+ """Формирует детальную статистику по ограничителям."""
163
+ current_time = time.monotonic()
164
+ async with self._lock:
165
+ return {
166
+ client_id: {
167
+ "config": self._configs[client_id],
168
+ "instances": len(self._instance_refs[client_id]),
169
+ "limiter": str(self._rate_limiters[client_id]),
170
+ "last_instance_creation": self._last_instance_creation.get(client_id),
171
+ "time_since_creation": current_time - self._last_instance_creation.get(client_id, current_time),
172
+ }
173
+ for client_id in self._rate_limiters
174
+ }
@@ -0,0 +1,75 @@
1
+ import asyncio
2
+ from contextlib import asynccontextmanager
3
+ from typing import AsyncIterator
4
+
5
+ from aiohttp import ClientSession, ClientTimeout, TCPConnector
6
+ from loguru import logger
7
+
8
+
9
+ class SessionManager:
10
+ """Менеджер для управления HTTP-сессиями."""
11
+
12
+ def __init__(self, timeout: float = 30.0, connector_limit: int = 100) -> None:
13
+ self._sessions: dict[str, ClientSession] = {}
14
+ self._session_refs: dict[str, set[int]] = {}
15
+ self._lock = asyncio.Lock()
16
+ self._timeout = ClientTimeout(total=timeout)
17
+ self._connector_limit = connector_limit
18
+
19
+ @asynccontextmanager
20
+ async def get_session(self, client_id: str, api_key: str, instance_id: int) -> AsyncIterator[ClientSession]:
21
+ """
22
+ Получает сессию для client_id.
23
+
24
+ Args:
25
+ client_id: Идентификатор клиента
26
+ api_key: Ключ API
27
+ instance_id: ID экземпляра
28
+
29
+ Yields:
30
+ ClientSession: HTTP-сессия
31
+ """
32
+ async with self._lock:
33
+ if client_id not in self._sessions:
34
+ self._sessions[client_id] = ClientSession(
35
+ headers={
36
+ "Client-Id": client_id,
37
+ "Api-Key": api_key,
38
+ },
39
+ timeout=self._timeout,
40
+ connector=TCPConnector(limit=self._connector_limit)
41
+ )
42
+ self._session_refs[client_id] = set()
43
+ logger.debug(f"Создана новая сессия для ClientID {client_id}")
44
+
45
+ self._session_refs[client_id].add(instance_id)
46
+
47
+ try:
48
+ yield self._sessions[client_id]
49
+ finally:
50
+ async with self._lock:
51
+ if client_id in self._session_refs:
52
+ self._session_refs[client_id].discard(instance_id)
53
+
54
+ async def close_session(self, client_id: str) -> None:
55
+ """Закрывает сессию для client_id."""
56
+ async with self._lock:
57
+ if client_id in self._sessions:
58
+ if client_id in self._session_refs and self._session_refs[client_id]:
59
+ return
60
+ session = self._sessions.pop(client_id)
61
+ self._session_refs.pop(client_id, None)
62
+ if not session.closed:
63
+ await session.close()
64
+ logger.debug(f"Сессия для ClientID {client_id} закрыта")
65
+
66
+ async def close_all(self) -> None:
67
+ """Закрывает все сессии."""
68
+ async with self._lock:
69
+ for client_id, session in list(self._sessions.items()):
70
+ if not session.closed:
71
+ await session.close()
72
+ logger.debug(f"Сессия для ClientID {client_id} закрыта")
73
+ self._sessions.clear()
74
+ self._session_refs.clear()
75
+ logger.debug("Все сессии закрыты")
@@ -0,0 +1,15 @@
1
+ __all__ = [
2
+ "SellerBarcodeAPI",
3
+ "SellerCategoryAPI",
4
+ "SellerFBSAPI",
5
+ "SellerPricesAndStocksAPI",
6
+ "SellerProductAPI",
7
+ "SellerWarehouseAPI",
8
+ ]
9
+
10
+ from .attributes_and_characteristics import SellerCategoryAPI
11
+ from .barcodes import SellerBarcodeAPI
12
+ from .fbs import SellerFBSAPI
13
+ from .prices_and_stocks import SellerPricesAndStocksAPI
14
+ from .products import SellerProductAPI
15
+ from .warehouses import SellerWarehouseAPI
@@ -0,0 +1,177 @@
1
+ from ..core import APIManager
2
+ from ..schemas.attributes_and_characteristics import (
3
+ DescriptionCategoryAttributeResponse,
4
+ DescriptionCategoryTreeResponse,
5
+ DescriptionCategoryTreeRequest,
6
+ DescriptionCategoryAttributeRequest,
7
+ DescriptionCategoryAttributeValuesRequest,
8
+ DescriptionCategoryAttributeValuesResponse,
9
+ DescriptionCategoryAttributeValuesSearchRequest,
10
+ DescriptionCategoryAttributeValuesSearchResponse
11
+ )
12
+
13
+
14
+ class SellerCategoryAPI(APIManager):
15
+ """Реализует методы раздела Атрибуты и характеристики Ozon.
16
+
17
+ References:
18
+ https://docs.ozon.ru/api/seller/?__rr=1#tag/CategoryAPI
19
+ """
20
+
21
+ async def description_category_tree(
22
+ self: "SellerCategoryAPI",
23
+ request: DescriptionCategoryTreeRequest = DescriptionCategoryTreeRequest.model_construct(),
24
+ ) -> DescriptionCategoryTreeResponse:
25
+ """Возвращает категории и типы для товаров в виде дерева.
26
+ Создание товаров доступно только в категориях последнего уровня, сравните именно их с категориями на своей площадке.
27
+ Категории не создаются по запросу пользователя.
28
+
29
+ Notes:
30
+ Внимательно выбирайте категорию для товара: для разных категорий применяется разный размер комиссии.
31
+
32
+ References:
33
+ https://docs.ozon.ru/api/seller/#operation/DescriptionCategoryAPI_GetTree
34
+
35
+ Args:
36
+ request: Запрос к серверу по схеме `DescriptionCategoryTreeRequest`
37
+
38
+ Returns:
39
+ Категории и типы для товаров в виде дерева по схеме `DescriptionCategoryTreeResponse`
40
+
41
+ Example:
42
+ async with SellerAPI(client_id, api_key) as api:
43
+ description_category_tree = await api.description_category_tree()
44
+ """
45
+ response = await self._request(
46
+ method="post",
47
+ api_version="v1",
48
+ endpoint="description-category/tree",
49
+ json=request.model_dump()
50
+ )
51
+ return DescriptionCategoryTreeResponse(**response)
52
+
53
+ async def description_category_attribute(
54
+ self: "SellerCategoryAPI",
55
+ request: DescriptionCategoryAttributeRequest,
56
+ ) -> DescriptionCategoryAttributeResponse:
57
+ """Получение характеристик для указанных категории и типа товара.
58
+ Если у `dictionary_id` значение `0`, у атрибута нет вложенных справочников. Если значение другое, то справочники есть.
59
+ Запросите их методом `description_category_attribute_values()`.
60
+
61
+ Notes:
62
+ • `attribute_id` - Идентификатор характеристики, можно получить с помощью метода `description_category_attribute()`
63
+ • `description_category_id` - Идентификатор категории, можно получить с помощью метода `description_category_tree()`
64
+ • `type_id` - Идентификатор типа товара, можно получить с помощью метода `description_category_tree()`
65
+
66
+ References:
67
+ https://docs.ozon.ru/api/seller/#operation/DescriptionCategoryAPI_GetAttributes
68
+
69
+ Args:
70
+ request: Запрос к серверу gо схеме `DescriptionCategoryAttributeRequest`
71
+
72
+ Returns:
73
+ Характеристики для указанных категории и типа товара по схеме `DescriptionCategoryAttributeResponse`
74
+
75
+ Example:
76
+ async with SellerAPI(client_id, api_key) as api:
77
+ description_category_attribute = await api.description_category_attribute(
78
+ DescriptionCategoryAttributeRequest(
79
+ description_category_id=200000933,
80
+ type_id=93080,
81
+ language=Language.DEFAULT,
82
+ )
83
+ )
84
+ """
85
+ response = await self._request(
86
+ method="post",
87
+ api_version="v1",
88
+ endpoint="description-category/attribute",
89
+ json=request.model_dump(),
90
+ )
91
+ return DescriptionCategoryAttributeResponse(**response)
92
+
93
+ async def description_category_attribute_values(
94
+ self: "SellerCategoryAPI",
95
+ request: DescriptionCategoryAttributeValuesRequest,
96
+ ) -> DescriptionCategoryAttributeValuesResponse:
97
+ """Возвращает справочник значений характеристики.
98
+ Узнать, есть ли вложенный справочник, можно через метод `description_category_attribute()`.
99
+
100
+ Notes:
101
+ • `attribute_id` - Идентификатор характеристики, можно получить с помощью метода `description_category_attribute()`
102
+ • `description_category_id` - Идентификатор категории, можно получить с помощью метода `description_category_tree()`
103
+ • `type_id` - Идентификатор типа товара, можно получить с помощью метода `description_category_tree()`
104
+ • Для пагинации используйте значение `last_value_id`
105
+
106
+ References:
107
+ https://docs.ozon.ru/api/seller/?__rr=2&abt_att=1#operation/DescriptionCategoryAPI_GetAttributeValues
108
+
109
+ Args:
110
+ request: Запрос к серверу по схеме `DescriptionCategoryAttributeValuesRequest`
111
+
112
+ Returns:
113
+ Cправочник значений характеристики по схеме `DescriptionCategoryAttributeValuesResponse`
114
+
115
+ Example:
116
+ async with SellerAPI(client_id, api_key) as api:
117
+ description_category_attribute_values = await api.description_category_attribute_values(
118
+ DescriptionCategoryAttributeValuesRequest(
119
+ attribute_id=85,
120
+ description_category_id=17054869,
121
+ language=Language.DEFAULT,
122
+ last_value_id=0,
123
+ limit=100,
124
+ type_id=97311
125
+ )
126
+ )
127
+ """
128
+ response = await self._request(
129
+ method="post",
130
+ api_version="v1",
131
+ endpoint="description-category/attribute/values",
132
+ json=request.model_dump(),
133
+ )
134
+ return DescriptionCategoryAttributeValuesResponse(**response)
135
+
136
+
137
+ async def description_category_attribute_values_search(
138
+ self: "SellerCategoryAPI",
139
+ request: DescriptionCategoryAttributeValuesSearchRequest,
140
+ ) -> DescriptionCategoryAttributeValuesSearchResponse:
141
+ """Возвращает справочные значения характеристики по заданному значению value в запросе.
142
+ Узнать, есть ли вложенный справочник, можно через метод `description_category_attribute()`.
143
+
144
+ Notes:
145
+ • `attribute_id` - Идентификатор характеристики, можно получить с помощью метода `description_category_attribute()`
146
+ • `description_category_id` - Идентификатор категории, можно получить с помощью метода `description_category_tree()`
147
+ • `type_id` - Идентификатор типа товара, можно получить с помощью метода `description_category_tree()`
148
+ • `value` - Поисковый запрос (минимум 2 символа)
149
+
150
+ References:
151
+ https://docs.ozon.ru/api/seller/?__rr=2&abt_att=1#operation/DescriptionCategoryAPI_SearchAttributeValues
152
+
153
+ Args:
154
+ request: Запрос к серверу по схеме `DescriptionCategoryAttributeValuesSearchRequest`
155
+
156
+ Returns:
157
+ Справочные значения характеристики по заданному значению value в запросе по схеме `DescriptionCategoryAttributeValuesSearchResponse`
158
+
159
+ Example:
160
+ async with SellerAPI(client_id, api_key) as api:
161
+ description_category_attribute_values = await api.description_category_attribute_values_search(
162
+ DescriptionCategoryAttributeValuesSearchRequest(
163
+ attribute_id=85,
164
+ description_category_id=17054869,
165
+ limit=100,
166
+ type_id=97311,
167
+ value="Красота"
168
+ )
169
+ )
170
+ """
171
+ response = await self._request(
172
+ method="post",
173
+ api_version="v1",
174
+ endpoint="description-category/attribute/values/search",
175
+ json=request.model_dump(),
176
+ )
177
+ return DescriptionCategoryAttributeValuesSearchResponse(**response)