pyaterochka-api 0.1.6__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.
@@ -0,0 +1,3 @@
1
+ from .manager import Pyaterochka
2
+
3
+ __all__ = ['Pyaterochka']
pyaterochka_api/api.py ADDED
@@ -0,0 +1,251 @@
1
+ import aiohttp
2
+ from fake_useragent import UserAgent
3
+ from enum import Enum
4
+ import re
5
+ from tqdm.asyncio import tqdm
6
+ from camoufox import AsyncCamoufox
7
+
8
+
9
+ class PyaterochkaAPI:
10
+ """
11
+ Класс для загрузки JSON/image и парсинга JavaScript-конфигураций из удаленного источника.
12
+ """
13
+
14
+ class Patterns(Enum):
15
+ JS = r'\s*let\s+n\s*=\s*({.*});\s*' # let n = {...};
16
+ STR = r'(\w+)\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"' # key: "value"
17
+ DICT = r'(\w+)\s*:\s*{(.*?)}' # key: {...}
18
+ LIST = r'(\w+)\s*:\s*\[([^\[\]]*(?:\[.*?\])*)\]' # key: [value]
19
+ FIND = r'\{.*?\}|\[.*?\]' # {} or []
20
+
21
+ def __init__(self, debug: bool = False, proxy: str = None, autoclose_browser: bool = False):
22
+ self._debug = debug
23
+ self._proxy = proxy
24
+ self._session = None
25
+ self._autoclose_browser = autoclose_browser
26
+ self._browser = None
27
+ self._bcontext = None
28
+
29
+ @property
30
+ def proxy(self) -> str | None:
31
+ return self._proxy if hasattr(self, '_proxy') else None
32
+
33
+ @proxy.setter
34
+ def proxy(self, value: str | None) -> None:
35
+ self._proxy = value
36
+
37
+ async def fetch(self, url: str) -> tuple[bool, dict | None | str, str]:
38
+ """
39
+ Выполняет HTTP-запрос к указанному URL и возвращает результат.
40
+
41
+ :return: Кортеж (успех, данные или None).
42
+ """
43
+ if self._debug:
44
+ print(f"Requesting \"{url}\"...", flush=True)
45
+
46
+ async with self._session.get(url=url) as response:
47
+ if self._debug:
48
+ print(f"Response status: {response.status}", flush=True)
49
+
50
+ if response.status == 200:
51
+ if response.headers['content-type'] == 'application/json':
52
+ output_response = response.json()
53
+ elif response.headers['content-type'] == 'image/jpeg':
54
+ output_response = response.read()
55
+ else:
56
+ output_response = response.text()
57
+
58
+ return True, await output_response, response.headers['content-type']
59
+ elif response.status == 403:
60
+ if self._debug:
61
+ print("Anti-bot protection. Use Russia IP address and try again.", flush=True)
62
+ return False, None, ''
63
+ else:
64
+ if self._debug:
65
+ print(f"Unexpected error: {response.status}", flush=True)
66
+ raise Exception(f"Response status: {response.status} (unknown error/status code)")
67
+
68
+ async def _parse_js(self, js_code: str) -> dict | None:
69
+ """
70
+ Парсит JavaScript-код и извлекает данные из переменной "n".
71
+
72
+ :param js_code: JS-код в виде строки.
73
+ :return: Распарсенные данные в виде словаря или None.
74
+ """
75
+ matches = re.finditer(self.Patterns.JS.value, js_code)
76
+ match_list = list(matches)
77
+
78
+ if self._debug:
79
+ print(f"Found matches {len(match_list)}")
80
+ progress_bar = tqdm(total=33, desc="Parsing JS", position=0)
81
+
82
+ async def parse_match(match: str) -> dict:
83
+ result = {}
84
+
85
+ if self._debug:
86
+ progress_bar.set_description("Parsing strings")
87
+
88
+ # Парсинг строк
89
+ string_matches = re.finditer(self.Patterns.STR.value, match)
90
+ for m in string_matches:
91
+ key, value = m.group(1), m.group(2)
92
+ result[key] = value.replace('\"', '"').replace('\\', '\\')
93
+
94
+ if self._debug:
95
+ progress_bar.update(1)
96
+ progress_bar.set_description("Parsing dictionaries")
97
+
98
+ # Парсинг словарей
99
+ dict_matches = re.finditer(self.Patterns.DICT.value, match)
100
+ for m in dict_matches:
101
+ key, value = m.group(1), m.group(2)
102
+ if not re.search(self.Patterns.STR.value, value):
103
+ result[key] = await parse_match(value)
104
+
105
+ if self._debug:
106
+ progress_bar.update(1)
107
+ progress_bar.set_description("Parsing lists")
108
+
109
+ # Парсинг списков
110
+ list_matches = re.finditer(self.Patterns.LIST.value, match)
111
+ for m in list_matches:
112
+ key, value = m.group(1), m.group(2)
113
+ if not re.search(self.Patterns.STR.value, value):
114
+ result[key] = [await parse_match(item.group(0)) for item in re.finditer(self.Patterns.FIND.value, value)]
115
+
116
+ if self._debug:
117
+ progress_bar.update(1)
118
+
119
+ return result
120
+
121
+ if match_list and len(match_list) >= 1:
122
+ if self._debug:
123
+ print("Starting to parse match")
124
+ result = await parse_match(match_list[1].group(0))
125
+ if self._debug:
126
+ progress_bar.close()
127
+ return result
128
+ else:
129
+ if self._debug:
130
+ progress_bar.close()
131
+ raise Exception("N variable in JS code not found")
132
+
133
+ async def download_config(self, config_url: str) -> dict | None:
134
+ """
135
+ Загружает и парсит JavaScript-конфигурацию с указанного URL.
136
+
137
+ :param config_url: URL для загрузки конфигурации.
138
+ :return: Распарсенные данные в виде словаря или None.
139
+ """
140
+ is_success, js_code, _response_type = await self.fetch(url=config_url)
141
+
142
+ if not is_success:
143
+ if self._debug:
144
+ print("Failed to fetch JS code")
145
+ return None
146
+ elif self._debug:
147
+ print("JS code fetched successfully")
148
+
149
+ return await self._parse_js(js_code=js_code)
150
+
151
+
152
+ async def _browser_fetch(self, url: str, selector: str, state: str = 'attached') -> dict:
153
+ if self._browser is None or self._bcontext is None:
154
+ await self._new_session(include_aiohttp=False, include_browser=True)
155
+
156
+ page = await self._bcontext.new_page()
157
+ await page.goto(url, wait_until='commit')
158
+ # Wait until the selector script tag appears
159
+ await page.wait_for_selector(selector=selector, state=state)
160
+ content = await page.content()
161
+ await page.close()
162
+
163
+ if self._autoclose_browser:
164
+ await self.close(include_aiohttp=False, include_browser=True)
165
+ return content
166
+
167
+ def _parse_proxy(self, proxy_str: str | None) -> dict | None:
168
+ if not proxy_str:
169
+ return None
170
+
171
+ # Example: user:pass@host:port or just host:port
172
+ match = re.match(
173
+ r'^(?:(?P<scheme>https?:\/\/))?(?:(?P<username>[^:@]+):(?P<password>[^@]+)@)?(?P<host>[^:]+):(?P<port>\d+)$',
174
+ proxy_str,
175
+ )
176
+
177
+ proxy_dict = {}
178
+ if not match:
179
+ proxy_dict['server'] = proxy_str
180
+
181
+ if not proxy_str.startswith('http://') and not proxy_str.startswith('https://'):
182
+ proxy_dict['server'] = f"http://{proxy_str}"
183
+
184
+ return proxy_dict
185
+ else:
186
+ match_dict = match.groupdict()
187
+ proxy_dict['server'] = f"{match_dict['scheme'] or 'http://'}{match_dict['host']}:{match_dict['port']}"
188
+
189
+ for key in ['username', 'password']:
190
+ if match_dict[key]:
191
+ proxy_dict[key] = match_dict[key]
192
+
193
+ return proxy_dict
194
+
195
+ async def _new_session(self, include_aiohttp: bool = True, include_browser: bool = False) -> None:
196
+ await self.close(include_aiohttp=include_aiohttp, include_browser=include_browser)
197
+
198
+ if include_aiohttp:
199
+ args = {"headers": {"User-Agent": UserAgent().random}}
200
+ if self._proxy: args["proxy"] = self._proxy
201
+ self._session = aiohttp.ClientSession(**args)
202
+
203
+ if self._debug: print(f"A new connection aiohttp has been opened. Proxy used: {args.get('proxy')}")
204
+
205
+ if include_browser:
206
+ self._browser = await AsyncCamoufox(headless=not self._debug, proxy=self._parse_proxy(self.proxy), geoip=True).__aenter__()
207
+ self._bcontext = await self._browser.new_context()
208
+
209
+ if self._debug: print(f"A new connection browser has been opened. Proxy used: {self.proxy}")
210
+
211
+ async def close(
212
+ self,
213
+ include_aiohttp: bool = True,
214
+ include_browser: bool = False
215
+ ) -> None:
216
+ """
217
+ Close the aiohttp session and/or Camoufox browser if they are open.
218
+ :param include_aiohttp: close aiohttp session if True
219
+ :param include_browser: close browser if True
220
+ """
221
+ to_close = []
222
+ if include_aiohttp:
223
+ to_close.append("session")
224
+ if include_browser:
225
+ to_close.append("bcontext")
226
+ to_close.append("browser")
227
+
228
+ if not to_close:
229
+ raise ValueError("No connections to close")
230
+
231
+ checks = {
232
+ "session": lambda a: a is not None and not a.closed,
233
+ "browser": lambda a: a is not None,
234
+ "bcontext": lambda a: a is not None
235
+ }
236
+
237
+ for name in to_close:
238
+ attr = getattr(self, f"_{name}", None)
239
+ if checks[name](attr):
240
+ if "browser" in name:
241
+ await attr.__aexit__(None, None, None)
242
+ else:
243
+ await attr.close()
244
+ setattr(self, f"_{name}", None)
245
+ if self._debug:
246
+ print(f"The {name} connection was closed")
247
+ else:
248
+ if self._debug:
249
+ print(f"The {name} connection was not open")
250
+
251
+
@@ -0,0 +1,232 @@
1
+ from .api import PyaterochkaAPI
2
+ from enum import Enum
3
+ import re
4
+ import json
5
+ from io import BytesIO
6
+
7
+
8
+ class Pyaterochka:
9
+ BASE_URL = "https://5ka.ru"
10
+ API_URL = "https://5d.5ka.ru/api"
11
+ HARDCODE_JS_CONFIG = "https://prod-cdn.5ka.ru/scripts/main.a0c039ea81eb8cf69492.js" # TODO сделать не хардкодным имя файла
12
+ DEFAULT_STORE_ID = "Y232"
13
+
14
+ class PurchaseMode(Enum):
15
+ STORE = "store"
16
+ DELIVERY = "delivery"
17
+
18
+ def __init__(self, debug: bool = False, proxy: str = None, autoclose_browser: bool = False):
19
+ self._debug = debug
20
+ self._proxy = proxy
21
+ self.api = PyaterochkaAPI(debug=self._debug, proxy=self._proxy, autoclose_browser=autoclose_browser)
22
+
23
+ def __enter__(self):
24
+ raise NotImplementedError("Use `async with Pyaterochka() as ...:`")
25
+
26
+ def __exit__(self, exc_type, exc_val, exc_tb):
27
+ pass
28
+
29
+ async def __aenter__(self):
30
+ await self.rebuild_connection(session=True)
31
+ return self
32
+
33
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
34
+ await self.close()
35
+
36
+ async def rebuild_connection(self, session: bool = True, browser: bool = False) -> None:
37
+ """
38
+ Rebuilds the connection to the Pyaterochka API.
39
+ Args:
40
+ session (bool, optional): Whether to create a new session (for all, except product_info). Defaults to True.
41
+ browser (bool, optional): Whether to create a new browser instance (for product_info). Defaults to False.
42
+ """
43
+ await self.api._new_session(session, browser)
44
+
45
+ async def close(self, session: bool = True, browser: bool = True) -> None:
46
+ """
47
+ Closes the connection to the Pyaterochka API.
48
+ Args:
49
+ session (bool, optional): Whether to close the session (for all, except product_info). Defaults to True.
50
+ browser (bool, optional): Whether to close the browser instance (for product_info). Defaults to True.
51
+ """
52
+ await self.api.close(include_aiohttp=session, include_browser=browser)
53
+
54
+ @property
55
+ def debug(self) -> bool:
56
+ """If True, it will print debug messages and disable headless in browser."""
57
+ return self._debug
58
+
59
+ @debug.setter
60
+ def debug(self, value: bool):
61
+ self._debug = value
62
+ self.api.debug = value
63
+
64
+ @property
65
+ def proxy(self) -> str:
66
+ """Proxy for requests. If None, it will be used without proxy."""
67
+ return self._proxy
68
+
69
+ @proxy.setter
70
+ def proxy(self, value: str):
71
+ self._proxy = value
72
+ self.api.proxy = value
73
+
74
+ @property
75
+ def autoclose_browser(self) -> bool:
76
+ """If True, the browser closes after each request, clearing all cookies and caches.
77
+ If you have more than one request and this function is enabled, the processing speed will be greatly affected! (all caches are recreated every time)"""
78
+ return self.api._autoclose_browser
79
+
80
+ @proxy.setter
81
+ def autoclose_browser(self, value: bool):
82
+ self.api._autoclose_browser = value
83
+
84
+
85
+ async def categories_list(
86
+ self,
87
+ subcategories: bool = False,
88
+ include_restrict: bool = True,
89
+ mode: PurchaseMode = PurchaseMode.STORE,
90
+ sap_code_store_id: str = DEFAULT_STORE_ID
91
+ ) -> dict | None:
92
+ f"""
93
+ Asynchronously retrieves a list of categories from the Pyaterochka API.
94
+
95
+ Args:
96
+ subcategories (bool, optional): Whether to include subcategories in the response. Defaults to False.
97
+ include_restrict (bool, optional): I DO NOT KNOW WHAT IS IT
98
+ mode (PurchaseMode, optional): The purchase mode to use. Defaults to PurchaseMode.STORE.
99
+ sap_code_store_id (str, optional): The store ID (official name in API is "sap_code") to use. Defaults to "{self.DEFAULT_STORE_ID}". This lib not support search ID stores.
100
+
101
+ Returns:
102
+ dict | None: A dictionary representing the categories list if the request is successful, None otherwise.
103
+
104
+ Raises:
105
+ Exception: If the response status is not 200 (OK) or 403 (Forbidden / Anti-bot).
106
+ """
107
+
108
+ request_url = f"{self.API_URL}/catalog/v2/stores/{sap_code_store_id}/categories?mode={mode.value}&include_restrict={include_restrict}&include_subcategories={1 if subcategories else 0}"
109
+ _is_success, response, _response_type = await self.api.fetch(url=request_url)
110
+ return response
111
+
112
+ async def products_list(
113
+ self,
114
+ category_id: int,
115
+ mode: PurchaseMode = PurchaseMode.STORE,
116
+ sap_code_store_id: str = DEFAULT_STORE_ID,
117
+ limit: int = 30
118
+ ) -> dict | None:
119
+ f"""
120
+ Asynchronously retrieves a list of products from the Pyaterochka API for a given category.
121
+
122
+ Args:
123
+ category_id (int): The ID of the category.
124
+ mode (PurchaseMode, optional): The purchase mode to use. Defaults to PurchaseMode.STORE.
125
+ sap_code_store_id (str, optional): The store ID (official name in API is "sap_code") to use. Defaults to "{self.DEFAULT_STORE_ID}". This lib not support search ID stores.
126
+ limit (int, optional): The maximum number of products to retrieve. Defaults to 30. Must be between 1 and 499.
127
+
128
+ Returns:
129
+ dict | None: A dictionary representing the products list if the request is successful, None otherwise.
130
+
131
+ Raises:
132
+ ValueError: If the limit is not between 1 and 499.
133
+ Exception: If the response status is not 200 (OK) or 403 (Forbidden / Anti-bot).
134
+ """
135
+
136
+ if limit < 1 or limit >= 500:
137
+ raise ValueError("Limit must be between 1 and 499")
138
+
139
+ request_url = f"{self.API_URL}/catalog/v2/stores/{sap_code_store_id}/categories/{category_id}/products?mode={mode.value}&limit={limit}"
140
+ _is_success, response, _response_type = await self.api.fetch(url=request_url)
141
+ return response
142
+
143
+ async def product_info(self, plu_id: int) -> dict:
144
+ """
145
+ Asynchronously retrieves product information from the Pyaterochka API for a given PLU ID. Average time processing 2 seconds (first start 6 seconds).
146
+ Args:
147
+ plu_id (int): The PLU ID of the product.
148
+ Returns:
149
+ dict: A dictionary representing the product information.
150
+ Raises:
151
+ ValueError: If the response does not contain the expected JSON data.
152
+ """
153
+
154
+ url = f"{self.BASE_URL}/product/{plu_id}/"
155
+ response = await self.api._browser_fetch(url=url, selector='script#__NEXT_DATA__[type="application/json"]')
156
+
157
+ match = re.search(
158
+ r'<script\s+id="__NEXT_DATA__"\s+type="application/json">(.+?)</script>',
159
+ response,
160
+ flags=re.DOTALL
161
+ )
162
+ if not match:
163
+ raise ValueError("product_info: Failed to find JSON data in the response")
164
+ json_text = match.group(1)
165
+ data = json.loads(json_text)
166
+ data["props"]["pageProps"]["props"]["productStore"] = json.loads(data["props"]["pageProps"]["props"]["productStore"])
167
+ data["props"]["pageProps"]["props"]["catalogStore"] = json.loads(data["props"]["pageProps"]["props"]["catalogStore"])
168
+ data["props"]["pageProps"]["props"]["filtersPageStore"] = json.loads(data["props"]["pageProps"]["props"]["filtersPageStore"])
169
+
170
+ return data
171
+
172
+ async def get_news(self, limit: int = None) -> dict | None:
173
+ """
174
+ Asynchronously retrieves news from the Pyaterochka API.
175
+
176
+ Args:
177
+ limit (int, optional): The maximum number of news items to retrieve. Defaults to None.
178
+
179
+ Returns:
180
+ dict | None: A dictionary representing the news if the request is successful, None otherwise.
181
+ """
182
+ url = f"{self.BASE_URL}/api/public/v1/news/"
183
+ if limit and limit > 0:
184
+ url += f"?limit={limit}"
185
+
186
+ _is_success, response, _response_type = await self.api.fetch(url=url)
187
+
188
+ return response
189
+
190
+ async def find_store(self, longitude: float, latitude: float) -> dict | None:
191
+ """
192
+ Asynchronously finds the store associated with the given coordinates.
193
+
194
+ Args:
195
+ longitude (float): The longitude of the location.
196
+ latitude (float): The latitude of the location.
197
+
198
+ Returns:
199
+ dict | None: A dictionary representing the store information if the request is successful, None otherwise.
200
+ """
201
+
202
+ request_url = f"{self.API_URL}/orders/v1/orders/stores/?lon={longitude}&lat={latitude}"
203
+ _is_success, response, _response_type = await self.api.fetch(url=request_url)
204
+ return response
205
+
206
+ async def download_image(self, url: str) -> BytesIO | None:
207
+ is_success, image_data, response_type = await self.api.fetch(url=url)
208
+
209
+ if not is_success:
210
+ if self.debug:
211
+ print("Failed to fetch image")
212
+ return None
213
+ elif self.debug:
214
+ print("Image fetched successfully")
215
+
216
+ image = BytesIO(image_data)
217
+ image.name = f'{url.split("/")[-1]}.{response_type.split("/")[-1]}'
218
+
219
+ return image
220
+
221
+ async def get_config(self) -> list | None:
222
+ """
223
+ Asynchronously retrieves the configuration from the hardcoded JavaScript file.
224
+
225
+ Args:
226
+ debug (bool, optional): Whether to print debug information. Defaults to False.
227
+
228
+ Returns:
229
+ list | None: A list representing the configuration if the request is successful, None otherwise.
230
+ """
231
+
232
+ return await self.api.download_config(config_url=self.HARDCODE_JS_CONFIG)
@@ -0,0 +1,141 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyaterochka_api
3
+ Version: 0.1.6
4
+ Summary: A Python API client for Pyaterochka store catalog
5
+ Home-page: https://github.com/Open-Inflation/pyaterochka_api
6
+ Author: Miskler
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Information Technology
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Internet
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: aiohttp
24
+ Requires-Dist: camoufox[geoip]
25
+ Requires-Dist: fake-useragent
26
+ Requires-Dist: tqdm
27
+ Provides-Extra: tests
28
+ Requires-Dist: pytest; extra == "tests"
29
+ Requires-Dist: pytest-asyncio; extra == "tests"
30
+ Requires-Dist: snapshottest~=1.0.0a1; extra == "tests"
31
+ Dynamic: author
32
+ Dynamic: classifier
33
+ Dynamic: description
34
+ Dynamic: description-content-type
35
+ Dynamic: home-page
36
+ Dynamic: license-file
37
+ Dynamic: provides-extra
38
+ Dynamic: requires-dist
39
+ Dynamic: requires-python
40
+ Dynamic: summary
41
+
42
+ # Pyaterochka API *(not official / не официальный)*
43
+
44
+ Pyaterochka (Пятёрочка) - https://5ka.ru/
45
+
46
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyaterochka_api)
47
+ ![PyPI - Package Version](https://img.shields.io/pypi/v/pyaterochka_api?color=blue)
48
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyaterochka_api?label=PyPi%20downloads)](https://pypi.org/project/pyaterochka-api/)
49
+ [![Discord](https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da)](https://discord.gg/UnJnGHNbBp)
50
+ [![Telegram](https://img.shields.io/badge/Telegram-24A1DE)](https://t.me/miskler_dev)
51
+
52
+
53
+
54
+ ## Installation / Установка:
55
+ 1. Install package / Установка пакета:
56
+ ```bash
57
+ pip install pyaterochka_api
58
+ ```
59
+ 2. ***Debian/Ubuntu Linux***: Install dependencies / Установка зависимостей:
60
+ ```bash
61
+ sudo apt update && sudo apt install -y libgtk-3-0 libx11-xcb1
62
+ ```
63
+ 3. Install browser / Установка браузера:
64
+ ```bash
65
+ camoufox fetch
66
+ ```
67
+
68
+ ### Usage / Использование:
69
+ ```py
70
+ from pyaterochka_api import Pyaterochka
71
+ import asyncio
72
+
73
+
74
+ async def main():
75
+ async with Pyaterochka(proxy="user:password@host:port", debug=False, autoclose_browser=False) as API:
76
+ # RUS: Вводим геоточку (самого магазина или рядом с ним) и получаем инфу о магазине
77
+ # ENG: Enter a geolocation (of the store or near it) and get info about the store
78
+ find_store = await API.find_store(longitude=37.63156, latitude=55.73768)
79
+ print(f"Store info output: {find_store!s:.100s}...\n")
80
+
81
+ # RUS: Выводит список всех категорий на сайте
82
+ # ENG: Outputs a list of all categories on the site
83
+ catalog = await API.categories_list(subcategories=True, mode=API.PurchaseMode.DELIVERY)
84
+ print(f"Categories list output: {catalog!s:.100s}...\n")
85
+
86
+ # RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
87
+ # ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
88
+ # Страниц не сущетвует, использовать желаемый лимит (до 499) / Pages do not exist, use the desired limit (up to 499)
89
+ items = await API.products_list(catalog[0]['id'], limit=5)
90
+ print(f"Items list output: {items!s:.100s}...\n")
91
+
92
+ # RUS: Выводит информацию о товаре (по его plu - id товара).
93
+ # Функция в первый раз достаточно долгая, порядка 5-9 секунды, последующие запросы около 2 секунд (если браузер не был закрыт)
94
+ # ENG: Outputs information about the product (by its plu - product id).
95
+ # The function is quite long the first time, about 5-9 seconds, subsequent requests take about 2 seconds (if the browser was not closed)
96
+ info = await API.product_info(43347)
97
+ print(f"Product output: {info["props"]["pageProps"]["props"]['productStore']!s:.100s}...\n")
98
+
99
+ # RUS: Влияет исключительно на функцию выше (product_info), если включено, то после отработки запроса браузер закроется и кеши очищаются.
100
+ # Не рекомендую включать, если вам все же нужно освободить память, лучше использовать API.close(session=False, browser=True)
101
+ # ENG: Affects only the function above (product_info), if enabled, the browser will close after the request is processed and caches are cleared.
102
+ # I do not recommend enabling it, if you still need to free up memory, it is better to use API.close(session=False, browser=True)
103
+ API.autoclose_browser = True
104
+
105
+ # RUS: Выводит список последних промо-акций/новостей (можно поставить ограничитель по количеству, опционально)
106
+ # ENG: Outputs a list of the latest promotions/news (you can set a limit on the number, optionally)
107
+ news = await API.get_news(limit=5)
108
+ print(f"News output: {news!s:.100s}...\n")
109
+
110
+ # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
111
+ # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
112
+ print(f"Main config: {await API.get_config()!s:.100s}...\n")
113
+
114
+ # RUS: Если требуется, можно настроить вывод логов в консоль
115
+ # ENG: If required, you can configure the output of logs in the console
116
+ API.debug = True
117
+
118
+ # RUS: Скачивает картинку товара (возвращает BytesIO или None)
119
+ # ENG: Downloads the product image (returns BytesIO or None)
120
+ image = await API.download_image(url=items['products'][0]['image_links']['normal'][0])
121
+ with open(image.name, 'wb') as f:
122
+ f.write(image.getbuffer())
123
+
124
+ # RUS: Так же как и debug, в рантайме можно переназначить прокси
125
+ # ENG: As with debug, you can reassign the proxy in runtime
126
+ API.proxy = "user:password@host:port"
127
+ # RUS: Чтобы применить изменения, нужно пересоздать подключение (session - aiohttp отвечающее за все, кроме product_info, за него browser)
128
+ # ENG: To apply changes, you need rebuild connection (session - aiohttp responsible for everything except product_info, for it browser)
129
+ await API.rebuild_connection()
130
+ await API.categories_list()
131
+
132
+
133
+ if __name__ == '__main__':
134
+ asyncio.run(main())
135
+ ```
136
+
137
+ ### Report / Обратная связь
138
+
139
+ If you have any problems using it /suggestions, do not hesitate to write to the [project's GitHub](https://github.com/Open-Inflation/pyaterochka_api/issues)!
140
+
141
+ Если у вас возникнут проблемы в использовании / пожелания, не стесняйтесь писать на [GitHub проекта](https://github.com/Open-Inflation/pyaterochka_api/issues)!
@@ -0,0 +1,12 @@
1
+ pyaterochka_api/__init__.py,sha256=s91K-ZPzFZkk93AY_BtXwhpSxZpO1IHi1a3RThd1jtM,60
2
+ pyaterochka_api/api.py,sha256=DHEbtxniZaFnlYUAit3fGBr9dn7wreawjBa1G6huCXU,9829
3
+ pyaterochka_api/manager.py,sha256=7jiGFMT344iklGSifbjNWeBCkeG9HNqQ3V-D0Gc__pA,9603
4
+ pyaterochka_api-0.1.6.dist-info/licenses/LICENSE,sha256=Ee_P5XQUYoJuffzRL24j4GWpqgoWphUOKswpB2f9HcQ,1071
5
+ tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ tests/base_tests.py,sha256=l_qweYyxL507nRv-SteZdrU-yHONDZQxJfyOSi8pmzw,2576
7
+ tests/snapshots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ tests/snapshots/snap_base_tests.py,sha256=0Vc6Vjm8rLyPVbSRUu2xX8XVOEtU_pns2-oJE-EETyQ,27182
9
+ pyaterochka_api-0.1.6.dist-info/METADATA,sha256=ksmYPe29fLKlxRIjVXkXZgcV5a4RbK4pkSV0G_T-3nk,8098
10
+ pyaterochka_api-0.1.6.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
11
+ pyaterochka_api-0.1.6.dist-info/top_level.txt,sha256=PXTSi8y2C5_Mz20pJJFqOUBnjAIAXP_cc38mthvJ2x4,22
12
+ pyaterochka_api-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.4.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Open Inflation
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,2 @@
1
+ pyaterochka_api
2
+ tests
tests/__init__.py ADDED
File without changes