pyaterochka-api 0.1.2__tar.gz → 0.1.3__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyaterochka_api
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -32,13 +32,22 @@ async def main():
32
32
  # RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
33
33
  # ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
34
34
  # Страниц не сущетвует, использовать желаемый лимит (до 499) / Pages do not exist, use the desired limit (up to 499)
35
- print(f"Items list output: {await pyaterochka_api.products_list(catalog[0]['id'], limit=5)!s:.100s}...\n")
35
+ items = await pyaterochka_api.products_list(catalog[0]['id'], limit=5)
36
+ print(f"Items list output: {items!s:.100s}...\n")
36
37
 
37
38
  # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
38
39
  # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
40
+ print(f"Main config: {await pyaterochka_api.get_config()!s:.100s}...\n")
41
+
39
42
  # RUS: Если требуется, можно настроить вывод логов в консоль
40
43
  # ENG: If required, you can configure the output of logs in the console
41
- print(f"Main config: {await pyaterochka_api.get_config(debug=True)!s:.100s}...\n")
44
+ pyaterochka_api.set_debug(True)
45
+
46
+ # RUS: Скачивает картинку товара (возвращает BytesIO или None)
47
+ # ENG: Downloads the product image (returns BytesIO or None)
48
+ image = await pyaterochka_api.download_image(url=items['products'][0]['image_links']['normal'][0])
49
+ with open(image.name, 'wb') as f:
50
+ f.write(image.getbuffer())
42
51
 
43
52
 
44
53
  if __name__ == '__main__':
@@ -17,13 +17,22 @@ async def main():
17
17
  # RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
18
18
  # ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
19
19
  # Страниц не сущетвует, использовать желаемый лимит (до 499) / Pages do not exist, use the desired limit (up to 499)
20
- print(f"Items list output: {await pyaterochka_api.products_list(catalog[0]['id'], limit=5)!s:.100s}...\n")
20
+ items = await pyaterochka_api.products_list(catalog[0]['id'], limit=5)
21
+ print(f"Items list output: {items!s:.100s}...\n")
21
22
 
22
23
  # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
23
24
  # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
25
+ print(f"Main config: {await pyaterochka_api.get_config()!s:.100s}...\n")
26
+
24
27
  # RUS: Если требуется, можно настроить вывод логов в консоль
25
28
  # ENG: If required, you can configure the output of logs in the console
26
- print(f"Main config: {await pyaterochka_api.get_config(debug=True)!s:.100s}...\n")
29
+ pyaterochka_api.set_debug(True)
30
+
31
+ # RUS: Скачивает картинку товара (возвращает BytesIO или None)
32
+ # ENG: Downloads the product image (returns BytesIO or None)
33
+ image = await pyaterochka_api.download_image(url=items['products'][0]['image_links']['normal'][0])
34
+ with open(image.name, 'wb') as f:
35
+ f.write(image.getbuffer())
27
36
 
28
37
 
29
38
  if __name__ == '__main__':
@@ -1,3 +1,3 @@
1
- from .manager import PurchaseMode, categories_list, products_list, get_config
1
+ from .manager import PurchaseMode, categories_list, products_list, get_config, download_image, PyaterochkaAPI, set_debug
2
2
 
