pyaterochka-api 0.1.7__tar.gz → 0.1.9__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 (23) hide show
  1. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/PKG-INFO +10 -3
  2. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/README.md +8 -1
  3. pyaterochka_api-0.1.9/pyaterochka_api/__init__.py +4 -0
  4. pyaterochka_api-0.1.9/pyaterochka_api/api.py +180 -0
  5. pyaterochka_api-0.1.9/pyaterochka_api/enums.py +14 -0
  6. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyaterochka_api/manager.py +42 -40
  7. pyaterochka_api-0.1.9/pyaterochka_api/tools.py +121 -0
  8. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyaterochka_api.egg-info/PKG-INFO +10 -3
  9. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyaterochka_api.egg-info/SOURCES.txt +4 -3
  10. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyaterochka_api.egg-info/requires.txt +1 -1
  11. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/setup.py +2 -2
  12. pyaterochka_api-0.1.7/tests/base_tests.py → pyaterochka_api-0.1.9/tests/api_tests.py +16 -25
  13. pyaterochka_api-0.1.9/tests/tools_tests.py +30 -0
  14. pyaterochka_api-0.1.7/pyaterochka_api/__init__.py +0 -3
  15. pyaterochka_api-0.1.7/pyaterochka_api/api.py +0 -279
  16. pyaterochka_api-0.1.7/tests/snapshots/__init__.py +0 -0
  17. pyaterochka_api-0.1.7/tests/snapshots/snap_base_tests.py +0 -813
  18. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/LICENSE +0 -0
  19. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyaterochka_api.egg-info/dependency_links.txt +0 -0
  20. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyaterochka_api.egg-info/top_level.txt +0 -0
  21. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/pyproject.toml +0 -0
  22. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/setup.cfg +0 -0
  23. {pyaterochka_api-0.1.7 → pyaterochka_api-0.1.9}/tests/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaterochka_api
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A Python API client for Pyaterochka store catalog
5
5
  Home-page: https://github.com/Open-Inflation/pyaterochka_api
6
6
  Author: Miskler
@@ -28,7 +28,7 @@ Requires-Dist: tqdm
28
28
  Provides-Extra: tests
29
29
  Requires-Dist: pytest; extra == "tests"
30
30
  Requires-Dist: pytest-asyncio; extra == "tests"
31
- Requires-Dist: snapshottest~=1.0.0a1; extra == "tests"
31
+ Requires-Dist: pytest-typed-schema-shot; extra == "tests"
32
32
  Dynamic: author
33
33
  Dynamic: classifier
34
34
  Dynamic: description
