pyaterochka-api 0.1.3__tar.gz → 0.1.7__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 (26) hide show
  1. pyaterochka_api-0.1.7/PKG-INFO +153 -0
  2. pyaterochka_api-0.1.7/README.md +111 -0
  3. pyaterochka_api-0.1.7/pyaterochka_api/__init__.py +3 -0
  4. pyaterochka_api-0.1.7/pyaterochka_api/api.py +279 -0
  5. pyaterochka_api-0.1.7/pyaterochka_api/manager.py +277 -0
  6. pyaterochka_api-0.1.7/pyaterochka_api.egg-info/PKG-INFO +153 -0
  7. {pyaterochka_api-0.1.3 → pyaterochka_api-0.1.7}/pyaterochka_api.egg-info/SOURCES.txt +6 -1
  8. pyaterochka_api-0.1.7/pyaterochka_api.egg-info/requires.txt +10 -0
  9. {pyaterochka_api-0.1.3 → pyaterochka_api-0.1.7}/pyaterochka_api.egg-info/top_level.txt +1 -0
  10. pyaterochka_api-0.1.7/pyproject.toml +7 -0
  11. pyaterochka_api-0.1.7/setup.py +42 -0
  12. pyaterochka_api-0.1.7/tests/__init__.py +0 -0
  13. pyaterochka_api-0.1.7/tests/base_tests.py +66 -0
  14. pyaterochka_api-0.1.7/tests/snapshots/__init__.py +0 -0
  15. pyaterochka_api-0.1.7/tests/snapshots/snap_base_tests.py +813 -0
  16. pyaterochka_api-0.1.3/PKG-INFO +0 -63
  17. pyaterochka_api-0.1.3/README.md +0 -46
  18. pyaterochka_api-0.1.3/pyaterochka_api/__init__.py +0 -3
  19. pyaterochka_api-0.1.3/pyaterochka_api/api.py +0 -146
  20. pyaterochka_api-0.1.3/pyaterochka_api/manager.py +0 -103
  21. pyaterochka_api-0.1.3/pyaterochka_api.egg-info/PKG-INFO +0 -63
  22. pyaterochka_api-0.1.3/pyaterochka_api.egg-info/requires.txt +0 -3
  23. pyaterochka_api-0.1.3/setup.py +0 -23
  24. {pyaterochka_api-0.1.3 → pyaterochka_api-0.1.7}/LICENSE +0 -0
  25. {pyaterochka_api-0.1.3 → pyaterochka_api-0.1.7}/pyaterochka_api.egg-info/dependency_links.txt +0 -0
  26. {pyaterochka_api-0.1.3 → pyaterochka_api-0.1.7}/setup.cfg +0 -0