3
- __all__ = ['PurchaseMode', 'categories_list', 'products_list', 'get_config']
3
+ __all__ = ['PurchaseMode', 'categories_list', 'products_list', 'get_config', 'download_image', 'PyaterochkaAPI', 'set_debug']
@@ -0,0 +1,146 @@
1
+ import aiohttp
2
+ from fake_useragent import UserAgent
3
+ from enum import Enum
4
+ import re
5
+ from tqdm.asyncio import tqdm
6
+
7
+
8
+ class PyaterochkaAPI:
9
+ """
10
+ Класс для загрузки JSON/image и парсинга JavaScript-конфигураций из удаленного источника.
11
+ """
12
+
13
+ class Patterns(Enum):
14
+ JS = r'\s*let\s+n\s*=\s*({.*});\s*' # let n = {...};
15
+ STR = r'(\w+)\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"' # key: "value"
16
+ DICT = r'(\w+)\s*:\s*{(.*?)}' # key: {...}
17
+ LIST = r'(\w+)\s*:\s*\[([^\[\]]*(?:\[.*?\])*)\]' # key: [value]
18
+ FIND = r'\{.*?\}|\[.*?\]' # {} or []
19
+
20
+ def __init__(self, debug: bool = False):
21
+ self._debug = False
22
+
23
+ def set_debug(self, debug: bool):
24
+ """Устанавливает режим дебага для экземпляра класса."""
25
+ self._debug = debug
26
+
27
+ async def fetch(self, url: str) -> tuple[bool, dict | None | str, str]:
28
+ """
29
+ Выполняет HTTP-запрос к указанному URL и возвращает результат.
30
+
31
+ :param url: URL для запроса.
32
+ :param is_json: Ожидать ли JSON в ответе.
33
+ :return: Кортеж (успех, данные или None).
34
+ """
35
+ async with aiohttp.ClientSession() as session:
36
+
37
+ if self._debug:
38
+ print(f"Requesting \"{url}\"...", flush=True)
39
+
40
+ async with session.get(
41
+ url=url,
42
+ headers={"User-Agent": UserAgent().random},
43
+ ) as response:
44
+ if self._debug:
45
+ print(f"Response status: {response.status}", flush=True)
46
+
47
+ if response.status == 200:
48
+ if response.headers['content-type'] == 'application/json':
49
+ output_response = response.json()
50
+ elif response.headers['content-type'] == 'image/jpeg':
51
+ output_response = response.read()
52
+ else:
53
+ output_response = response.text()
54
+
55
+ return True, await output_response, response.headers['content-type']
56
+ elif response.status == 403:
57
+ if self._debug:
58
+ print("Anti-bot protection. Use Russia IP address and try again.", flush=True)
59
+ return False, None, ''
60
+ else:
61
+ if self._debug:
62
+ print(f"Unexpected error: {response.status}", flush=True)
63
+ raise Exception(f"Response status: {response.status} (unknown error/status code)")
64
+
65
+ async def _parse_js(self, js_code: str) -> dict | None:
66
+ """
67
+ Парсит JavaScript-код и извлекает данные из переменной "n".
68
+
69
+ :param js_code: JS-код в виде строки.
70
+ :return: Распарсенные данные в виде словаря или None.
71
+ """
72
+ matches = re.finditer(self.Patterns.JS.value, js_code)
73
+ match_list = list(matches)
74
+
75
+ if self._debug:
76
+ print(f"Found matches {len(match_list)}")
77
+ progress_bar = tqdm(total=33, desc="Parsing JS", position=0)
78
+
79
+ async def parse_match(match: str) -> dict:
80
+ result = {}
81
+
82
+ if self._debug:
83
+ progress_bar.set_description("Parsing strings")
84
+
85
+ # Парсинг строк
86
+ string_matches = re.finditer(self.Patterns.STR.value, match)
87
+ for m in string_matches:
88
+ key, value = m.group(1), m.group(2)
89
+ result[key] = value.replace('\"', '"').replace('\\', '\\')
90
+
91
+ if self._debug:
92
+ progress_bar.update(1)
93
+ progress_bar.set_description("Parsing dictionaries")
94
+
95
+ # Парсинг словарей
96
+ dict_matches = re.finditer(self.Patterns.DICT.value, match)
97
+ for m in dict_matches:
98
+ key, value = m.group(1), m.group(2)
99
+ if not re.search(self.Patterns.STR.value, value):
100
+ result[key] = await parse_match(value)
101
+
102
+ if self._debug:
103
+ progress_bar.update(1)
104
+ progress_bar.set_description("Parsing lists")
105
+
106
+ # Парсинг списков
107
+ list_matches = re.finditer(self.Patterns.LIST.value, match)
108
+ for m in list_matches:
109
+ key, value = m.group(1), m.group(2)
110
+ if not re.search(self.Patterns.STR.value, value):
111
+ result[key] = [await parse_match(item.group(0)) for item in re.finditer(self.Patterns.FIND.value, value)]
112
+
113
+ if self._debug:
114
+ progress_bar.update(1)
115
+
116
+ return result
117
+
118
+ if match_list and len(match_list) >= 1:
119
+ if self._debug:
120
+ print("Starting to parse match")
121
+ result = await parse_match(match_list[1].group(0))
122
+ if self._debug:
123
+ progress_bar.close()
124
+ return result
125
+ else:
126
+ if self._debug:
127
+ progress_bar.close()
128
+ raise Exception("N variable in JS code not found")
129
+
130
+ async def download_config(self, config_url: str) -> dict | None:
131
+ """
132
+ Загружает и парсит JavaScript-конфигурацию с указанного URL.
133
+
134
+ :param config_url: URL для загрузки конфигурации.
135
+ :return: Распарсенные данные в виде словаря или None.
136
+ """
137
+ is_success, js_code, _response_type = await self.fetch(url=config_url)
138
+
139
+ if not is_success:
140
+ if self._debug:
141
+ print("Failed to fetch JS code")
142
+ return None
143
+ elif self._debug:
144
+ print("JS code fetched successfully")
145
+
146
+ return await self._parse_js(js_code=js_code)
@@ -1,5 +1,7 @@
1
- from .api import main_fetch, download_hardcode_config
1
+ from .api import PyaterochkaAPI as ClassPyaterochkaAPI
2
2
  from enum import Enum