@@ -48,6 +48,7 @@ Pyaterochka (Пятёрочка) - https://5ka.ru/
48
48
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyaterochka_api)
49
49
  ![PyPI - Package Version](https://img.shields.io/pypi/v/pyaterochka_api?color=blue)
50
50
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyaterochka_api?label=PyPi%20downloads)](https://pypi.org/project/pyaterochka-api/)
51
+ [![API Documentation](https://img.shields.io/badge/API-Documentation-blue)](https://open-inflation.github.io/pyaterochka_api/)
51
52
  [![Discord](https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da)](https://discord.gg/UnJnGHNbBp)
52
53
  [![Telegram](https://img.shields.io/badge/Telegram-24A1DE)](https://t.me/miskler_dev)
53
54
 
@@ -69,7 +70,7 @@ camoufox fetch
69
70
 
70
71
  ### Usage / Использование:
71
72
  ```py
72
- from pyaterochka_api import Pyaterochka
73
+ from pyaterochka_api import Pyaterochka, PurchaseMode
73
74
  import asyncio
74
75
 
75
76
 
@@ -146,6 +147,12 @@ if __name__ == '__main__':
146
147
  asyncio.run(main())
147
148
  ```
148
149
 
150
+ ### API Documentation / Документация API
151
+
152
+ Автоматически сгенерированная документация API доступна по ссылке: [API Documentation](https://open-inflation.github.io/pyaterochka_api/)
153
+
154
+ Документация содержит подробную структуру всех ответов сервера в виде схем (на базе тестов).
155
+
149
156
  ### Report / Обратная связь
150
157
 
151
158
  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)!
@@ -6,6 +6,7 @@ Pyaterochka (Пятёрочка) - https://5ka.ru/
6
6
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyaterochka_api)
7
7
  ![PyPI - Package Version](https://img.shields.io/pypi/v/pyaterochka_api?color=blue)
8
8
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyaterochka_api?label=PyPi%20downloads)](https://pypi.org/project/pyaterochka-api/)
9
+ [![API Documentation](https://img.shields.io/badge/API-Documentation-blue)](https://open-inflation.github.io/pyaterochka_api/)
9
10
  [![Discord](https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da)](https://discord.gg/UnJnGHNbBp)
10
11
  [![Telegram](https://img.shields.io/badge/Telegram-24A1DE)](https://t.me/miskler_dev)
11
12
 
@@ -27,7 +28,7 @@ camoufox fetch
27
28
 
28
29
  ### Usage / Использование:
29
30
  ```py
30
- from pyaterochka_api import Pyaterochka
31
+ from pyaterochka_api import Pyaterochka, PurchaseMode
31
32
  import asyncio
32
33
 
33
34
 
@@ -104,6 +105,12 @@ if __name__ == '__main__':
104
105
  asyncio.run(main())
105
106
  ```
106
107
 
108
+ ### API Documentation / Документация API
109
+
110
+ Автоматически сгенерированная документация API доступна по ссылке: [API Documentation](https://open-inflation.github.io/pyaterochka_api/)
111
+
112
+ Документация содержит подробную структуру всех ответов сервера в виде схем (на базе тестов).
113
+
107
114
  ### Report / Обратная связь
108
115
 
109
116
  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)!
@@ -0,0 +1,4 @@
1
+ from .manager import Pyaterochka
2
+ from .enums import PurchaseMode
3
+
4
+ __all__ = ['Pyaterochka', 'PurchaseMode']
@@ -0,0 +1,180 @@
1
+ import aiohttp
2
+ from fake_useragent import UserAgent
3
+ from camoufox import AsyncCamoufox
4
+ import logging
5
+ from .tools import parse_proxy, parse_js, get_env_proxy
6
+
7
+
8
+ class PyaterochkaAPI:
9
+ """
10
+ Класс для загрузки JSON/image и парсинга JavaScript-конфигураций из удаленного источника.
11
+ """
12
+
13
+ def __init__(self,
14
+ debug: bool = False,
15
+ proxy: str | None = None,
16
+ autoclose_browser: bool = False,
17
+ trust_env: bool = False,
18
+ timeout: float = 10.0
19
+ ):
20
+ self._debug = debug
21
+ self._proxy = proxy
22
+ self._session = None
23
+ self._autoclose_browser = autoclose_browser
24
+ self._browser = None
25
+ self._bcontext = None
26
+ self._trust_env = trust_env
27
+ self._timeout = timeout
28
+
29
+ self._logger = logging.getLogger(self.__class__.__name__)
30
+ handler = logging.StreamHandler()
31
+ formatter = logging.Formatter('[%(asctime)s] %(levelname)s %(name)s: %(message)s')
32
+ handler.setFormatter(formatter)
33
+ if not self._logger.hasHandlers():
34
+ self._logger.addHandler(handler)
35
+
36
+ async def fetch(self, url: str) -> tuple[bool, dict | None | str, str]:
37
+ """
38
+ Выполняет HTTP-запрос к указанному URL и возвращает результат.
39
+
40
+ :return: Кортеж (успех, данные или None, тип данных или пустота).
41
+ """
42
+ args = {'url': url, 'timeout': aiohttp.ClientTimeout(total=self._timeout)}
43
+ if self._proxy: args["proxy"] = self._proxy
44
+
45
+ self._logger.info(f'Requesting "{url}" with proxy: "{args.get("proxy") or ("SYSTEM_PROXY" if get_env_proxy() else "WITHOUT")}", timeout: {self._timeout}...')
46
+
47
+ async with self._session.get(**args) as response:
48
+ self._logger.info(f'Response status: {response.status}')
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
+ self._logger.warning('Anti-bot protection. Use Russia IP address and try again.')
61
+ return False, None, ''
62
+ else:
63
+ self._logger.error(f'Unexpected error: {response.status}')
64
+ raise Exception(f"Response status: {response.status} (unknown error/status code)")
65
+
66
+ async def download_config(self, config_url: str) -> dict | None:
67
+ """
68
+ Загружает и парсит JavaScript-конфигурацию с указанного URL.
69
+
70
+ :param config_url: URL для загрузки конфигурации.
71
+ :return: Распарсенные данные в виде словаря или None.
72
+ """
73
+ is_success, js_code, _response_type = await self.fetch(url=config_url)
74
+
75
+ if not is_success:
76
+ if self._debug:
77
+ self._logger.error('Failed to fetch JS code')
78
+ return None
79
+ elif self._debug:
80
+ self._logger.debug('JS code fetched successfully')
81
+
82
+ return await parse_js(js_code=js_code, debug=self._debug, logger=self._logger)
83
+
84
+
85
+ async def browser_fetch(self, url: str, selector: str, state: str = 'attached') -> dict:
86
+ if self._browser is None or self._bcontext is None:
87
+ await self.new_session(include_aiohttp=False, include_browser=True)
88
+
89
+ page = await self._bcontext.new_page()
90
+ await page.goto(url, wait_until='commit', timeout=self._timeout * 1000)
91
+ # Wait until the selector script tag appears
92
+ await page.wait_for_selector(selector=selector, state=state, timeout=self._timeout * 1000)
93
+ content = await page.content()
94
+ await page.close()
95
+
96
+ if self._autoclose_browser:
97
+ await self.close(include_aiohttp=False, include_browser=True)
98
+ return content
99
+
100
+ async def new_session(self, include_aiohttp: bool = True, include_browser: bool = False) -> None:
101
+ await self.close(include_aiohttp=include_aiohttp, include_browser=include_browser)
102
+
103
+ if include_aiohttp:
104
+ args = {
105
+ "headers": {
106
+ "User-Agent": UserAgent().random,
107
+ "Accept": "application/json, text/plain, */*",
108
+ "Accept-Language": "en-GB,en;q=0.5",
109
+ "Accept-Encoding": "gzip, deflate, br, zstd",
110
+ "X-PLATFORM": "webapp",
111
+ "Origin": "https://5ka.ru",
112
+ "Connection": "keep-alive",
113
+ "Sec-Fetch-Dest": "empty",
114
+ "Sec-Fetch-Mode": "cors",
115
+ "Sec-Fetch-Site": "same-site",
116
+ "Pragma": "no-cache",
117
+ "Cache-Control": "no-cache",
118
+ "TE": "trailers",
119
+ },
120
+ "trust_env": self._trust_env,
121
+ }
122
+ self._session = aiohttp.ClientSession(**args)
123
+ self._logger.info(f"A new aiohttp connection has been opened. trust_env: {args.get('trust_env')}")
124
+
125
+ if include_browser:
126
+ prox = parse_proxy(self._proxy, self._trust_env, self._logger)
127
+ self._logger.info(f"Opening new browser connection with proxy: {'SYSTEM_PROXY' if prox and not self._proxy else prox}")
128
+ self._browser = await AsyncCamoufox(headless=not self._debug, proxy=prox, geoip=True).__aenter__()
129
+ self._bcontext = await self._browser.new_context()
130
+ self._logger.info(f"A new browser context has been opened.")
131
+
132
+ async def close(
133
+ self,
134
+ include_aiohttp: bool = True,
135
+ include_browser: bool = False
136
+ ) -> None:
137
+ """
138
+ Close the aiohttp session and/or Camoufox browser if they are open.
139
+ :param include_aiohttp: close aiohttp session if True
140
+ :param include_browser: close browser if True
141
+ """
142
+ to_close = []
143
+ if include_aiohttp:
144
+ to_close.append("session")
145
+ if include_browser:
146
+ to_close.append("bcontext")
147
+ to_close.append("browser")
148
+
149
+ self._logger.info(f"Preparing to close: {to_close if to_close else 'nothing'}")
150
+
151
+ if not to_close:
152
+ self._logger.warning("No connections to close")
153
+ return
154
+
155
+ checks = {
156
+ "session": lambda a: a is not None and not a.closed,
157
+ "browser": lambda a: a is not None,
158
+ "bcontext": lambda a: a is not None
159
+ }
160
+
161
+ for name in to_close:
162
+ attr = getattr(self, f"_{name}", None)
163
+ if checks[name](attr):
164
+ self._logger.info(f"Closing {name} connection...")
165
+ try:
166
+ if name == "browser":
167
+ await attr.__aexit__(None, None, None)
168
+ elif name in ["bcontext", "session"]:
169
+ await attr.close()
170
+ else:
171
+ raise ValueError(f"Unknown connection type: {name}")
172
+
173
+ setattr(self, f"_{name}", None)
174
+ self._logger.info(f"The {name} connection was closed")
175
+ except Exception as e:
176
+ self._logger.error(f"Error closing {name}: {e}")
177
+ else:
178
+ self._logger.warning(f"The {name} connection was not open")
179
+
180
+
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+ class Patterns(Enum):
4
+ JS = r'\s*let\s+n\s*=\s*({.*});\s*' # let n = {...};
5
+ STR = r'(\w+)\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"' # key: "value"
6
+ DICT = r'(\w+)\s*:\s*{(.*?)}' # key: {...}
7
+ LIST = r'(\w+)\s*:\s*\[([^\[\]]*(?:\[.*?\])*)\]' # key: [value]
8
+ FIND = r'\{.*?\}|\[.*?\]' # {} or []
9
+ # http(s)://user:pass@host:port
10
+ PROXY = r'^(?:(?P<scheme>https?:\/\/))?(?:(?P<username>[^:@]+):(?P<password>[^@]+)@)?(?P<host>[^:\/]+)(?::(?P<port>\d+))?$'
11
+
12
+ class PurchaseMode(Enum):
13
+ STORE = "store"
14
+ DELIVERY = "delivery"
@@ -4,23 +4,24 @@ import re
4
4
  import json
5
5
  from io import BytesIO
6
6
  from beartype import beartype
7
+ from .enums import PurchaseMode
7
8
 
8
9
 
9
10
  class Pyaterochka:
10
- BASE_URL = "https://5ka.ru"
11
- API_URL = "https://5d.5ka.ru/api"
11
+ BASE_URL = "https://5ka.ru"
12
+ API_URL = "https://5d.5ka.ru/api"
12
13
  HARDCODE_JS_CONFIG = "https://prod-cdn.5ka.ru/scripts/main.a0c039ea81eb8cf69492.js" # TODO сделать не хардкодным имя файла
13
- DEFAULT_STORE_ID = "Y232"
14
-
15
- class PurchaseMode(Enum):
16
- STORE = "store"
17
- DELIVERY = "delivery"
14
+ DEFAULT_STORE_ID = "Y232"
18
15
 
19
16
  @beartype
20
- def __init__(self, debug: bool = False, proxy: str = None, autoclose_browser: bool = False, trust_env: bool = False, timeout: int = 10):
21
- self._debug = debug
22
- self._proxy = proxy
23
- self.api = PyaterochkaAPI(debug=self._debug, proxy=self._proxy, autoclose_browser=autoclose_browser, trust_env=trust_env, timeout=timeout)
17
+ def __init__(self, debug: bool = False, proxy: str | None = None, autoclose_browser: bool = False, trust_env: bool = False, timeout: float = 10.0):
18
+ self.api = PyaterochkaAPI()
19
+
20
+ self.debug = debug
21
+ self.proxy = proxy
22
+ self.autoclose_browser = autoclose_browser
23
+ self.trust_env = trust_env
24
+ self.timeout = timeout
24
25
 
25
26
  @beartype
26
27
  def __enter__(self):
@@ -47,7 +48,7 @@ class Pyaterochka:
47
48
  session (bool, optional): Whether to create a new session (for all, except product_info). Defaults to True.
48
49
  browser (bool, optional): Whether to create a new browser instance (for product_info). Defaults to False.
49
50
  """
50
- await self.api._new_session(session, browser)
51
+ await self.api.new_session(session, browser)
51
52
 
52
53
  @beartype
53
54
  async def close(self, session: bool = True, browser: bool = True) -> None:
@@ -63,25 +64,23 @@ class Pyaterochka:
63
64
  @beartype
64
65
  def debug(self) -> bool:
65
66
  """If True, it will print debug messages and disable headless in browser."""
66
- return self._debug
67
+ return self.api._debug
67
68
 
68
69
  @debug.setter
69
70
  @beartype
70
71
  def debug(self, value: bool):
71
- self._debug = value
72
- self.api.debug = value
72
+ self.api._debug = value
73
73
 
74
74
  @property
75
75
  @beartype
76
- def proxy(self) -> str:
76
+ def proxy(self) -> str | None:
77
77
  """Proxy for requests. If None, it will be used without proxy."""
78
- return self._proxy
78
+ return self.api._proxy
79
79
 
80
80
  @proxy.setter
81
81
  @beartype
82
- def proxy(self, value: str):
83
- self._proxy = value
84
- self.api.proxy = value
82
+ def proxy(self, value: str | None):
83
+ self.api._proxy = value
85
84
 
86
85
  @property
87
86
  @beartype
@@ -108,13 +107,13 @@ class Pyaterochka:
108
107
 
109
108
  @property
110
109
  @beartype
111
- def timeout(self) -> int:
110
+ def timeout(self) -> float:
112
111
  """Timeout value for the API requests."""
113
112
  return self.api._timeout
114
113
 
115
- @trust_env.setter
114
+ @timeout.setter
116
115
  @beartype
117
- def timeout(self, value: int):
116
+ def timeout(self, value: float):
118
117
  if value <= 0:
119
118
  raise ValueError("Timeout must be greater than 0")
120
119
 
@@ -127,7 +126,7 @@ class Pyaterochka:
127
126
  include_restrict: bool = True,
128
127
  mode: PurchaseMode = PurchaseMode.STORE,
129
128
  sap_code_store_id: str = DEFAULT_STORE_ID
130
- ) -> list[dict] | None:
129
+ ) -> list[dict]:
131
130
  f"""
132
131
  Asynchronously retrieves a list of categories from the Pyaterochka API.
133
132
 
@@ -138,7 +137,7 @@ class Pyaterochka:
138
137
  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.
139
138
 
140
139
  Returns:
141
- dict | None: A dictionary representing the categories list if the request is successful, None otherwise.
140
+ list[dict]: A dictionary representing the categories list if the request is successful, error otherwise.
142
141
 
143
142
  Raises:
144
143
  Exception: If the response status is not 200 (OK) or 403 (Forbidden / Anti-bot).
@@ -155,7 +154,7 @@ class Pyaterochka:
155
154
  mode: PurchaseMode = PurchaseMode.STORE,
156
155
  sap_code_store_id: str = DEFAULT_STORE_ID,
157
156
  limit: int = 30
158
- ) -> dict | None:
157
+ ) -> dict:
159
158
  f"""
160
159
  Asynchronously retrieves a list of products from the Pyaterochka API for a given category.
161
160
 
@@ -166,7 +165,7 @@ class Pyaterochka:
166
165
  limit (int, optional): The maximum number of products to retrieve. Defaults to 30. Must be between 1 and 499.
167
166
 
168
167
  Returns:
169
- dict | None: A dictionary representing the products list if the request is successful, None otherwise.
168
+ dict: A dictionary representing the products list if the request is successful, error otherwise.
170
169
 
171
170
  Raises:
172
171
  ValueError: If the limit is not between 1 and 499.
@@ -184,6 +183,7 @@ class Pyaterochka:
184
183
  async def product_info(self, plu_id: int) -> dict:
185
184
  """
186
185
  Asynchronously retrieves product information from the Pyaterochka API for a given PLU ID. Average time processing 2 seconds (first start 6 seconds).
186
+
187
187
  Args:
188
188
  plu_id (int): The PLU ID of the product.
189
189
  Returns:
@@ -193,7 +193,7 @@ class Pyaterochka:
193
193
  """
194
194
 
195
195
  url = f"{self.BASE_URL}/product/{plu_id}/"
196
- response = await self.api._browser_fetch(url=url, selector='script#__NEXT_DATA__[type="application/json"]')
196
+ response = await self.api.browser_fetch(url=url, selector='script#__NEXT_DATA__[type="application/json"]')
197
197
 
198
198
  match = re.search(
199
199
  r'<script\s+id="__NEXT_DATA__"\s+type="application/json">(.+?)</script>',
@@ -211,7 +211,7 @@ class Pyaterochka:
211
211
  return data
212
212
 
213
213
  @beartype
214
- async def get_news(self, limit: int = None) -> dict | None:
214
+ async def get_news(self, limit: int | None = None) -> dict:
215
215
  """
216
216
  Asynchronously retrieves news from the Pyaterochka API.
217
217
 
@@ -219,7 +219,7 @@ class Pyaterochka:
219
219
  limit (int, optional): The maximum number of news items to retrieve. Defaults to None.
220
220
 
221
221
  Returns:
222
- dict | None: A dictionary representing the news if the request is successful, None otherwise.
222
+ dict: A dictionary representing the news if the request is successful, error otherwise.
223
223
  """
224
224
  url = f"{self.BASE_URL}/api/public/v1/news/"
225
225
  if limit and limit > 0:
@@ -230,7 +230,7 @@ class Pyaterochka:
230
230
  return response
231
231
 
232
232
  @beartype
233
- async def find_store(self, longitude: float, latitude: float) -> dict | None:
233
+ async def find_store(self, longitude: float, latitude: float) -> dict:
234
234
  """
235
235
  Asynchronously finds the store associated with the given coordinates.
236
236
 
@@ -239,7 +239,7 @@ class Pyaterochka:
239
239
  latitude (float): The latitude of the location.
240
240
 
241
241
  Returns:
242
- dict | None: A dictionary representing the store information if the request is successful, None otherwise.
242
+ dict: A dictionary representing the store information if the request is successful, error otherwise.
243
243
  """
244
244
 
245
245
  request_url = f"{self.API_URL}/orders/v1/orders/stores/?lon={longitude}&lat={latitude}"
@@ -247,15 +247,17 @@ class Pyaterochka:
247
247
  return response
248
248
 
249
249
  @beartype
250
- async def download_image(self, url: str) -> BytesIO | None:
250
+ async def download_image(self, url: str) -> BytesIO:
251
251
  is_success, image_data, response_type = await self.api.fetch(url=url)
252
252
 
253
253
  if not is_success:
254
- if self.debug:
255
- print("Failed to fetch image")
256
- return None
257
- elif self.debug:
258
- print("Image fetched successfully")
254
+ self.api._logger.error("Failed to fetch image")
255
+ return
256
+ elif not isinstance(image_data, (bytes, bytearray)):
257
+ self.api._logger.error("Image data is not bytes")
258
+ return
259
+
260
+ self.api._logger.debug("Image fetched successfully")
259
261
 
260
262
  image = BytesIO(image_data)
261
263
  image.name = f'{url.split("/")[-1]}.{response_type.split("/")[-1]}'
@@ -263,7 +265,7 @@ class Pyaterochka:
263
265
  return image
264
266
 
265
267
  @beartype
266
- async def get_config(self) -> dict | None:
268
+ async def get_config(self) -> dict:
267
269
  """
268
270
  Asynchronously retrieves the configuration from the hardcoded JavaScript file.
269
271
 
@@ -271,7 +273,7 @@ class Pyaterochka:
271
273
  debug (bool, optional): Whether to print debug information. Defaults to False.
272
274
 
273
275
  Returns:
274
- dict | None: A dictionary representing the configuration if the request is successful, None otherwise.
276
+ dict: A dictionary representing the configuration if the request is successful, error otherwise.
275
277
  """
276
278
 
277
279
  return await self.api.download_config(config_url=self.HARDCODE_JS_CONFIG)
@@ -0,0 +1,121 @@
1
+ from .enums import Patterns
2
+ import os
3
+ import re
4
+ from tqdm import tqdm
5
+
6
+ def get_env_proxy() -> str | None:
7
+ """
8
+ Получает прокси из переменных окружения.
9
+ :return: Прокси-строка или None.
10
+ """
11
+ proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy") or os.environ.get("HTTP_PROXY") or os.environ.get("http_proxy")
12
+ return proxy if proxy else None
13
+
14
+ def parse_proxy(proxy_str: str | None, trust_env: bool, logger) -> dict | None:
15
+ logger.debug(f"Parsing proxy string: {proxy_str}")
16
+
17
+ if not proxy_str:
18
+ if trust_env:
19
+ logger.debug("Proxy string not provided, checking environment variables for HTTP(S)_PROXY")
20
+ proxy_str = get_env_proxy()
21
+
22
+ if not proxy_str:
23
+ logger.info("No proxy string found, returning None")
24
+ return None
25
+ else:
26
+ logger.info(f"Proxy string found in environment variables")
27
+
28
+ # Example: user:pass@host:port or just host:port
29
+ match = re.match(Patterns.PROXY.value, proxy_str)
30
+
31
+ proxy_dict = {}
32
+ if not match:
33
+ logger.warning(f"Proxy string did not match expected pattern, using basic formating")
34
+ proxy_dict['server'] = proxy_str
35
+
36
+ if not proxy_str.startswith('http://') and not proxy_str.startswith('https://'):
37
+ logger.warning("Proxy string missing protocol, prepending 'http://'")
38
+ proxy_dict['server'] = f"http://{proxy_str}"
39
+
40
+ logger.info(f"Proxy parsed as basic")
41
+ return proxy_dict
42
+ else:
43
+ match_dict = match.groupdict()
44
+ proxy_dict['server'] = f"{match_dict['scheme'] or 'http://'}{match_dict['host']}"
45
+ if match_dict['port']:
46
+ proxy_dict['server'] += f":{match_dict['port']}"
47
+
48
+ for key in ['username', 'password']:
49
+ if match_dict[key]:
50
+ proxy_dict[key] = match_dict[key]
51
+
52
+ logger.info(f"Proxy WITH{'OUT' if 'username' not in proxy_dict else ''} credentials")
53
+
54
+ logger.info(f"Proxy parsed as regex")
55
+ return proxy_dict
56
+
57
+ async def _parse_match(match: str, progress_bar: tqdm | None = None) -> dict:
58
+ result = {}
59
+
60
+ if progress_bar:
61
+ progress_bar.set_description("Parsing strings")
62
+
63
+ # Парсинг строк
64
+ string_matches = re.finditer(Patterns.STR.value, match)
65
+ for m in string_matches:
66
+ key, value = m.group(1), m.group(2)
67
+ result[key] = value.replace('\"', '"').replace('\\', '\\')
68
+
69
+ if progress_bar:
70
+ progress_bar.update(1)
71
+ progress_bar.set_description("Parsing dictionaries")
72
+
73
+ # Парсинг словарей
74
+ dict_matches = re.finditer(Patterns.DICT.value, match)
75
+ for m in dict_matches:
76
+ key, value = m.group(1), m.group(2)
77
+ if not re.search(Patterns.STR.value, value):
78
+ result[key] = await _parse_match(value, progress_bar)
79
+
80
+ if progress_bar:
81
+ progress_bar.update(1)
82
+ progress_bar.set_description("Parsing lists")
83
+
84
+ # Парсинг списков
85
+ list_matches = re.finditer(Patterns.LIST.value, match)
86
+ for m in list_matches:
87
+ key, value = m.group(1), m.group(2)
88
+ if not re.search(Patterns.STR.value, value):
89
+ result[key] = [await _parse_match(item.group(0), progress_bar) for item in re.finditer(Patterns.FIND.value, value)]
90
+
91
+ if progress_bar:
92
+ progress_bar.update(1)
93
+
94
+ return result
95
+
96
+ async def parse_js(js_code: str, debug: bool, logger) -> dict | None:
97
+ """
98
+ Парсит JavaScript-код и извлекает данные из переменной "n".
99
+
100
+ :param js_code: JS-код в виде строки.
101
+ :return: Распарсенные данные в виде словаря или None.
102
+ """
103
+ matches = re.finditer(Patterns.JS.value, js_code)
104
+ match_list = list(matches)
105
+
106
+ logger.debug(f'Found matches {len(match_list)}')
107
+
108
+ progress_bar = tqdm(total=33, desc="Parsing JS", position=0) if debug else None
109
+
110
+ if match_list and len(match_list) >= 1:
111
+ logger.info('Starting to parse match')
112
+ result = await _parse_match(match_list[1].group(0), progress_bar)
113
+
114
+ if progress_bar:
115
+ progress_bar.close()
116
+ logger.info('Complited parsing match')
117
+ return result
118
+ else:
119
+ if progress_bar:
120
+ progress_bar.close()
121
+ raise Exception("N variable in JS code not found")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaterochka_api
3
- Version: 0.1.7
3
+ Version: 0.1.9
4
4
  Summary: A Python API client for Pyaterochka store catalog
5
5
  Home-page: https://github.com/Open-Inflation/pyaterochka_api
6
6
  Author: Miskler
@@ -28,7 +28,7 @@ Requires-Dist: tqdm
28
28
  Provides-Extra: tests
29
29
  Requires-Dist: pytest; extra == "tests"
30
30
  Requires-Dist: pytest-asyncio; extra == "tests"
31
- Requires-Dist: snapshottest~=1.0.0a1; extra == "tests"
31
+ Requires-Dist: pytest-typed-schema-shot; extra == "tests"
32
32
  Dynamic: author
33
33
  Dynamic: classifier
34
34
  Dynamic: description
@@ -48,6 +48,7 @@ Pyaterochka (Пятёрочка) - https://5ka.ru/
48
48
  ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyaterochka_api)
49
49
  ![PyPI - Package Version](https://img.shields.io/pypi/v/pyaterochka_api?color=blue)
50
50
  [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyaterochka_api?label=PyPi%20downloads)](https://pypi.org/project/pyaterochka-api/)
51
+ [![API Documentation](https://img.shields.io/badge/API-Documentation-blue)](https://open-inflation.github.io/pyaterochka_api/)
51
52
  [![Discord](https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da)](https://discord.gg/UnJnGHNbBp)
52
53
  [![Telegram](https://img.shields.io/badge/Telegram-24A1DE)](https://t.me/miskler_dev)
53
54
 
@@ -69,7 +70,7 @@ camoufox fetch
69
70
 
70
71
  ### Usage / Использование:
71
72
  ```py
72
- from pyaterochka_api import Pyaterochka
73
+ from pyaterochka_api import Pyaterochka, PurchaseMode
73
74
  import asyncio
74
75
 
75
76
 
@@ -146,6 +147,12 @@ if __name__ == '__main__':
146
147
  asyncio.run(main())
147
148
  ```
148
149
 
150
+ ### API Documentation / Документация API
151
+
152
+ Автоматически сгенерированная документация API доступна по ссылке: [API Documentation](https://open-inflation.github.io/pyaterochka_api/)
153
+
154
+ Документация содержит подробную структуру всех ответов сервера в виде схем (на базе тестов).
155
+
149
156
  ### Report / Обратная связь
150
157
 
151
158
  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)!