Skip to content

Commit c96cdb8

Browse files
committed
refactore
1 parent 27a799b commit c96cdb8

6 files changed

Lines changed: 61 additions & 100 deletions

File tree

main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ async def main():
66
async with PyaterochkaAPI(headless=False) as API:
77
# RUS: Выводит активные предложения магазина
88
# ENG: Outputs active offers of the store
9-
print(f"Active offers output: {(await API.Advertising.active_inout()).json()!s:.100s}...\n")
9+
print(f"Active offers output: {(await API.Catalog.tree(sap_code_store_id="35XY")).json()!s:.100s}...\n")
1010

1111
import asyncio
1212
asyncio.run(main())

pyaterochka_api/endpoints/advertising.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from human_requests.abstraction import FetchResponse, HttpMethod
66

77
if TYPE_CHECKING:
8-
from ..old import ChizhikAPI
8+
from ..manager import PyaterochkaAPI
99

1010

1111
class ClassAdvertising:
@@ -14,9 +14,8 @@ class ClassAdvertising:
1414
Включает получение баннеров, слайдеров, буклетов и другого рекламного контента.
1515
"""
1616

17-
def __init__(self, parent: "ChizhikAPI", CATALOG_URL: str):
18-
self._parent: "ChizhikAPI" = parent
19-
self.CATALOG_URL: str = CATALOG_URL
17+
def __init__(self, parent: "PyaterochkaAPI"):
18+
self._parent: "PyaterochkaAPI" = parent
2019

2120
async def get_news(self, limit: int | None = None) -> FetchResponse:
2221
"""
@@ -28,10 +27,8 @@ async def get_news(self, limit: int | None = None) -> FetchResponse:
2827
Returns:
2928
dict: A dictionary representing the news if the request is successful, error otherwise.
3029
"""
31-
url = f"{self.BASE_URL}/api/public/v1/news/"
30+
url = f"{self._parent.MAIN_SITE_URL}/api/public/v1/news/"
3231
if limit and limit > 0:
3332
url += f"?limit={limit}"
3433

35-
_is_success, response, _response_type = await self.api.fetch(url=url)
36-
37-
return response
34+
return await self._parent._request(method=HttpMethod.GET, url=url)
Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
"""Работа с каталогом"""
22

3-
from typing import TYPE_CHECKING, Optional
3+
from typing import TYPE_CHECKING
44

55
from human_requests.abstraction import FetchResponse, HttpMethod
66

7+
from ..enums import PurchaseMode
78
if TYPE_CHECKING:
8-
from ..old import ChizhikAPI
9+
from ..manager import PyaterochkaAPI
910

1011

1112
class ClassCatalog:
@@ -15,19 +16,18 @@ class ClassCatalog:
1516
работу с фидами товаров и отзывами.
1617
"""
1718

18-
def __init__(self, parent: "ChizhikAPI", CATALOG_URL: str):
19-
self._parent: "ChizhikAPI" = parent
20-
self.CATALOG_URL: str = CATALOG_URL
19+
def __init__(self, parent: "PyaterochkaAPI"):
20+
self._parent: "PyaterochkaAPI" = parent
2121
self.Product: ProductService = ProductService(
22-
parent=self._parent, CATALOG_URL=CATALOG_URL
22+
parent=self._parent
2323
)
2424
"""Сервис для работы с товарами в каталоге."""
2525

2626
async def tree(self,
27+
sap_code_store_id: str,
2728
subcategories: bool = False,
2829
include_restrict: bool = True,
29-
mode: PurchaseMode = PurchaseMode.STORE,
30-
sap_code_store_id: str = DEFAULT_STORE_ID) -> FetchResponse:
30+
mode: PurchaseMode = PurchaseMode.STORE) -> FetchResponse:
3131
"""
3232
Asynchronously retrieves a list of categories from the Pyaterochka API.
3333
@@ -44,16 +44,14 @@ async def tree(self,
4444
Exception: If the response status is not 200 (OK) or 403 (Forbidden / Anti-bot).
4545
"""
4646

47-
request_url = f"{self.API_URL}/catalog/v2/stores/{sap_code_store_id}/categories?mode={mode.value}&include_restrict={include_restrict}&include_subcategories={1 if subcategories else 0}"
48-
_is_success, response, _response_type = await self.api.fetch(url=request_url)
49-
return response
47+
request_url = f"{self._parent.CATALOG_URL}/catalog/v2/stores/{sap_code_store_id}/categories?mode={mode.value}&include_restrict={include_restrict}&include_subcategories={1 if subcategories else 0}"
48+
return await self._parent._request(method=HttpMethod.GET, url=request_url)
5049