3
+ from io import BytesIO
4
+
3
5
 
4
6
  CATALOG_URL = "https://5d.5ka.ru/api/catalog/v2/stores"
5
7
  HARDCODE_JS_CONFIG = "https://prod-cdn.5ka.ru/scripts/main.a0c039ea81eb8cf69492.js" # TODO сделать не хардкодным имя файла
@@ -9,11 +11,13 @@ class PurchaseMode(Enum):
9
11
  DELIVERY = "delivery"
10
12
 
11
13
 
14
+ PyaterochkaAPI = ClassPyaterochkaAPI(debug=False)
15
+
16
+
12
17
  async def categories_list(
13
18
  subcategories: bool = False,
14
19
  mode: PurchaseMode = PurchaseMode.STORE,
15
- sap_code_store_id: str = "Y232",
16
- debug: bool = False
20
+ sap_code_store_id: str = "Y232"
17
21
  ) -> dict | None:
18
22
  """
19
23
  Asynchronously retrieves a list of categories from the Pyaterochka API.
@@ -32,16 +36,14 @@ async def categories_list(
32
36
  """
33
37
 
34
38
  request_url = f"{CATALOG_URL}/{sap_code_store_id}/categories?mode={mode.value}&include_subcategories={1 if subcategories else 0}"
35
- is_success, response = await main_fetch(url=request_url, debug=debug)
39
+ _is_success, response, _response_type = await PyaterochkaAPI.fetch(url=request_url)
36
40
  return response
37
41
 
38
-
39
42
  async def products_list(
40
43
  category_id: int,
41
44
  mode: PurchaseMode = PurchaseMode.STORE,
42
45
  sap_code_store_id: str = "Y232",
43
- limit: int = 30,
44
- debug: bool = False
46
+ limit: int = 30
45
47
  ) -> dict | None:
46
48
  """
47
49
  Asynchronously retrieves a list of products from the Pyaterochka API for a given category.
@@ -64,11 +66,25 @@ async def products_list(
64
66
  raise ValueError("Limit must be between 1 and 499")
65
67
 
66
68
  request_url = f"{CATALOG_URL}/{sap_code_store_id}/categories/{category_id}/products?mode={mode.value}&limit={limit}"
67
- is_success, response = await main_fetch(url=request_url, debug=debug)
69
+ _is_success, response, _response_type = await PyaterochkaAPI.fetch(url=request_url)
68
70
  return response
69
71
 
72
+ async def download_image(url: str) -> BytesIO | None:
73
+ is_success, image_data, response_type = await PyaterochkaAPI.fetch(url=url)
74
+
75
+ if not is_success:
76
+ if PyaterochkaAPI._debug:
77
+ print("Failed to fetch image")
78
+ return None
79
+ elif PyaterochkaAPI._debug:
80
+ print("Image fetched successfully")
70
81
 
71
- async def get_config(debug: bool = False) -> list | None:
82
+ image = BytesIO(image_data)
83
+ image.name = f'{url.split("/")[-1]}.{response_type.split("/")[-1]}'
84
+
85
+ return image
86
+
87
+ async def get_config() -> list | None:
72
88
  """
73
89
  Asynchronously retrieves the configuration from the hardcoded JavaScript file.
74
90
 
@@ -79,4 +95,9 @@ async def get_config(debug: bool = False) -> list | None:
79
95
  list | None: A list representing the configuration if the request is successful, None otherwise.
