pyaterochka-api 0.1.6__py3-none-any.whl → 0.1.8__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.
- pyaterochka_api/__init__.py +2 -1
- pyaterochka_api/api.py +75 -146
- pyaterochka_api/enums.py +14 -0
- pyaterochka_api/manager.py +88 -41
- pyaterochka_api/tools.py +121 -0
- {pyaterochka_api-0.1.6.dist-info → pyaterochka_api-0.1.8.dist-info}/METADATA +19 -7
- pyaterochka_api-0.1.8.dist-info/RECORD +15 -0
- tests/{base_tests.py → api_tests.py} +11 -11
- tests/snapshots/snap_base_tests.py +4 -0
- tests/tools_tests.py +30 -0
- pyaterochka_api-0.1.6.dist-info/RECORD +0 -12
- {pyaterochka_api-0.1.6.dist-info → pyaterochka_api-0.1.8.dist-info}/WHEEL +0 -0
- {pyaterochka_api-0.1.6.dist-info → pyaterochka_api-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {pyaterochka_api-0.1.6.dist-info → pyaterochka_api-0.1.8.dist-info}/top_level.txt +0 -0
pyaterochka_api/__init__.py
CHANGED
pyaterochka_api/api.py
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
import aiohttp
|
2
2
|
from fake_useragent import UserAgent
|
3
|
-
from enum import Enum
|
4
|
-
import re
|
5
|
-
from tqdm.asyncio import tqdm
|
6
3
|
from camoufox import AsyncCamoufox
|
4
|
+
import logging
|
5
|
+
from .tools import parse_proxy, parse_js, get_env_proxy
|
7
6
|
|
8
7
|
|
9
8
|
class PyaterochkaAPI:
|
@@ -11,41 +10,42 @@ class PyaterochkaAPI:
|
|
11
10
|
Класс для загрузки JSON/image и парсинга JavaScript-конфигураций из удаленного источника.
|
12
11
|
"""
|
13
12
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def __init__(self, debug: bool = False, proxy: str = None, autoclose_browser: bool = False):
|
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
|
+
):
|
22
20
|
self._debug = debug
|
23
21
|
self._proxy = proxy
|
24
22
|
self._session = None
|
25
23
|
self._autoclose_browser = autoclose_browser
|
26
24
|
self._browser = None
|
27
25
|
self._bcontext = None
|
26
|
+
self._trust_env = trust_env
|
27
|
+
self._timeout = timeout
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
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)
|
32
35
|
|
33
|
-
@proxy.setter
|
34
|
-
def proxy(self, value: str | None) -> None:
|
35
|
-
self._proxy = value
|
36
|
-
|
37
36
|
async def fetch(self, url: str) -> tuple[bool, dict | None | str, str]:
|
38
37
|
"""
|
39
38
|
Выполняет HTTP-запрос к указанному URL и возвращает результат.
|
40
39
|
|
41
|
-
:return: Кортеж (успех, данные или None).
|
40
|
+
:return: Кортеж (успех, данные или None, тип данных или пустота).
|
42
41
|
"""
|
43
|
-
|
44
|
-
|
42
|
+
args = {'url': url, 'timeout': aiohttp.ClientTimeout(total=self._timeout)}
|
43
|
+
if self._proxy: args["proxy"] = self._proxy
|
45
44
|
|
46
|
-
|
47
|
-
|
48
|
-
|
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
49
|
|
50
50
|
if response.status == 200:
|
51
51
|
if response.headers['content-type'] == 'application/json':
|
@@ -57,79 +57,12 @@ class PyaterochkaAPI:
|
|
57
57
|
|
58
58
|
return True, await output_response, response.headers['content-type']
|
59
59
|
elif response.status == 403:
|
60
|
-
|
61
|
-
print("Anti-bot protection. Use Russia IP address and try again.", flush=True)
|
60
|
+
self._logger.warning('Anti-bot protection. Use Russia IP address and try again.')
|
62
61
|
return False, None, ''
|
63
62
|
else:
|
64
|
-
|
65
|
-
print(f"Unexpected error: {response.status}", flush=True)
|
63
|
+
self._logger.error(f'Unexpected error: {response.status}')
|
66
64
|
raise Exception(f"Response status: {response.status} (unknown error/status code)")
|
67
65
|
|
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
66
|
async def download_config(self, config_url: str) -> dict | None:
|
134
67
|
"""
|
135
68
|
Загружает и парсит JavaScript-конфигурацию с указанного URL.
|
@@ -141,22 +74,22 @@ class PyaterochkaAPI:
|
|
141
74
|
|
142
75
|
if not is_success:
|
143
76
|
if self._debug:
|
144
|
-
|
77
|
+
self._logger.error('Failed to fetch JS code')
|
145
78
|
return None
|
146
79
|
elif self._debug:
|
147
|
-
|
80
|
+
self._logger.debug('JS code fetched successfully')
|
148
81
|
|
149
|
-
return await
|
82
|
+
return await parse_js(js_code=js_code, debug=self._debug, logger=self._logger)
|
150
83
|
|
151
84
|
|
152
|
-
async def
|
85
|
+
async def browser_fetch(self, url: str, selector: str, state: str = 'attached') -> dict:
|
153
86
|
if self._browser is None or self._bcontext is None:
|
154
|
-
await self.
|
87
|
+
await self.new_session(include_aiohttp=False, include_browser=True)
|
155
88
|
|
156
89
|
page = await self._bcontext.new_page()
|
157
|
-
await page.goto(url, wait_until='commit')
|
90
|
+
await page.goto(url, wait_until='commit', timeout=self._timeout * 1000)
|
158
91
|
# Wait until the selector script tag appears
|
159
|
-
await page.wait_for_selector(selector=selector, state=state)
|
92
|
+
await page.wait_for_selector(selector=selector, state=state, timeout=self._timeout * 1000)
|
160
93
|
content = await page.content()
|
161
94
|
await page.close()
|
162
95
|
|
@@ -164,49 +97,37 @@ class PyaterochkaAPI:
|
|
164
97
|
await self.close(include_aiohttp=False, include_browser=True)
|
165
98
|
return content
|
166
99
|
|
167
|
-
def
|
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:
|
100
|
+
async def new_session(self, include_aiohttp: bool = True, include_browser: bool = False) -> None:
|
196
101
|
await self.close(include_aiohttp=include_aiohttp, include_browser=include_browser)
|
197
102
|
|
198
103
|
if include_aiohttp:
|
199
|
-
args = {
|
200
|
-
|
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
|
+
}
|
201
122
|
self._session = aiohttp.ClientSession(**args)
|
202
|
-
|
203
|
-
if self._debug: print(f"A new connection aiohttp has been opened. Proxy used: {args.get('proxy')}")
|
123
|
+
self._logger.info(f"A new aiohttp connection has been opened. trust_env: {args.get('trust_env')}")
|
204
124
|
|
205
125
|
if include_browser:
|
206
|
-
|
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__()
|
207
129
|
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}")
|
130
|
+
self._logger.info(f"A new browser context has been opened.")
|
210
131
|
|
211
132
|
async def close(
|
212
133
|
self,
|
@@ -225,8 +146,11 @@ class PyaterochkaAPI:
|
|
225
146
|
to_close.append("bcontext")
|
226
147
|
to_close.append("browser")
|
227
148
|
|
149
|
+
self._logger.info(f"Preparing to close: {to_close if to_close else 'nothing'}")
|
150
|
+
|
228
151
|
if not to_close:
|
229
|
-
|
152
|
+
self._logger.warning("No connections to close")
|
153
|
+
return
|
230
154
|
|
231
155
|
checks = {
|
232
156
|
"session": lambda a: a is not None and not a.closed,
|
@@ -237,15 +161,20 @@ class PyaterochkaAPI:
|
|
237
161
|
for name in to_close:
|
238
162
|
attr = getattr(self, f"_{name}", None)
|
239
163
|
if checks[name](attr):
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
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}")
|
247
177
|
else:
|
248
|
-
|
249
|
-
print(f"The {name} connection was not open")
|
178
|
+
self._logger.warning(f"The {name} connection was not open")
|
250
179
|
|
251
180
|
|
pyaterochka_api/enums.py
ADDED
@@ -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"
|
pyaterochka_api/manager.py
CHANGED
@@ -3,36 +3,44 @@ from enum import Enum
|
|
3
3
|
import re
|
4
4
|
import json
|
5
5
|
from io import BytesIO
|
6
|
+
from beartype import beartype
|
7
|
+
from .enums import PurchaseMode
|
6
8
|
|
7
9
|
|
8
10
|
class Pyaterochka:
|
9
|
-
BASE_URL
|
10
|
-
API_URL
|
11
|
+
BASE_URL = "https://5ka.ru"
|
12
|
+
API_URL = "https://5d.5ka.ru/api"
|
11
13
|
HARDCODE_JS_CONFIG = "https://prod-cdn.5ka.ru/scripts/main.a0c039ea81eb8cf69492.js" # TODO сделать не хардкодным имя файла
|
12
|
-
DEFAULT_STORE_ID
|
14
|
+
DEFAULT_STORE_ID = "Y232"
|
13
15
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
self.
|
20
|
-
self.
|
21
|
-
self.
|
16
|
+
@beartype
|
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
|
22
25
|
|
26
|
+
@beartype
|
23
27
|
def __enter__(self):
|
24
28
|
raise NotImplementedError("Use `async with Pyaterochka() as ...:`")
|
25
29
|
|
30
|
+
@beartype
|
26
31
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
27
32
|
pass
|
28
33
|
|
34
|
+
@beartype
|
29
35
|
async def __aenter__(self):
|
30
|
-
await self.rebuild_connection(session=True)
|
36
|
+
await self.rebuild_connection(session=True, browser=False)
|
31
37
|
return self
|
32
38
|
|
39
|
+
@beartype
|
33
40
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
34
41
|
await self.close()
|
35
42
|
|
43
|
+
@beartype
|
36
44
|
async def rebuild_connection(self, session: bool = True, browser: bool = False) -> None:
|
37
45
|
"""
|
38
46
|
Rebuilds the connection to the Pyaterochka API.
|
@@ -40,8 +48,9 @@ class Pyaterochka:
|
|
40
48
|
session (bool, optional): Whether to create a new session (for all, except product_info). Defaults to True.
|
41
49
|
browser (bool, optional): Whether to create a new browser instance (for product_info). Defaults to False.
|
42
50
|
"""
|
43
|
-
await self.api.
|
51
|
+
await self.api.new_session(session, browser)
|
44
52
|
|
53
|
+
@beartype
|
45
54
|
async def close(self, session: bool = True, browser: bool = True) -> None:
|
46
55
|
"""
|
47
56
|
Closes the connection to the Pyaterochka API.
|
@@ -52,43 +61,72 @@ class Pyaterochka:
|
|
52
61
|
await self.api.close(include_aiohttp=session, include_browser=browser)
|
53
62
|
|
54
63
|
@property
|
64
|
+
@beartype
|
55
65
|
def debug(self) -> bool:
|
56
66
|
"""If True, it will print debug messages and disable headless in browser."""
|
57
|
-
return self._debug
|
67
|
+
return self.api._debug
|
58
68
|
|
59
69
|
@debug.setter
|
70
|
+
@beartype
|
60
71
|
def debug(self, value: bool):
|
61
|
-
self._debug = value
|
62
|
-
self.api.debug = value
|
72
|
+
self.api._debug = value
|
63
73
|
|
64
74
|
@property
|
65
|
-
|
75
|
+
@beartype
|
76
|
+
def proxy(self) -> str | None:
|
66
77
|
"""Proxy for requests. If None, it will be used without proxy."""
|
67
|
-
return self._proxy
|
78
|
+
return self.api._proxy
|
68
79
|
|
69
80
|
@proxy.setter
|
70
|
-
|
71
|
-
|
72
|
-
self.api.
|
81
|
+
@beartype
|
82
|
+
def proxy(self, value: str | None):
|
83
|
+
self.api._proxy = value
|
73
84
|
|
74
85
|
@property
|
86
|
+
@beartype
|
75
87
|
def autoclose_browser(self) -> bool:
|
76
88
|
"""If True, the browser closes after each request, clearing all cookies and caches.
|
77
89
|
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
90
|
return self.api._autoclose_browser
|
79
91
|
|
80
|
-
@
|
92
|
+
@autoclose_browser.setter
|
93
|
+
@beartype
|
81
94
|
def autoclose_browser(self, value: bool):
|
82
95
|
self.api._autoclose_browser = value
|
96
|
+
|
97
|
+
@property
|
98
|
+
@beartype
|
99
|
+
def trust_env(self) -> bool:
|
100
|
+
"""Passed directly to aiohttp. Also, if no proxy is specified, the system proxy (variable HTTPS_PROXY) will be used for the browser."""
|
101
|
+
return self.api._trust_env
|
102
|
+
|
103
|
+
@trust_env.setter
|
104
|
+
@beartype
|
105
|
+
def trust_env(self, value: bool):
|
106
|
+
self.api._trust_env = value
|
107
|
+
|
108
|
+
@property
|
109
|
+
@beartype
|
110
|
+
def timeout(self) -> float:
|
111
|
+
"""Timeout value for the API requests."""
|
112
|
+
return self.api._timeout
|
113
|
+
|
114
|
+
@timeout.setter
|
115
|
+
@beartype
|
116
|
+
def timeout(self, value: float):
|
117
|
+
if value <= 0:
|
118
|
+
raise ValueError("Timeout must be greater than 0")
|
83
119
|
|
120
|
+
self.api._timeout = value
|
84
121
|
|
122
|
+
@beartype
|
85
123
|
async def categories_list(
|
86
124
|
self,
|
87
125
|
subcategories: bool = False,
|
88
126
|
include_restrict: bool = True,
|
89
127
|
mode: PurchaseMode = PurchaseMode.STORE,
|
90
128
|
sap_code_store_id: str = DEFAULT_STORE_ID
|
91
|
-
) -> dict
|
129
|
+
) -> list[dict]:
|
92
130
|
f"""
|
93
131
|
Asynchronously retrieves a list of categories from the Pyaterochka API.
|
94
132
|
|
@@ -99,7 +137,7 @@ class Pyaterochka:
|
|
99
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.
|
100
138
|
|
101
139
|
Returns:
|
102
|
-
dict
|
140
|
+
list[dict]: A dictionary representing the categories list if the request is successful, error otherwise.
|
103
141
|
|
104
142
|
Raises:
|
105
143
|
Exception: If the response status is not 200 (OK) or 403 (Forbidden / Anti-bot).
|
@@ -109,24 +147,25 @@ class Pyaterochka:
|
|
109
147
|
_is_success, response, _response_type = await self.api.fetch(url=request_url)
|
110
148
|
return response
|
111
149
|
|
150
|
+
@beartype
|
112
151
|
async def products_list(
|
113
152
|
self,
|
114
|
-
category_id:
|
153
|
+
category_id: str,
|
115
154
|
mode: PurchaseMode = PurchaseMode.STORE,
|
116
155
|
sap_code_store_id: str = DEFAULT_STORE_ID,
|
117
156
|
limit: int = 30
|
118
|
-
) -> dict
|
157
|
+
) -> dict:
|
119
158
|
f"""
|
120
159
|
Asynchronously retrieves a list of products from the Pyaterochka API for a given category.
|
121
160
|
|
122
161
|
Args:
|
123
|
-
category_id (
|
162
|
+
category_id (str): The ID of the (sub)category.
|
124
163
|
mode (PurchaseMode, optional): The purchase mode to use. Defaults to PurchaseMode.STORE.
|
125
164
|
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
165
|
limit (int, optional): The maximum number of products to retrieve. Defaults to 30. Must be between 1 and 499.
|
127
166
|
|
128
167
|
Returns:
|
129
|
-
dict
|
168
|
+
dict: A dictionary representing the products list if the request is successful, error otherwise.
|
130
169
|
|
131
170
|
Raises:
|
132
171
|
ValueError: If the limit is not between 1 and 499.
|
@@ -140,9 +179,11 @@ class Pyaterochka:
|
|
140
179
|
_is_success, response, _response_type = await self.api.fetch(url=request_url)
|
141
180
|
return response
|
142
181
|
|
182
|
+
@beartype
|
143
183
|
async def product_info(self, plu_id: int) -> dict:
|
144
184
|
"""
|
145
185
|
Asynchronously retrieves product information from the Pyaterochka API for a given PLU ID. Average time processing 2 seconds (first start 6 seconds).
|
186
|
+
|
146
187
|
Args:
|
147
188
|
plu_id (int): The PLU ID of the product.
|
148
189
|
Returns:
|
@@ -152,7 +193,7 @@ class Pyaterochka:
|
|
152
193
|
"""
|
153
194
|
|
154
195
|
url = f"{self.BASE_URL}/product/{plu_id}/"
|
155
|
-
response = await self.api.
|
196
|
+
response = await self.api.browser_fetch(url=url, selector='script#__NEXT_DATA__[type="application/json"]')
|
156
197
|
|
157
198
|
match = re.search(
|
158
199
|
r'<script\s+id="__NEXT_DATA__"\s+type="application/json">(.+?)</script>',
|
@@ -169,7 +210,8 @@ class Pyaterochka:
|
|
169
210
|
|
170
211
|
return data
|
171
212
|
|
172
|
-
|
213
|
+
@beartype
|
214
|
+
async def get_news(self, limit: int | None = None) -> dict:
|
173
215
|
"""
|
174
216
|
Asynchronously retrieves news from the Pyaterochka API.
|
175
217
|
|
@@ -177,7 +219,7 @@ class Pyaterochka:
|
|
177
219
|
limit (int, optional): The maximum number of news items to retrieve. Defaults to None.
|
178
220
|
|
179
221
|
Returns:
|
180
|
-
dict
|
222
|
+
dict: A dictionary representing the news if the request is successful, error otherwise.
|
181
223
|
"""
|
182
224
|
url = f"{self.BASE_URL}/api/public/v1/news/"
|
183
225
|
if limit and limit > 0:
|
@@ -187,7 +229,8 @@ class Pyaterochka:
|
|
187
229
|
|
188
230
|
return response
|
189
231
|
|
190
|
-
|
232
|
+
@beartype
|
233
|
+
async def find_store(self, longitude: float, latitude: float) -> dict:
|
191
234
|
"""
|
192
235
|
Asynchronously finds the store associated with the given coordinates.
|
193
236
|
|
@@ -196,29 +239,33 @@ class Pyaterochka:
|
|
196
239
|
latitude (float): The latitude of the location.
|
197
240
|
|
198
241
|
Returns:
|
199
|
-
dict
|
242
|
+
dict: A dictionary representing the store information if the request is successful, error otherwise.
|
200
243
|
"""
|
201
244
|
|
202
245
|
request_url = f"{self.API_URL}/orders/v1/orders/stores/?lon={longitude}&lat={latitude}"
|
203
246
|
_is_success, response, _response_type = await self.api.fetch(url=request_url)
|
204
247
|
return response
|
205
248
|
|
206
|
-
|
249
|
+
@beartype
|
250
|
+
async def download_image(self, url: str) -> BytesIO:
|
207
251
|
is_success, image_data, response_type = await self.api.fetch(url=url)
|
208
252
|
|
209
253
|
if not is_success:
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
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")
|
215
261
|
|
216
262
|
image = BytesIO(image_data)
|
217
263
|
image.name = f'{url.split("/")[-1]}.{response_type.split("/")[-1]}'
|
218
264
|
|
219
265
|
return image
|
220
266
|
|
221
|
-
|
267
|
+
@beartype
|
268
|
+
async def get_config(self) -> dict:
|
222
269
|
"""
|
223
270
|
Asynchronously retrieves the configuration from the hardcoded JavaScript file.
|
224
271
|
|
@@ -226,7 +273,7 @@ class Pyaterochka:
|
|
226
273
|
debug (bool, optional): Whether to print debug information. Defaults to False.
|
227
274
|
|
228
275
|
Returns:
|
229
|
-
|
276
|
+
dict: A dictionary representing the configuration if the request is successful, error otherwise.
|
230
277
|
"""
|
231
278
|
|
232
279
|
return await self.api.download_config(config_url=self.HARDCODE_JS_CONFIG)
|
pyaterochka_api/tools.py
ADDED
@@ -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.
|
3
|
+
Version: 0.1.8
|
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
|
@@ -22,6 +22,7 @@ Description-Content-Type: text/markdown
|
|
22
22
|
License-File: LICENSE
|
23
23
|
Requires-Dist: aiohttp
|
24
24
|
Requires-Dist: camoufox[geoip]
|
25
|
+
Requires-Dist: beartype
|
25
26
|
Requires-Dist: fake-useragent
|
26
27
|
Requires-Dist: tqdm
|
27
28
|
Provides-Extra: tests
|
@@ -43,6 +44,7 @@ Dynamic: summary
|
|
43
44
|
|
44
45
|
Pyaterochka (Пятёрочка) - https://5ka.ru/
|
45
46
|
|
47
|
+
[](https://github.com/Open-Inflation/pyaterochka_api/actions?query=workflow%3A"API+Tests+Daily?query=branch%3Amain")
|
46
48
|

|
47
49
|

|
48
50
|
[](https://pypi.org/project/pyaterochka-api/)
|
@@ -67,12 +69,12 @@ camoufox fetch
|
|
67
69
|
|
68
70
|
### Usage / Использование:
|
69
71
|
```py
|
70
|
-
from pyaterochka_api import Pyaterochka
|
72
|
+
from pyaterochka_api import Pyaterochka, PurchaseMode
|
71
73
|
import asyncio
|
72
74
|
|
73
75
|
|
74
76
|
async def main():
|
75
|
-
async with Pyaterochka(proxy="user:password@host:port", debug=False, autoclose_browser=False) as API:
|
77
|
+
async with Pyaterochka(proxy="user:password@host:port", debug=False, autoclose_browser=False, trust_env=False) as API:
|
76
78
|
# RUS: Вводим геоточку (самого магазина или рядом с ним) и получаем инфу о магазине
|
77
79
|
# ENG: Enter a geolocation (of the store or near it) and get info about the store
|
78
80
|
find_store = await API.find_store(longitude=37.63156, latitude=55.73768)
|
@@ -102,6 +104,12 @@ async def main():
|
|
102
104
|
# 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
105
|
API.autoclose_browser = True
|
104
106
|
|
107
|
+
# RUS: Напрямую передается в aiohttp, так же учитывается в браузере. В первую очередь нужен для использования системного `HTTPS_PROXY`.
|
108
|
+
# Но системный прокси применяется, только если не указали иное напрямую в `API.proxy`.
|
109
|
+
# ENG: Directly passed to aiohttp, also taken into account in the browser. Primarily needed for using the system `HTTPS_PROXY`.
|
110
|
+
# But the system proxy is applied only if you did not specify otherwise directly in `API.proxy`.
|
111
|
+
API.trust_env = True
|
112
|
+
|
105
113
|
# RUS: Выводит список последних промо-акций/новостей (можно поставить ограничитель по количеству, опционально)
|
106
114
|
# ENG: Outputs a list of the latest promotions/news (you can set a limit on the number, optionally)
|
107
115
|
news = await API.get_news(limit=5)
|
@@ -121,13 +129,17 @@ async def main():
|
|
121
129
|
with open(image.name, 'wb') as f:
|
122
130
|
f.write(image.getbuffer())
|
123
131
|
|
132
|
+
# RUS: Можно указать свой таймаут (браузер может его интерпретировать как x2 т.к. там 2 итерации скачивания)
|
133
|
+
# ENG: You can specify your own timeout (the browser may interpret it as x2 since there are 2 iterations of downloading)
|
134
|
+
API.timeout = 7
|
135
|
+
|
124
136
|
# RUS: Так же как и debug, в рантайме можно переназначить прокси
|
125
137
|
# ENG: As with debug, you can reassign the proxy in runtime
|
126
138
|
API.proxy = "user:password@host:port"
|
127
|
-
# RUS:
|
128
|
-
# ENG:
|
129
|
-
await API.rebuild_connection()
|
130
|
-
await API.
|
139
|
+
# RUS: Изменения происходят сразу же, кроме product_info, т.к. за него отвечает браузер
|
140
|
+
# ENG: Changes take effect immediately, except for product_info, as it is handled by the browser
|
141
|
+
await API.rebuild_connection(session=False, browser=True)
|
142
|
+
await API.product_info(43347)
|
131
143
|
|
132
144
|
|
133
145
|
if __name__ == '__main__':
|
@@ -0,0 +1,15 @@
|
|
1
|
+
pyaterochka_api/__init__.py,sha256=6XnalFPUumYqDJFyXw2puejJ612o-D1tYjZ_IjQ7Hx0,108
|
2
|
+
pyaterochka_api/api.py,sha256=KEr28n1aH69cVo0ztHCgB4ANVVZ-CezLWYTiPpfAmoc,7793
|
3
|
+
pyaterochka_api/enums.py,sha256=JnX4JiHzXyRo4se8sCFx0LyqcKlXXED0VcA0xI7r_ZI,621
|
4
|
+
pyaterochka_api/manager.py,sha256=yQw0njGCsropysT-_siuyNxXNpX-VJKOcVD8b-aV48I,10765
|
5
|
+
pyaterochka_api/tools.py,sha256=xFOThNRClX4u0cMfmQ5fVQKLM2Fn-rBAekzo_yBvRnQ,4447
|
6
|
+
pyaterochka_api-0.1.8.dist-info/licenses/LICENSE,sha256=Ee_P5XQUYoJuffzRL24j4GWpqgoWphUOKswpB2f9HcQ,1071
|
7
|
+
tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
8
|
+
tests/api_tests.py,sha256=XgOFu8yUKYqeRZ6QuQE-ZjMuPreQUb9y8iR7bDw-DOE,2763
|
9
|
+
tests/tools_tests.py,sha256=UKWVHu-QkEzc7iLsm5wIhA17FLq3E7SxwkLHc5GEI2M,1040
|
10
|
+
tests/snapshots/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
11
|
+
tests/snapshots/snap_base_tests.py,sha256=ryxaXZI10UonxclonGuLatjS3ZNMzkUeyz8uB0U5PdQ,27356
|
12
|
+
pyaterochka_api-0.1.8.dist-info/METADATA,sha256=3rFmk1lprX-iELCmBvC4-wFZE-EIOLx7UlZJECjgr_8,9305
|
13
|
+
pyaterochka_api-0.1.8.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
14
|
+
pyaterochka_api-0.1.8.dist-info/top_level.txt,sha256=PXTSi8y2C5_Mz20pJJFqOUBnjAIAXP_cc38mthvJ2x4,22
|
15
|
+
pyaterochka_api-0.1.8.dist-info/RECORD,,
|
@@ -14,7 +14,7 @@ def gen_schema(data):
|
|
14
14
|
|
15
15
|
@pytest.mark.asyncio
|
16
16
|
async def test_list(snapshot: SnapshotTest):
|
17
|
-
async with Pyaterochka() as API:
|
17
|
+
async with Pyaterochka(debug=True, trust_env=True) as API:
|
18
18
|
categories = await API.categories_list(subcategories=True)
|
19
19
|
snapshot.assert_match(gen_schema(categories), "categories_list")
|
20
20
|
|
@@ -23,25 +23,25 @@ async def test_list(snapshot: SnapshotTest):
|
|
23
23
|
|
24
24
|
@pytest.mark.asyncio
|
25
25
|
async def test_product_info(snapshot: SnapshotTest):
|
26
|
-
async with Pyaterochka() as API:
|
26
|
+
async with Pyaterochka(trust_env=True) as API:
|
27
27
|
result = await API.product_info(43347)
|
28
28
|
snapshot.assert_match(gen_schema(result), "product_info")
|
29
29
|
|
30
30
|
@pytest.mark.asyncio
|
31
31
|
async def test_get_news(snapshot: SnapshotTest):
|
32
|
-
async with Pyaterochka() as API:
|
32
|
+
async with Pyaterochka(debug=True, trust_env=True) as API:
|
33
33
|
result = await API.get_news(limit=5)
|
34
34
|
snapshot.assert_match(gen_schema(result), "get_news")
|
35
35
|
|
36
36
|
@pytest.mark.asyncio
|
37
37
|
async def test_find_store(snapshot: SnapshotTest):
|
38
|
-
async with Pyaterochka() as API:
|
38
|
+
async with Pyaterochka(debug=True, trust_env=True) as API:
|
39
39
|
categories = await API.find_store(longitude=37.63156, latitude=55.73768)
|
40
40
|
snapshot.assert_match(gen_schema(categories), "store_info")
|
41
41
|
|
42
42
|
@pytest.mark.asyncio
|
43
43
|
async def test_download_image(snapshot: SnapshotTest):
|
44
|
-
async with Pyaterochka() as API:
|
44
|
+
async with Pyaterochka(debug=True, trust_env=True) as API:
|
45
45
|
result = await API.download_image("https://photos.okolo.app/product/1392827-main/800x800.jpeg")
|
46
46
|
assert isinstance(result, BytesIO)
|
47
47
|
assert result.getvalue()
|
@@ -55,12 +55,12 @@ async def test_set_debug(snapshot: SnapshotTest):
|
|
55
55
|
|
56
56
|
@pytest.mark.asyncio
|
57
57
|
async def test_rebuild_connection(snapshot: SnapshotTest):
|
58
|
-
async with Pyaterochka() as API:
|
58
|
+
async with Pyaterochka(debug=True, trust_env=True) as API:
|
59
59
|
await API.rebuild_connection()
|
60
60
|
snapshot.assert_match("connection has been rebuilt", "rebuild_connection")
|
61
61
|
|
62
|
-
|
63
|
-
async def test_get_config(snapshot: SnapshotTest):
|
64
|
-
async with Pyaterochka() as API:
|
65
|
-
result = await API.get_config()
|
66
|
-
snapshot.assert_match(gen_schema(result), "get_config")
|
62
|
+
#@pytest.mark.asyncio
|
63
|
+
#async def test_get_config(snapshot: SnapshotTest):
|
64
|
+
# async with Pyaterochka(debug=True, trust_env=True, timeout=30) as API:
|
65
|
+
# result = await API.get_config()
|
66
|
+
# snapshot.assert_match(gen_schema(result), "get_config")
|
@@ -667,6 +667,7 @@ snapshots['test_list products_list'] = {
|
|
667
667
|
'str'
|
668
668
|
]
|
669
669
|
},
|
670
|
+
'initial_weight_step': 'str',
|
670
671
|
'labels': [
|
671
672
|
{
|
672
673
|
'bg_color': 'str',
|
@@ -674,6 +675,7 @@ snapshots['test_list products_list'] = {
|
|
674
675
|
'text_color': 'str'
|
675
676
|
}
|
676
677
|
],
|
678
|
+
'min_weight': 'str',
|
677
679
|
'name': 'str',
|
678
680
|
'plu': 'int',
|
679
681
|
'prices': {
|
@@ -763,6 +765,7 @@ snapshots['test_product_info product_info'] = {
|
|
763
765
|
},
|
764
766
|
'ingredients': 'str',
|
765
767
|
'ingredients_html': 'NoneType',
|
768
|
+
'initial_weight_step': 'str',
|
766
769
|
'is_available': 'bool',
|
767
770
|
'is_various_manufacturers': 'bool',
|
768
771
|
'labels': [
|
@@ -772,6 +775,7 @@ snapshots['test_product_info product_info'] = {
|
|
772
775
|
'text_color': 'str'
|
773
776
|
}
|
774
777
|
],
|
778
|
+
'min_weight': 'str',
|
775
779
|
'name': 'str',
|
776
780
|
'nutrients': [
|
777
781
|
{
|
tests/tools_tests.py
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
import pytest
|
2
|
+
from pyaterochka_api.tools import parse_proxy
|
3
|
+
import itertools
|
4
|
+
import logging
|
5
|
+
|
6
|
+
@pytest.mark.asyncio
|
7
|
+
async def test_parse_proxy():
|
8
|
+
# Варианты параметров
|
9
|
+
schemes = ['http://', 'https://', '']
|
10
|
+
auths = [('', ''), ('user', 'pass')]
|
11
|
+
hosts = ['127.0.0.1', 'example.com']
|
12
|
+
ports = ['', '8080']
|
13
|
+
|
14
|
+
logger = logging.getLogger("test_parse_proxy")
|
15
|
+
|
16
|
+
for scheme, (username, password), host, port in itertools.product(schemes, auths, hosts, ports):
|
17
|
+
# Формируем строку прокси
|
18
|
+
auth_part = f"{username}:{password}@" if username else ""
|
19
|
+
port_part = f":{port}" if port else ""
|
20
|
+
proxy_str = f"{scheme}{auth_part}{host}{port_part}"
|
21
|
+
|
22
|
+
expected = {'server': f"{scheme}{host}{port_part}"}
|
23
|
+
if not scheme:
|
24
|
+
expected['server'] = "http://"+expected['server']
|
25
|
+
if username:
|
26
|
+
expected['username'] = username
|
27
|
+
expected['password'] = password
|
28
|
+
|
29
|
+
assert parse_proxy(proxy_str, True, logger) == expected
|
30
|
+
|
@@ -1,12 +0,0 @@
|
|
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,,
|
File without changes
|
File without changes
|
File without changes
|