@@ -0,0 +1,153 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyaterochka_api
3
+ Version: 0.1.7
4
+ Summary: A Python API client for Pyaterochka store catalog
5
+ Home-page: https://github.com/Open-Inflation/pyaterochka_api
6
+ Author: Miskler
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3.10
9
+ Classifier: Programming Language :: Python :: 3.11
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Operating System :: Microsoft :: Windows
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Intended Audience :: Information Technology
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Classifier: Topic :: Internet
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: aiohttp
24
+ Requires-Dist: camoufox[geoip]
25
+ Requires-Dist: beartype
26
+ Requires-Dist: fake-useragent
27
+ Requires-Dist: tqdm
28
+ Provides-Extra: tests
29
+ Requires-Dist: pytest; extra == "tests"
30
+ Requires-Dist: pytest-asyncio; extra == "tests"
31
+ Requires-Dist: snapshottest~=1.0.0a1; extra == "tests"
32
+ Dynamic: author
33
+ Dynamic: classifier
34
+ Dynamic: description
35
+ Dynamic: description-content-type
36
+ Dynamic: home-page
37
+ Dynamic: license-file
38
+ Dynamic: provides-extra
39
+ Dynamic: requires-dist
40
+ Dynamic: requires-python
41
+ Dynamic: summary
42
+
43
+ # Pyaterochka API *(not official / не официальный)*
44
+
45
+ Pyaterochka (Пятёрочка) - https://5ka.ru/
46
+
47
+ [![GitHub Actions](https://github.com/Open-Inflation/pyaterochka_api/workflows/API%20Tests%20Daily/badge.svg)](https://github.com/Open-Inflation/pyaterochka_api/actions?query=workflow%3A"API+Tests+Daily?query=branch%3Amain")
48
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyaterochka_api)
49
+ ![PyPI - Package Version](https://img.shields.io/pypi/v/pyaterochka_api?color=blue)
50
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyaterochka_api?label=PyPi%20downloads)](https://pypi.org/project/pyaterochka-api/)
51
+ [![Discord](https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da)](https://discord.gg/UnJnGHNbBp)
52
+ [![Telegram](https://img.shields.io/badge/Telegram-24A1DE)](https://t.me/miskler_dev)
53
+
54
+
55
+
56
+ ## Installation / Установка:
57
+ 1. Install package / Установка пакета:
58
+ ```bash
59
+ pip install pyaterochka_api
60
+ ```
61
+ 2. ***Debian/Ubuntu Linux***: Install dependencies / Установка зависимостей:
62
+ ```bash
63
+ sudo apt update && sudo apt install -y libgtk-3-0 libx11-xcb1
64
+ ```
65
+ 3. Install browser / Установка браузера:
66
+ ```bash
67
+ camoufox fetch
68
+ ```
69
+
70
+ ### Usage / Использование:
71
+ ```py
72
+ from pyaterochka_api import Pyaterochka
73
+ import asyncio
74
+
75
+
76
+ async def main():
77
+ async with Pyaterochka(proxy="user:password@host:port", debug=False, autoclose_browser=False, trust_env=False) as API:
78
+ # RUS: Вводим геоточку (самого магазина или рядом с ним) и получаем инфу о магазине
79
+ # ENG: Enter a geolocation (of the store or near it) and get info about the store
80
+ find_store = await API.find_store(longitude=37.63156, latitude=55.73768)
81
+ print(f"Store info output: {find_store!s:.100s}...\n")
82
+
83
+ # RUS: Выводит список всех категорий на сайте
84
+ # ENG: Outputs a list of all categories on the site
85
+ catalog = await API.categories_list(subcategories=True, mode=API.PurchaseMode.DELIVERY)
86
+ print(f"Categories list output: {catalog!s:.100s}...\n")
87
+
88
+ # RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
89
+ # ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
90
+ # Страниц не сущетвует, использовать желаемый лимит (до 499) / Pages do not exist, use the desired limit (up to 499)
91
+ items = await API.products_list(catalog[0]['id'], limit=5)
92
+ print(f"Items list output: {items!s:.100s}...\n")
93
+
94
+ # RUS: Выводит информацию о товаре (по его plu - id товара).
95
+ # Функция в первый раз достаточно долгая, порядка 5-9 секунды, последующие запросы около 2 секунд (если браузер не был закрыт)
96
+ # ENG: Outputs information about the product (by its plu - product id).
97
+ # The function is quite long the first time, about 5-9 seconds, subsequent requests take about 2 seconds (if the browser was not closed)
98
+ info = await API.product_info(43347)
99
+ print(f"Product output: {info["props"]["pageProps"]["props"]['productStore']!s:.100s}...\n")
100
+
101
+ # RUS: Влияет исключительно на функцию выше (product_info), если включено, то после отработки запроса браузер закроется и кеши очищаются.
102
+ # Не рекомендую включать, если вам все же нужно освободить память, лучше использовать API.close(session=False, browser=True)
103
+ # ENG: Affects only the function above (product_info), if enabled, the browser will close after the request is processed and caches are cleared.
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)
105
+ API.autoclose_browser = True
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
+
113
+ # RUS: Выводит список последних промо-акций/новостей (можно поставить ограничитель по количеству, опционально)
114
+ # ENG: Outputs a list of the latest promotions/news (you can set a limit on the number, optionally)
115
+ news = await API.get_news(limit=5)
116
+ print(f"News output: {news!s:.100s}...\n")
117
+
118
+ # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
119
+ # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
120
+ print(f"Main config: {await API.get_config()!s:.100s}...\n")
121
+
122
+ # RUS: Если требуется, можно настроить вывод логов в консоль
123
+ # ENG: If required, you can configure the output of logs in the console
124
+ API.debug = True
125
+
126
+ # RUS: Скачивает картинку товара (возвращает BytesIO или None)
127
+ # ENG: Downloads the product image (returns BytesIO or None)
128
+ image = await API.download_image(url=items['products'][0]['image_links']['normal'][0])
129
+ with open(image.name, 'wb') as f:
130
+ f.write(image.getbuffer())
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
+
136
+ # RUS: Так же как и debug, в рантайме можно переназначить прокси
137
+ # ENG: As with debug, you can reassign the proxy in runtime
138
+ API.proxy = "user:password@host:port"
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)
143
+
144
+
145
+ if __name__ == '__main__':
146
+ asyncio.run(main())
147
+ ```
148
+
149
+ ### Report / Обратная связь
150
+
151
+ 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)!
152
+
153
+ Если у вас возникнут проблемы в использовании / пожелания, не стесняйтесь писать на [GitHub проекта](https://github.com/Open-Inflation/pyaterochka_api/issues)!
@@ -0,0 +1,111 @@
1
+ # Pyaterochka API *(not official / не официальный)*
2
+
3
+ Pyaterochka (Пятёрочка) - https://5ka.ru/
4
+
5
+ [![GitHub Actions](https://github.com/Open-Inflation/pyaterochka_api/workflows/API%20Tests%20Daily/badge.svg)](https://github.com/Open-Inflation/pyaterochka_api/actions?query=workflow%3A"API+Tests+Daily?query=branch%3Amain")
6
+ ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pyaterochka_api)
7
+ ![PyPI - Package Version](https://img.shields.io/pypi/v/pyaterochka_api?color=blue)
8
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/pyaterochka_api?label=PyPi%20downloads)](https://pypi.org/project/pyaterochka-api/)
9
+ [![Discord](https://img.shields.io/discord/792572437292253224?label=Discord&labelColor=%232c2f33&color=%237289da)](https://discord.gg/UnJnGHNbBp)
10
+ [![Telegram](https://img.shields.io/badge/Telegram-24A1DE)](https://t.me/miskler_dev)
11
+
12
+
13
+
14
+ ## Installation / Установка:
15
+ 1. Install package / Установка пакета:
16
+ ```bash
17
+ pip install pyaterochka_api
18
+ ```
19
+ 2. ***Debian/Ubuntu Linux***: Install dependencies / Установка зависимостей:
20
+ ```bash
21
+ sudo apt update && sudo apt install -y libgtk-3-0 libx11-xcb1
22
+ ```
23
+ 3. Install browser / Установка браузера:
24
+ ```bash
25
+ camoufox fetch
26
+ ```
27
+
28
+ ### Usage / Использование:
29
+ ```py
30
+ from pyaterochka_api import Pyaterochka
31
+ import asyncio
32
+
33
+
34
+ async def main():
35
+ async with Pyaterochka(proxy="user:password@host:port", debug=False, autoclose_browser=False, trust_env=False) as API:
36
+ # RUS: Вводим геоточку (самого магазина или рядом с ним) и получаем инфу о магазине
37
+ # ENG: Enter a geolocation (of the store or near it) and get info about the store
38
+ find_store = await API.find_store(longitude=37.63156, latitude=55.73768)
39
+ print(f"Store info output: {find_store!s:.100s}...\n")
40
+
41
+ # RUS: Выводит список всех категорий на сайте
42
+ # ENG: Outputs a list of all categories on the site
43
+ catalog = await API.categories_list(subcategories=True, mode=API.PurchaseMode.DELIVERY)
44
+ print(f"Categories list output: {catalog!s:.100s}...\n")
45
+
46
+ # RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
47
+ # ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
48
+ # Страниц не сущетвует, использовать желаемый лимит (до 499) / Pages do not exist, use the desired limit (up to 499)
49
+ items = await API.products_list(catalog[0]['id'], limit=5)
50
+ print(f"Items list output: {items!s:.100s}...\n")
51
+
52
+ # RUS: Выводит информацию о товаре (по его plu - id товара).
53
+ # Функция в первый раз достаточно долгая, порядка 5-9 секунды, последующие запросы около 2 секунд (если браузер не был закрыт)
54
+ # ENG: Outputs information about the product (by its plu - product id).
55
+ # The function is quite long the first time, about 5-9 seconds, subsequent requests take about 2 seconds (if the browser was not closed)
56
+ info = await API.product_info(43347)
57
+ print(f"Product output: {info["props"]["pageProps"]["props"]['productStore']!s:.100s}...\n")
58
+
59
+ # RUS: Влияет исключительно на функцию выше (product_info), если включено, то после отработки запроса браузер закроется и кеши очищаются.
60
+ # Не рекомендую включать, если вам все же нужно освободить память, лучше использовать API.close(session=False, browser=True)
61
+ # ENG: Affects only the function above (product_info), if enabled, the browser will close after the request is processed and caches are cleared.
62
+ # 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)
63
+ API.autoclose_browser = True
64
+
65
+ # RUS: Напрямую передается в aiohttp, так же учитывается в браузере. В первую очередь нужен для использования системного `HTTPS_PROXY`.
66
+ # Но системный прокси применяется, только если не указали иное напрямую в `API.proxy`.
67
+ # ENG: Directly passed to aiohttp, also taken into account in the browser. Primarily needed for using the system `HTTPS_PROXY`.
68
+ # But the system proxy is applied only if you did not specify otherwise directly in `API.proxy`.
69
+ API.trust_env = True
70
+
71
+ # RUS: Выводит список последних промо-акций/новостей (можно поставить ограничитель по количеству, опционально)
72
+ # ENG: Outputs a list of the latest promotions/news (you can set a limit on the number, optionally)
73
+ news = await API.get_news(limit=5)
74
+ print(f"News output: {news!s:.100s}...\n")
75
+
76
+ # RUS: Выводит основной конфиг сайта (очень долгая функция, рекомендую сохранять в файл и переиспользовать)
77
+ # ENG: Outputs the main config of the site (large function, recommend to save in a file and re-use it)
78
+ print(f"Main config: {await API.get_config()!s:.100s}...\n")
79
+
80
+ # RUS: Если требуется, можно настроить вывод логов в консоль
81
+ # ENG: If required, you can configure the output of logs in the console
82
+ API.debug = True
83
+
84
+ # RUS: Скачивает картинку товара (возвращает BytesIO или None)
85
+ # ENG: Downloads the product image (returns BytesIO or None)
86
+ image = await API.download_image(url=items['products'][0]['image_links']['normal'][0])
87
+ with open(image.name, 'wb') as f:
88
+ f.write(image.getbuffer())
89
+
90
+ # RUS: Можно указать свой таймаут (браузер может его интерпретировать как x2 т.к. там 2 итерации скачивания)
91
+ # ENG: You can specify your own timeout (the browser may interpret it as x2 since there are 2 iterations of downloading)
92
+ API.timeout = 7
93
+
94
+ # RUS: Так же как и debug, в рантайме можно переназначить прокси
95
+ # ENG: As with debug, you can reassign the proxy in runtime
96
+ API.proxy = "user:password@host:port"
97
+ # RUS: Изменения происходят сразу же, кроме product_info, т.к. за него отвечает браузер
98
+ # ENG: Changes take effect immediately, except for product_info, as it is handled by the browser
99
+ await API.rebuild_connection(session=False, browser=True)
100
+ await API.product_info(43347)
101
+
102
+
103
+ if __name__ == '__main__':
104
+ asyncio.run(main())
105
+ ```
106
+
107
+ ### Report / Обратная связь
108
+
109
+ 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)!
110
+
111
+ Если у вас возникнут проблемы в использовании / пожелания, не стесняйтесь писать на [GitHub проекта](https://github.com/Open-Inflation/pyaterochka_api/issues)!
@@ -0,0 +1,3 @@
1
+ from .manager import Pyaterochka
2
+
3
+ __all__ = ['Pyaterochka']
@@ -0,0 +1,279 @@
1
+ import aiohttp
2
+ from fake_useragent import UserAgent
3
+ from enum import Enum
4
+ import re
5
+ from tqdm.asyncio import tqdm
6
+ from camoufox import AsyncCamoufox
7
+ import os
8
+
9
+
10
+ class PyaterochkaAPI:
11
+ """
12
+ Класс для загрузки JSON/image и парсинга JavaScript-конфигураций из удаленного источника.
13
+ """
14
+
15
+ class Patterns(Enum):
16
+ JS = r'\s*let\s+n\s*=\s*({.*});\s*' # let n = {...};
17
+ STR = r'(\w+)\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"' # key: "value"
18
+ DICT = r'(\w+)\s*:\s*{(.*?)}' # key: {...}
19
+ LIST = r'(\w+)\s*:\s*\[([^\[\]]*(?:\[.*?\])*)\]' # key: [value]
20
+ FIND = r'\{.*?\}|\[.*?\]' # {} or []
21
+
22
+ def __init__(self, debug: bool = False, proxy: str = None, autoclose_browser: bool = False, trust_env: bool = False, timeout: int = 10):
23
+ self._debug = debug
24
+ self._proxy = proxy
25
+ self._session = None
26
+ self._autoclose_browser = autoclose_browser
27
+ self._browser = None
28
+ self._bcontext = None
29
+ self._trust_env = trust_env
30
+ self._timeout = timeout
31
+
32
+ @property
33
+ def proxy(self) -> str | None:
34
+ return self._proxy if hasattr(self, '_proxy') else None
35
+
36
+ @proxy.setter
37
+ def proxy(self, value: str | None) -> None:
38
+ self._proxy = value
39
+
40
+ async def fetch(self, url: str) -> tuple[bool, dict | None | str, str]:
41
+ """
42
+ Выполняет HTTP-запрос к указанному URL и возвращает результат.
43
+
44
+ :return: Кортеж (успех, данные или None, тип данных или пустота).
45
+ """
46
+ args = {'url': url, 'timeout': aiohttp.ClientTimeout(total=self._timeout)}
47
+ if self._proxy: args["proxy"] = self._proxy
48
+
49
+ if self._debug:
50
+ print(f"Requesting \"{url}\" with proxy \"{args.get('proxy')}\", timeout {self._timeout}...", flush=True)
51
+
52
+ async with self._session.get(**args) as response:
53
+ if self._debug:
54
+ print(f"Response status: {response.status}", flush=True)
55
+
56
+ if response.status == 200:
57
+ if response.headers['content-type'] == 'application/json':
58
+ output_response = response.json()
59
+ elif response.headers['content-type'] == 'image/jpeg':
60
+ output_response = response.read()
61
+ else:
62
+ output_response = response.text()
63
+
64
+ return True, await output_response, response.headers['content-type']
65
+ elif response.status == 403:
66
+ if self._debug:
67
+ print("Anti-bot protection. Use Russia IP address and try again.", flush=True)
68
+ return False, None, ''
69
+ else:
70
+ if self._debug:
71
+ print(f"Unexpected error: {response.status}", flush=True)
72
+ raise Exception(f"Response status: {response.status} (unknown error/status code)")
73
+
74
+ async def _parse_js(self, js_code: str) -> dict | None:
75
+ """
76
+ Парсит JavaScript-код и извлекает данные из переменной "n".
77
+
78
+ :param js_code: JS-код в виде строки.
79
+ :return: Распарсенные данные в виде словаря или None.
80
+ """
81
+ matches = re.finditer(self.Patterns.JS.value, js_code)
82
+ match_list = list(matches)
83
+
84
+ if self._debug:
85
+ print(f"Found matches {len(match_list)}")
86
+ progress_bar = tqdm(total=33, desc="Parsing JS", position=0)
87
+
88
+ async def parse_match(match: str) -> dict:
89
+ result = {}
90
+
91
+ if self._debug:
92
+ progress_bar.set_description("Parsing strings")
93
+
94
+ # Парсинг строк
95
+ string_matches = re.finditer(self.Patterns.STR.value, match)
96
+ for m in string_matches:
97
+ key, value = m.group(1), m.group(2)
98
+ result[key] = value.replace('\"', '"').replace('\\', '\\')
99
+
100
+ if self._debug:
101
+ progress_bar.update(1)
102
+ progress_bar.set_description("Parsing dictionaries")
103
+
104
+ # Парсинг словарей
105
+ dict_matches = re.finditer(self.Patterns.DICT.value, match)
106
+ for m in dict_matches:
107
+ key, value = m.group(1), m.group(2)
108
+ if not re.search(self.Patterns.STR.value, value):
109
+ result[key] = await parse_match(value)
110
+
111
+ if self._debug:
112
+ progress_bar.update(1)
113
+ progress_bar.set_description("Parsing lists")
114
+
115
+ # Парсинг списков
116
+ list_matches = re.finditer(self.Patterns.LIST.value, match)
117
+ for m in list_matches:
118
+ key, value = m.group(1), m.group(2)
119
+ if not re.search(self.Patterns.STR.value, value):
120
+ result[key] = [await parse_match(item.group(0)) for item in re.finditer(self.Patterns.FIND.value, value)]
121
+
122
+ if self._debug:
123
+ progress_bar.update(1)
124
+
125
+ return result
126
+
127
+ if match_list and len(match_list) >= 1:
128
+ if self._debug:
129
+ print("Starting to parse match")
130
+ result = await parse_match(match_list[1].group(0))
131
+ if self._debug:
132
+ progress_bar.close()
133
+ return result
134
+ else:
135
+ if self._debug:
136
+ progress_bar.close()
137
+ raise Exception("N variable in JS code not found")
138
+
139
+ async def download_config(self, config_url: str) -> dict | None:
140
+ """
141
+ Загружает и парсит JavaScript-конфигурацию с указанного URL.
142
+
143
+ :param config_url: URL для загрузки конфигурации.
144
+ :return: Распарсенные данные в виде словаря или None.
145
+ """
146
+ is_success, js_code, _response_type = await self.fetch(url=config_url)
147
+
148
+ if not is_success:
149
+ if self._debug:
150
+ print("Failed to fetch JS code")
151
+ return None
152
+ elif self._debug:
153
+ print("JS code fetched successfully")
154
+
155
+ return await self._parse_js(js_code=js_code)
156
+
157
+
158
+ async def _browser_fetch(self, url: str, selector: str, state: str = 'attached') -> dict:
159
+ if self._browser is None or self._bcontext is None:
160
+ await self._new_session(include_aiohttp=False, include_browser=True)
161
+
162
+ page = await self._bcontext.new_page()
163
+ await page.goto(url, wait_until='commit', timeout=self._timeout * 1000)
164
+ # Wait until the selector script tag appears
165
+ await page.wait_for_selector(selector=selector, state=state, timeout=self._timeout * 1000)
166
+ content = await page.content()
167
+ await page.close()
168
+
169
+ if self._autoclose_browser:
170
+ await self.close(include_aiohttp=False, include_browser=True)
171
+ return content
172
+
173
+ def _parse_proxy(self, proxy_str: str | None) -> dict | None:
174
+ if not proxy_str:
175
+ if self._trust_env:
176
+ proxy_str = os.environ.get("HTTPS_PROXY") or os.environ.get("https_proxy")
177
+
178
+ if not proxy_str:
179
+ return None
180
+
181
+ # Example: user:pass@host:port or just host:port
182
+ match = re.match(
183
+ r'^(?:(?P<scheme>https?:\/\/))?(?:(?P<username>[^:@]+):(?P<password>[^@]+)@)?(?P<host>[^:]+):(?P<port>\d+)$',
184
+ proxy_str,
185
+ )
186
+
187
+ proxy_dict = {}
188
+ if not match:
189
+ proxy_dict['server'] = proxy_str
190
+
191
+ if not proxy_str.startswith('http://') and not proxy_str.startswith('https://'):
192
+ proxy_dict['server'] = f"http://{proxy_str}"
193
+
194
+ return proxy_dict
195
+ else:
196
+ match_dict = match.groupdict()
197
+ proxy_dict['server'] = f"{match_dict['scheme'] or 'http://'}{match_dict['host']}:{match_dict['port']}"
198
+
199
+ for key in ['username', 'password']:
200
+ if match_dict[key]:
201
+ proxy_dict[key] = match_dict[key]
202
+
203
+ return proxy_dict
204
+
205
+ async def _new_session(self, include_aiohttp: bool = True, include_browser: bool = False) -> None:
206
+ await self.close(include_aiohttp=include_aiohttp, include_browser=include_browser)
207
+
208
+ if include_aiohttp:
209
+ args = {
210
+ "headers": {
211
+ "User-Agent": UserAgent().random,
212
+ "Accept": "application/json, text/plain, */*",
213
+ "Accept-Language": "en-GB,en;q=0.5",
214
+ "Accept-Encoding": "gzip, deflate, br, zstd",
215
+ "X-PLATFORM": "webapp",
216
+ "Origin": "https://5ka.ru",
217
+ "Connection": "keep-alive",
218
+ "Sec-Fetch-Dest": "empty",
219
+ "Sec-Fetch-Mode": "cors",
220
+ "Sec-Fetch-Site": "same-site",
221
+ "Pragma": "no-cache",
222
+ "Cache-Control": "no-cache",
223
+ "TE": "trailers",
224
+ },
225
+ "trust_env": self._trust_env,
226
+ }
227
+ self._session = aiohttp.ClientSession(**args)
228
+
229
+ if self._debug: print(f"A new connection aiohttp has been opened. trust_env: {args.get('trust_env')}")
230
+
231
+ if include_browser:
232
+ prox = self._parse_proxy(self.proxy)
233
+ self._browser = await AsyncCamoufox(headless=not self._debug, proxy=prox, geoip=True).__aenter__()
234
+ self._bcontext = await self._browser.new_context()
235
+
236
+ toprint = "SYSTEM_PROXY" if prox and not self.proxy else prox
237
+ if self._debug: print(f"A new connection browser has been opened. Proxy used: {toprint}")
238
+
239
+ async def close(
240
+ self,
241
+ include_aiohttp: bool = True,
242
+ include_browser: bool = False
243
+ ) -> None:
244
+ """
245
+ Close the aiohttp session and/or Camoufox browser if they are open.
246
+ :param include_aiohttp: close aiohttp session if True
247
+ :param include_browser: close browser if True
248
+ """
249
+ to_close = []
250
+ if include_aiohttp:
251
+ to_close.append("session")
252
+ if include_browser:
253
+ to_close.append("bcontext")
254
+ to_close.append("browser")
255
+
256
+ if not to_close:
257
+ raise ValueError("No connections to close")
258
+
259
+ checks = {
260
+ "session": lambda a: a is not None and not a.closed,
261
+ "browser": lambda a: a is not None,
262
+ "bcontext": lambda a: a is not None
263
+ }
264
+
265
+ for name in to_close:
266
+ attr = getattr(self, f"_{name}", None)
267
+ if checks[name](attr):
268
+ if "browser" in name:
269
+ await attr.__aexit__(None, None, None)
270
+ else:
271
+ await attr.close()
272
+ setattr(self, f"_{name}", None)
273
+ if self._debug:
274
+ print(f"The {name} connection was closed")
275
+ else:
276
+ if self._debug:
277
+ print(f"The {name} connection was not open")
278
+
279
+