80
96
  """
81
97
 
82
- return await download_hardcode_config(config_url=HARDCODE_JS_CONFIG, debug=debug)
98
+ return await PyaterochkaAPI.download_config(config_url=HARDCODE_JS_CONFIG)
99
+
100
+
101
+ def set_debug(debug: bool) -> None:
102
+ PyaterochkaAPI.set_debug(debug=debug)
103
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pyaterochka-api
3
- Version: 0.1.2
3
+ Version: 0.1.3
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
@@ -32,13 +32,22 @@ async def main():
32
32
  # RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
33
33
  # ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
34
34
  # Страниц не сущетвует, использовать желаемый лимит (до 499) / Pages do not exist, use the desired limit (up to 499)
35
- print(f"Items list output: {await pyaterochka_api.products_list(catalog[0]['id'], limit=5)!s:.100s}...\n")
35
+ items = await pyaterochka_api.products_list(catalog[0]['id'], limit=5)
36
+ print(f"Items list output: {items!s:.100s}...\n")
36
37
 
37
38
  # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
38
39
  # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
40
+ print(f"Main config: {await pyaterochka_api.get_config()!s:.100s}...\n")
41
+
39
42
  # RUS: Если требуется, можно настроить вывод логов в консоль
40
43
  # ENG: If required, you can configure the output of logs in the console
41
- print(f"Main config: {await pyaterochka_api.get_config(debug=True)!s:.100s}...\n")
44
+ pyaterochka_api.set_debug(True)
45
+
46
+ # RUS: Скачивает картинку товара (возвращает BytesIO или None)
47
+ # ENG: Downloads the product image (returns BytesIO or None)
48
+ image = await pyaterochka_api.download_image(url=items['products'][0]['image_links']['normal'][0])
49
+ with open(image.name, 'wb') as f:
50
+ f.write(image.getbuffer())
42
51
 
43
52
 
44
53
  if __name__ == '__main__':
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='pyaterochka_api',
5
- version='0.1.2',
5
+ version='0.1.3',
6
6
  packages=find_packages(),
7
7
  install_requires=[
8
8
  'aiohttp',
@@ -1,116 +0,0 @@
1
- import aiohttp
2
- from fake_useragent import UserAgent
3
- from enum import Enum
4
- import re
5
- from tqdm.asyncio import tqdm
6
-
7
-
8
- class Patterns(Enum):
9
- JS = r'\s*let\s+n\s*=\s*({.*});\s*'
10
- STR = r'(\w+)\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"'
11
- DICT = r'(\w+)\s*:\s*{(.*?)}'
12
- LIST = r'(\w+)\s*:\s*\[([^\[\]]*(?:\[.*?\])*)\]'
13
- FIND = r'\{.*?\}|\[.*?\]'
14
-
15
-
16
-
17
- async def main_fetch(url: str, debug: bool = False, is_json: bool = True) -> tuple[bool, dict | None]:
18
- async with aiohttp.ClientSession() as session:
19
- if debug:
20
- print(f"Requesting \"{url}\"...", flush=True)
21
-
22
- async with session.get(
23
- url=url,
24
- headers={"User-Agent": UserAgent().random},
25
- ) as response:
26
- if debug:
27
- print(f"Response status: {response.status}", flush=True)
28
-
29
- if response.status == 200: # 200 OK
30
- if debug:
31
- print("Correct response", flush=True)
32
- return True, await response.json() if is_json else await response.text()
33
- elif response.status == 403: # 403 Forbidden (сервер воспринял как бота)
34
- if debug:
35
- print("Anti-bot protection. Use Russia IP address and try again.", flush=True)
36
- return False, None
37
- else:
38
- if debug:
39
- print(f"Please, create issue on GitHub", flush=True)
40
- raise Exception(f"Response status: {response.status} (unknown error/status code)")
41
-
42
- async def download_hardcode_config(config_url: str, debug: bool = False) -> dict | None:
43
- is_success, js_code = await main_fetch(url=config_url, debug=debug, is_json=False)
44
-
45
- if not is_success:
46
- if debug:
47
- print("Failed to fetch JS code")
48
- return None
49
- elif debug:
50
- print("JS code fetched successfully")
51
-
52
- # Регулярное выражение для извлечения определения переменной n
53
- matches = re.finditer(Patterns.JS.value, js_code)
54
-
55
- match_list = list(matches)
56
- if debug:
57
- print(f"Found matches {len(match_list)}")
58
-
59
- if debug:
60
- progress_bar = tqdm(total=33, desc="Parsing JS", position=0) # примерно 33 операции
61
-
62
- async def parse_match(match: str) -> dict:
63
- result = {}
64
-
65
- if debug:
66
- # Обновление описания прогресса
67
- progress_bar.set_description("Parsing strings")
68
-
69
- # Парсинг строк
70
- string_matches = re.finditer(Patterns.STR.value, match)
71
- for m in string_matches:
72
- key, value = m.group(1), m.group(2)
73
- result[key] = value.replace('\"', '"').replace('\\\\', '\\')
74
-
75
- if debug:
76
- progress_bar.update(1)
77
- # Обновление описания прогресса
78
- progress_bar.set_description("Parsing dictionaries")
79
-
80
- # Парсинг словарей
81
- dict_matches = re.finditer(Patterns.DICT.value, match)
82
- for m in dict_matches:
83
- key, value = m.group(1), m.group(2)
84
- if not re.search(Patterns.STR.value, value):
85
- result[key] = await parse_match(value)
86
-
87
- if debug:
88
- progress_bar.update(1)
89
- # Обновление описания прогресса
90
- progress_bar.set_description("Parsing lists")
91
-
92
- # Парсинг списков
93
- list_matches = re.finditer(Patterns.LIST.value, match)
94
- for m in list_matches:
95
- key, value = m.group(1), m.group(2)
96
- if not re.search(Patterns.STR.value, value):
97
- result[key] = [await parse_match(item.group(0)) for item in re.finditer(Patterns.FIND.value, value)]
98
-
99
- if debug:
100
- # Обновление прогресса
101
- progress_bar.update(1)
102
-
103
- return result
104
-
105
- if match_list and len(match_list) >= 1: # нужная переменная идет второй из трех
106
- if debug:
107
- print("Starting to parse match")
108
- result = await parse_match(match_list[1].group(0))
109
- if debug:
110
- progress_bar.close()
111
- return result
112
- else:
113
- if debug:
114
- progress_bar.close()
115
- raise Exception("N variable in JS code not found")
116
-
File without changes