5150
async def products_list(
52-
5351
self,
5452
category_id: str,
53+
sap_code_store_id: str,
5554
mode: PurchaseMode = PurchaseMode.STORE,
56-
sap_code_store_id: str = DEFAULT_STORE_ID,
5755
limit: int = 30
5856
) -> FetchResponse:
5957
f"""
@@ -76,20 +74,22 @@ async def products_list(
7674
if limit < 1 or limit >= 500:
7775
raise ValueError("Limit must be between 1 and 499")
7876

79-
request_url = f"{self.API_URL}/catalog/v2/stores/{sap_code_store_id}/categories/{category_id}/products?mode={mode.value}&limit={limit}"
80-
_is_success, response, _response_type = await self.api.fetch(url=request_url)
81-
return response
77+
request_url = f"{self._parent.CATALOG_URL}/catalog/v2/stores/{sap_code_store_id}/categories/{category_id}/products?mode={mode.value}&limit={limit}"
78+
return await self._parent._request(method=HttpMethod.GET, url=request_url)
8279

8380

8481
class ProductService:
8582
"""Сервис для работы с товарами в каталоге."""
8683

87-
def __init__(self, parent: "ChizhikAPI", CATALOG_URL: str):
88-
self._parent: "ChizhikAPI" = parent
89-
self.CATALOG_URL: str = CATALOG_URL
84+
def __init__(self, parent: "PyaterochkaAPI"):
85+
self._parent: "PyaterochkaAPI" = parent
9086

9187
async def info(
92-
self, plu_id: int
88+
self,
89+
sap_code_store_id: str,
90+
plu_id: int,
91+
mode: PurchaseMode = PurchaseMode.STORE,
92+
include_restrict: bool = True
9393
) -> FetchResponse:
9494
"""
9595
Asynchronously retrieves product information from the Pyaterochka API for a given PLU ID. Average time processing 2 seconds (first start 6 seconds).
@@ -101,21 +101,6 @@ async def info(
101101
Raises:
102102
ValueError: If the response does not contain the expected JSON data.
103103
"""
104-
105-
url = f"{self.BASE_URL}/product/{plu_id}/"
106-
response = await self.api.browser_fetch(url=url, selector='script#__NEXT_DATA__[type="application/json"]')
107-
108-
match = re.search(
109-
r'<script\s+id="__NEXT_DATA__"\s+type="application/json">(.+?)</script>',
110-
response,
111-
flags=re.DOTALL
112-
)
113-
if not match:
114-
raise ValueError("product_info: Failed to find JSON data in the response")
115-
json_text = match.group(1)
116-
data = json.loads(json_text)
117-
data["props"]["pageProps"]["props"]["productStore"] = json.loads(data["props"]["pageProps"]["props"]["productStore"])
118-
data["props"]["pageProps"]["props"]["catalogStore"] = json.loads(data["props"]["pageProps"]["props"]["catalogStore"])
119-
data["props"]["pageProps"]["props"]["filtersPageStore"] = json.loads(data["props"]["pageProps"]["props"]["filtersPageStore"])
120-
121-
return data
104+
105+
request_url = f"{self._parent.CATALOG_URL}/catalog/v2/stores/{sap_code_store_id}/products/{plu_id}?mode={mode.value}&include_restrict={str(include_restrict).lower()}"
106+
return await self._parent._request(method=HttpMethod.GET, url=request_url)

pyaterochka_api/endpoints/general.py

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77

88
if TYPE_CHECKING:
9-
from ..old import ChizhikAPI
9+
from ..manager import PyaterochkaAPI
1010

1111

1212
class ClassGeneral:
@@ -16,23 +16,19 @@ class ClassGeneral:
1616
получения информации о пользователе и других общих функций.
1717
"""
1818

19-
def __init__(self, parent: "ChizhikAPI", CATALOG_URL: str):
20-
self._parent: ChizhikAPI = parent
21-
self.CATALOG_URL: str = CATALOG_URL
22-
23-
async def download_image(self, url: str) -> BytesIO:
24-
is_success, image_data, response_type = await self.api.fetch(url=url)
25-
26-
if not is_success:
27-
self.api._logger.error("Failed to fetch image")
28-
return
29-
elif not isinstance(image_data, (bytes, bytearray)):
30-
self.api._logger.error("Image data is not bytes")
31-
return
32-
33-
self.api._logger.debug("Image fetched successfully")
34-
35-
image = BytesIO(image_data)
36-
image.name = f'{url.split("/")[-1]}.{response_type.split("/")[-1]}'
37-
38-
return image
19+
def __init__(self, parent: "PyaterochkaAPI"):
20+
self._parent: "PyaterochkaAPI" = parent
21+
22+
async def download_image(self,
23+
url: str,
24+
retry_attempts: int = 3,
25+
timeout: float = 10) -> BytesIO:
26+
"""Скачать изображение по URL."""
27+
retry_options = ExponentialRetry(attempts=retry_attempts, start_timeout=3.0, max_timeout=timeout)
28+
29+
async with RetryClient(retry_options=retry_options) as retry_client:
30+
async with retry_client.get(url, raise_for_status=True) as resp:
31+
body = await resp.read()
32+
file = BytesIO(body)
33+
file.name = url.split("/")[-1]
34+
return file

pyaterochka_api/endpoints/geolocation.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from human_requests.abstraction import FetchResponse, HttpMethod
66

77
if TYPE_CHECKING:
8-
from ..old import ChizhikAPI
8+
from ..manager import PyaterochkaAPI
99

1010

1111
class ClassGeolocation:
@@ -15,11 +15,10 @@ class ClassGeolocation:
1515
и управление настройками доставки.
1616
"""
1717

18-
def __init__(self, parent: "ChizhikAPI", CATALOG_URL: str):
19-
self._parent: ChizhikAPI = parent
20-
self.CATALOG_URL: str = CATALOG_URL
18+
def __init__(self, parent: "PyaterochkaAPI"):
19+
self._parent: PyaterochkaAPI = parent
2120

22-
async def find_store(self, longitude: float, latitude: float) -> dict:
21+
async def find_store(self, longitude: float, latitude: float) -> FetchResponse:
2322
"""
2423
Asynchronously finds the store associated with the given coordinates.
2524
@@ -31,6 +30,5 @@ async def find_store(self, longitude: float, latitude: float) -> dict:
3130
dict: A dictionary representing the store information if the request is successful, error otherwise.
3231
"""
3332

34-
request_url = f"{self.API_URL}/orders/v1/orders/stores/?lon={longitude}&lat={latitude}"
35-
_is_success, response, _response_type = await self.api.fetch(url=request_url)
36-
return response
33+
request_url = f"{self._parent.CATALOG_URL}/orders/v1/orders/stores/?lon={longitude}&lat={latitude}"
34+
return await self._parent._request(method=HttpMethod.GET, url=request_url)

pyaterochka_api/manager.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from typing import Any
66

77
from camoufox.async_api import AsyncCamoufox
8-
from playwright.async_api import TimeoutError
98
from human_requests import HumanBrowser, HumanContext, HumanPage
109
from human_requests.abstraction import FetchResponse, HttpMethod, Proxy
1110

@@ -60,10 +59,10 @@ class PyaterochkaAPI:
6059

6160
# ───── lifecycle ─────
6261
def __post_init__(self) -> None:
63-
self.Geolocation = ClassGeolocation(self, self.CATALOG_URL)
64-
self.Catalog = ClassCatalog(self, self.CATALOG_URL)
65-
self.Advertising = ClassAdvertising(self, self.CATALOG_URL)
66-
self.General = ClassGeneral(self, self.CATALOG_URL)
62+
self.Geolocation = ClassGeolocation(self)
63+
self.Catalog = ClassCatalog(self)
64+
self.Advertising = ClassAdvertising(self)
65+
self.General = ClassGeneral(self)
6766

6867
async def __aenter__(self):
6968
"""Вход в контекстный менеджер с автоматическим прогревом сессии."""
@@ -82,24 +81,10 @@ async def _warmup(self) -> None:
8281
self.session = HumanBrowser.replace(br)
8382
self.ctx = await self.session.new_context()
8483
self.page = await self.ctx.new_page()
85-
await self.page.goto(self.CATALOG_URL, wait_until="networkidle")
86-
87-
ok = False
88-
try_count = 3
89-
while not ok or try_count <= 0:
90-
try_count -= 1
91-
try:
92-
await self.page.wait_for_selector(
93-
"pre", timeout=self.timeout_ms, state="attached"
94-
)
95-
ok = True
96-
except TimeoutError:
97-
await self.page.reload()
98-
if not ok:
99-
raise RuntimeError(self.page.content)
100-
101-
# await self.page.wait_for_load_state("networkidle")
102-
# await asyncio.sleep(3)
84+
85+
await self.page.goto("https://5ka.ru", wait_until="load")
86+
await self.page.wait_for_selector(selector="next-route-announcer", state="attached")
87+
await self.page.wait_for_load_state('networkidle')
10388

10489
async def __aexit__(self, *exc):
10590
"""Выход из контекстного менеджера с закрытием сессии."""
@@ -134,7 +119,7 @@ async def _request(
134119
body=json_body,
135120
mode="cors",
136121
timeout_ms=self.timeout_ms,
137-
referrer=self.MAIN_SITE_ORIGIN,
122+
referrer=self.MAIN_SITE_URL,
138123
headers={"Accept": "application/json, text/plain, */*"},
139124
)
140125

0 commit comments

Comments
 (0)