pyaterochka-api 0.1.9__py3-none-any.whl → 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  from .manager import Pyaterochka
2
2
  from .enums import PurchaseMode
3
3
 
4
+ __version__ = "0.2.0"
4
5
  __all__ = ['Pyaterochka', 'PurchaseMode']
pyaterochka_api/api.py CHANGED
@@ -2,7 +2,9 @@ import aiohttp
2
2
  from fake_useragent import UserAgent
3
3
  from camoufox import AsyncCamoufox
4
4
  import logging
5
- from .tools import parse_proxy, parse_js, get_env_proxy
5
+ from typing import Union, Optional
6
+ from beartype import beartype
7
+ from .tools import parse_proxy, get_env_proxy
6
8
 
7
9
 
8
10
  class PyaterochkaAPI:
@@ -10,16 +12,17 @@ class PyaterochkaAPI:
10
12
  Класс для загрузки JSON/image и парсинга JavaScript-конфигураций из удаленного источника.
11
13
  """
12
14
 
15
+ @beartype
13
16
  def __init__(self,
14
17
  debug: bool = False,
15
18
  proxy: str | None = None,
16
19
  autoclose_browser: bool = False,
17
20
  trust_env: bool = False,
18
21
  timeout: float = 10.0
19
- ):
22
+ ) -> None:
20
23
  self._debug = debug
21
24
  self._proxy = proxy
22
- self._session = None
25
+ self._session: Optional[aiohttp.ClientSession] = None
23
26
  self._autoclose_browser = autoclose_browser
24
27
  self._browser = None
25
28
  self._bcontext = None
@@ -33,7 +36,8 @@ class PyaterochkaAPI:
33
36
  if not self._logger.hasHandlers():
34
37
  self._logger.addHandler(handler)
35
38
 
36
- async def fetch(self, url: str) -> tuple[bool, dict | None | str, str]:
39
+ @beartype
40
+ async def fetch(self, url: str) -> tuple[bool, Union[dict, list[dict], bytes, str, None], str]:
37
41
  """
38
42
  Выполняет HTTP-запрос к указанному URL и возвращает результат.
39
43
 
@@ -63,26 +67,8 @@ class PyaterochkaAPI:
63
67
  self._logger.error(f'Unexpected error: {response.status}')
64
68
  raise Exception(f"Response status: {response.status} (unknown error/status code)")
65
69
 
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:
70
+ @beartype
71
+ async def browser_fetch(self, url: str, selector: str, state: str = 'attached') -> str:
86
72
  if self._browser is None or self._bcontext is None:
87
73
  await self.new_session(include_aiohttp=False, include_browser=True)
88
74
 
@@ -97,6 +83,7 @@ class PyaterochkaAPI:
97
83
  await self.close(include_aiohttp=False, include_browser=True)
98
84
  return content
99
85
 
86
+ @beartype
100
87
  async def new_session(self, include_aiohttp: bool = True, include_browser: bool = False) -> None:
101
88
  await self.close(include_aiohttp=include_aiohttp, include_browser=include_browser)
102
89
 
@@ -129,6 +116,7 @@ class PyaterochkaAPI:
129
116
  self._bcontext = await self._browser.new_context()
130
117
  self._logger.info(f"A new browser context has been opened.")
131
118
 
119
+ @beartype
132
120
  async def close(
133
121
  self,
134
122
  include_aiohttp: bool = True,
pyaterochka_api/enums.py CHANGED
@@ -1,11 +1,6 @@
1
1
  from enum import Enum
2
2
 
3
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
4
  # http(s)://user:pass@host:port
10
5
  PROXY = r'^(?:(?P<scheme>https?:\/\/))?(?:(?P<username>[^:@]+):(?P<password>[^@]+)@)?(?P<host>[^:\/]+)(?::(?P<port>\d+))?$'
11
6
 
@@ -1,5 +1,4 @@
1
1
  from .api import PyaterochkaAPI
2
- from enum import Enum
3
2
  import re
4
3
  import json
5
4
  from io import BytesIO
@@ -263,17 +262,3 @@ class Pyaterochka:
263
262
  image.name = f'{url.split("/")[-1]}.{response_type.split("/")[-1]}'
264
263
 
265
264
  return image
266
-
267
- @beartype
268
- async def get_config(self) -> dict:
269
- """
270
- Asynchronously retrieves the configuration from the hardcoded JavaScript file.
271
-
272
- Args:
273
- debug (bool, optional): Whether to print debug information. Defaults to False.
274
-
275
- Returns:
276
- dict: A dictionary representing the configuration if the request is successful, error otherwise.
277
- """
278
-
279
- return await self.api.download_config(config_url=self.HARDCODE_JS_CONFIG)
pyaterochka_api/tools.py CHANGED
@@ -1,8 +1,10 @@
1
1
  from .enums import Patterns
2
2
  import os
3
3
  import re
4
- from tqdm import tqdm
4
+ from beartype import beartype
5
+ import logging
5
6
 
7
+ @beartype
6
8
  def get_env_proxy() -> str | None:
7
9
  """
8
10
  Получает прокси из переменных окружения.
@@ -11,7 +13,8 @@ def get_env_proxy() -> str | None:
11
13
  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
14
  return proxy if proxy else None
13
15
 
14
- def parse_proxy(proxy_str: str | None, trust_env: bool, logger) -> dict | None:
16
+ @beartype
17
+ def parse_proxy(proxy_str: str | None, trust_env: bool, logger: logging.Logger) -> dict | None:
15
18
  logger.debug(f"Parsing proxy string: {proxy_str}")
16
19
 
17
20
  if not proxy_str:
@@ -53,69 +56,3 @@ def parse_proxy(proxy_str: str | None, trust_env: bool, logger) -> dict | None:
53
56
 
54
57
  logger.info(f"Proxy parsed as regex")
55
58
  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,15 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyaterochka_api
3
- Version: 0.1.9
3
+ Version: 0.2.0
4
4
  Summary: A Python API client for Pyaterochka store catalog
5
- Home-page: https://github.com/Open-Inflation/pyaterochka_api
6
5
  Author: Miskler
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Open-Inflation/pyaterochka_api
8
+ Project-URL: Repository, https://github.com/Open-Inflation/pyaterochka_api
9
+ Project-URL: Documentation, https://open-inflation.github.io/pyaterochka_api/
10
+ Keywords: api,pyaterochka,store,catalog
7
11
  Classifier: Programming Language :: Python :: 3
8
12
  Classifier: Programming Language :: Python :: 3.10
9
13
  Classifier: Programming Language :: Python :: 3.11
10
14
  Classifier: Programming Language :: Python :: 3.12
11
15
  Classifier: Programming Language :: Python :: 3.13
12
- Classifier: License :: OSI Approved :: MIT License
13
16
  Classifier: Operating System :: Microsoft :: Windows
14
17
  Classifier: Operating System :: POSIX :: Linux
15
18
  Classifier: Intended Audience :: Developers
@@ -24,21 +27,11 @@ Requires-Dist: aiohttp
24
27
  Requires-Dist: camoufox[geoip]
25
28
  Requires-Dist: beartype
26
29
  Requires-Dist: fake-useragent
27
- Requires-Dist: tqdm
28
30
  Provides-Extra: tests
29
31
  Requires-Dist: pytest; extra == "tests"
30
32
  Requires-Dist: pytest-asyncio; extra == "tests"
31
33
  Requires-Dist: pytest-typed-schema-shot; extra == "tests"
32
- Dynamic: author
33
- Dynamic: classifier
34
- Dynamic: description
35
- Dynamic: description-content-type
36
- Dynamic: home-page
37
34
  Dynamic: license-file
38
- Dynamic: provides-extra
39
- Dynamic: requires-dist
40
- Dynamic: requires-python
41
- Dynamic: summary
42
35
 
43
36
  # Pyaterochka API *(not official / не официальный)*
44
37
 
@@ -116,10 +109,6 @@ async def main():
116
109
  news = await API.get_news(limit=5)
117
110
  print(f"News output: {news!s:.100s}...\n")
118
111
 
119
- # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
120
- # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
121
- print(f"Main config: {await API.get_config()!s:.100s}...\n")
122
-
123
112
  # RUS: Если требуется, можно настроить вывод логов в консоль
124
113
  # ENG: If required, you can configure the output of logs in the console
125
114
  API.debug = True
@@ -0,0 +1,10 @@
1
+ pyaterochka_api/__init__.py,sha256=-rsfMGZCT77fmjEHQi4cyyErAZrsQQdbuaqlbOt-a08,130
2
+ pyaterochka_api/api.py,sha256=EsMsyNaFEWCif3iRK6GJCT1o4QHDY72rBgjfeu1BTSU,7204
3
+ pyaterochka_api/enums.py,sha256=H864-J-1VWiEsRxNaGLAkcH2kq92FEj7fhuxwqfRVaw,285
4
+ pyaterochka_api/manager.py,sha256=Wuugv3VuYOInyCBMfGyCCEn6lf4zdr3LwwZk1iT4R44,10261
5
+ pyaterochka_api/tools.py,sha256=fxBa-FKJfbKmup2ZrDTF1u3qyawiHODjG9ofq5ZCG0w,2178
6
+ pyaterochka_api-0.2.0.dist-info/licenses/LICENSE,sha256=Ee_P5XQUYoJuffzRL24j4GWpqgoWphUOKswpB2f9HcQ,1071
7
+ pyaterochka_api-0.2.0.dist-info/METADATA,sha256=7-bQZJW9MFgReD8e2hX1x0p3nVN2YcGixP7PrrpZUQc,9432
8
+ pyaterochka_api-0.2.0.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
9
+ pyaterochka_api-0.2.0.dist-info/top_level.txt,sha256=NTJa4yZBzfmC9B5FQuFETD9ngGd_KYZ3Hjfw_33aDTE,16
10
+ pyaterochka_api-0.2.0.dist-info/RECORD,,
@@ -1,13 +0,0 @@
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.9.dist-info/licenses/LICENSE,sha256=Ee_P5XQUYoJuffzRL24j4GWpqgoWphUOKswpB2f9HcQ,1071
7
- tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- tests/api_tests.py,sha256=Sx2tAVGLbubpzJOn166ECJj64oYUv1S6c5fYfGLGvu0,2227
9
- tests/tools_tests.py,sha256=UKWVHu-QkEzc7iLsm5wIhA17FLq3E7SxwkLHc5GEI2M,1040
10
- pyaterochka_api-0.1.9.dist-info/METADATA,sha256=nUC4TfS_mT1VQ2MZLKNpLM4YF1ZfvaZ33bqOHhimHZk,9855
11
- pyaterochka_api-0.1.9.dist-info/WHEEL,sha256=zaaOINJESkSfm_4HQVc5ssNzHCPXhJm0kEUakpsEHaU,91
12
- pyaterochka_api-0.1.9.dist-info/top_level.txt,sha256=PXTSi8y2C5_Mz20pJJFqOUBnjAIAXP_cc38mthvJ2x4,22
13
- pyaterochka_api-0.1.9.dist-info/RECORD,,
tests/__init__.py DELETED
File without changes
tests/api_tests.py DELETED
@@ -1,57 +0,0 @@
1
- import pytest
2
- from pyaterochka_api import Pyaterochka
3
- from io import BytesIO
4
- from typed_schema_shot import SchemaShot
5
-
6
-
7
- @pytest.mark.asyncio
8
- async def test_list(schemashot: SchemaShot):
9
- async with Pyaterochka(debug=True, trust_env=True) as API:
10
- categories = await API.categories_list(subcategories=True)
11
- schemashot.assert_match(categories, "categories_list")
12
-
13
- result = await API.products_list(category_id=categories[0]['id'], limit=5)
14
- schemashot.assert_match(result, "products_list")
15
-
16
- @pytest.mark.asyncio
17
- async def test_product_info(schemashot: SchemaShot):
18
- async with Pyaterochka(trust_env=True) as API:
19
- result = await API.product_info(43347)
20
- schemashot.assert_match(result, "product_info")
21
-
22
- @pytest.mark.asyncio
23
- async def test_get_news(schemashot: SchemaShot):
24
- async with Pyaterochka(debug=True, trust_env=True) as API:
25
- result = await API.get_news(limit=5)
26
- schemashot.assert_match(result, "get_news")
27
-
28
- @pytest.mark.asyncio
29
- async def test_find_store(schemashot: SchemaShot):
30
- async with Pyaterochka(debug=True, trust_env=True) as API:
31
- categories = await API.find_store(longitude=37.63156, latitude=55.73768)
32
- schemashot.assert_match(categories, "store_info")
33
-
34
- @pytest.mark.asyncio
35
- async def test_download_image(schemashot: SchemaShot):
36
- async with Pyaterochka(debug=True, trust_env=True) as API:
37
- result = await API.download_image("https://photos.okolo.app/product/1392827-main/800x800.jpeg")
38
- assert isinstance(result, BytesIO)
39
- assert result.getvalue()
40
-
41
- @pytest.mark.asyncio
42
- async def test_set_debug(schemashot: SchemaShot):
43
- async with Pyaterochka(debug=True) as API:
44
- assert API.debug == True
45
- API.debug = False
46
- assert API.debug == False
47
-
48
- @pytest.mark.asyncio
49
- async def test_rebuild_connection(schemashot: SchemaShot):
50
- async with Pyaterochka(debug=True, trust_env=True) as API:
51
- await API.rebuild_connection()
52
-
53
- #@pytest.mark.asyncio
54
- #async def test_get_config(snapshot: SnapshotTest):
55
- # async with Pyaterochka(debug=True, trust_env=True, timeout=30) as API:
56
- # result = await API.get_config()
57
- # snapshot.assert_match(gen_schema(result), "get_config")
tests/tools_tests.py DELETED
@@ -1,30 +0,0 @@
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
-