Skip to content

Commit aec996f

Browse files
committed
Merge main into snapshot-bot/update-snapshots
2 parents 4fc660c + 19a6b32 commit aec996f

20 files changed

Lines changed: 4469 additions & 525 deletions

.github/workflows/publish.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ on:
1111
options: [docs, package, all]
1212

1313
permissions:
14-
contents: read
14+
contents: write
15+
issues: write
16+
pull-requests: write
1517
pages: write
1618
id-token: write # нужно для PyPI Trusted Publishing (OIDC)
1719

Makefile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ install-dev:
77
pip install -e .[dev]
88

99
test:
10-
pytest --cov=chizhik_api --cov-report=xml --cov-report=html --cov-report=term-missing
10+
pytest --cov=fixprice_api --cov-report=xml --cov-report=html --cov-report=term-missing
1111

1212
test-quick:
1313
pytest --tb=short
@@ -19,8 +19,8 @@ type-check:
1919
python -m mypy fixprice_api
2020

2121
format:
22-
black chizhik_api/ tests/
23-
isort chizhik_api/ tests/
22+
black fixprice_api/ tests/
23+
isort fixprice_api/ tests/
2424

2525
clean:
2626
rm -rf build/ dist/ *.egg-info/

README.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ async def main():
5555
)
5656
).json()
5757
first_product_id = products[0]["id"]
58+
first_product_url = products[0]["url"]
5859
print(f"Первый товар: {products[0]['title']!s:.60s} ({first_product_id})")
5960

6061
# 3. Геолокация (влияет на каталог и баланс)
@@ -66,7 +67,11 @@ async def main():
6667
balance = (await api.Catalog.Product.balance(product_id=first_product_id)).json()
6768
print(f"Проверено магазинов: {len(balance)}")
6869

69-
# 5. Загрузка изображения
70+
# 5. Подробное инфо о товаре
71+
info = (await api.Catalog.Product.info(url=first_product_url)).json()
72+
print(f"Подробно о товаре: {list(info.keys())}")
73+
74+
# 6. Загрузка изображения
7075
image_url = products[0]["images"][0]["src"]
7176
image_stream = await api.General.download_image(image_url)
7277
with Image.open(image_stream) as img:

docs/source/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ def _get_version() -> str:
3838
pass
3939
# 3) fallback: import from package (may fail on RTD without deps)
4040
try:
41-
from chizhik_api import __version__ # type: ignore
41+
from fixprice_api import __version__ # type: ignore
4242
return __version__
4343
except Exception:
4444
return "0.0.0"

docs/source/quick_start.rst

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -8,41 +8,53 @@ Quick Start
88
99
.. code-block:: python
1010
11-
from chizhik_api import ChizhikAPI
11+
import asyncio
12+
from fixprice_api import FixPriceAPI, CatalogSort
13+
from PIL import Image
14+
1215
1316
async def main():
14-
# RUS: Использование проксирования опционально. Вы можете создать несколько агентов с разными прокси для ускорения парса.
15-
# ENG: Proxy usage is optional. You can create multiple agents with different proxies for faster parsing.
16-
async with ChizhikAPI(proxy="user:password@host:port", headless=False) as API:
17-
# RUS: Выводит активные предложения магазина
18-
# ENG: Outputs active offers of the store
19-
print(f"Active offers output: {(await API.Advertising.active_inout()).json()!s:.100s}...\n")
20-
21-
# RUS: Выводит список городов соответствующих поисковому запросу (только на русском языке)
22-
# ENG: Outputs a list of cities corresponding to the search query (only in Russian language)
23-
city_list = (await API.Geolocation.cities_list(search_name='ар', page=1)).json()
24-
print(f"Cities list output: {city_list!s:.100s}...\n")
25-
# Счет страниц с единицы / index starts from 1
26-
27-
# RUS: Выводит список всех категорий на сайте
28-
# ENG: Outputs a list of all categories on the site
29-
catalog = (await API.Catalog.tree()).json()
30-
print(f"Categories list output: {catalog!s:.100s}...\n")
31-
32-
# RUS: Выводит список всех товаров выбранной категории (ограничение 100 элементов, если превышает - запрашивайте через дополнительные страницы)
33-
# ENG: Outputs a list of all items in the selected category (limiting to 100 elements, if exceeds - request through additional pages)
34-
items = (await API.Catalog.products_list(category_id=catalog[0]['id'], page=1)).json()
35-
print(f"Items list output: {items!s:.100s}...\n")
36-
# Счет страниц с единицы / index starts from 1
37-
38-
# RUS: Сохраняем изображение с сервера (в принципе, сервер отдал бы их и без обертки моего объекта, но лучше максимально претворяться обычным пользователем)
39-
# ENG: Saving an image from the server (in fact, the server gave them and without wrapping my object, but better to be as a regular user)
40-
image = await API.General.download_image(items['items'][0]['images'][0]['image'])
41-
with open(image.name, 'wb') as f:
42-
f.write(image.read())
17+
async with FixPriceAPI() as api:
18+
# 1. Получаем дерево категорий
19+
tree_data = (await api.Catalog.tree()).json()
20+
first_alias = tree_data[next(iter(tree_data))]["alias"]
21+
print(f"Первая категория: {first_alias}")
4322
44-
import asyncio
45-
asyncio.run(main())
23+
# 2. Список товаров в категории
24+
products = (
25+
await api.Catalog.products_list(
26+
category_alias=first_alias,
27+
page=1,
28+
limit=24,
29+
sort=CatalogSort.POPULARITY,
30+
)
31+
).json()
32+
first_product_id = products[0]["id"]
33+
first_product_url = products[0]["url"]
34+
print(f"Первый товар: {products[0]['title']!s:.60s} ({first_product_id})")
35+
36+
# 3. Геолокация (влияет на каталог и баланс)
37+
cities = (await api.Geolocation.cities_list(country_id=2)).json() # Россия
38+
api.city_id = cities[0]["id"]
39+
print(f"Текущий city_id: {api.city_id}")
40+
41+
# 4. Проверка наличия товара по магазинам
42+
balance = (await api.Catalog.Product.balance(product_id=first_product_id)).json()
43+
print(f"Проверено магазинов: {len(balance)}")
44+
45+
# 5. Подробное инфо о товаре
46+
info = (await api.Catalog.Product.info(url=first_product_url)).json()
47+
print(f"Подробно о товаре: {list(info.keys())}")
48+
49+
# 6. Загрузка изображения
50+
image_url = products[0]["images"][0]["src"]
51+
image_stream = await api.General.download_image(image_url)
52+
with Image.open(image_stream) as img:
53+
print(f"Image format: {img.format}, size: {img.size}")
54+
55+
56+
if __name__ == "__main__":
57+
asyncio.run(main())
4658
4759
.. code-block:: console
4860

fixprice_api/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
from .abstraction import CatalogSort
33

44
__all__ = ["FixPriceAPI", "CatalogSort"]
5-
__version__ = "0.2.2"
5+
__version__ = "0.2.4"

fixprice_api/abstraction.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
class CatalogSort:
32
POPULARITY = "sold"
43
"""Сначало самые популярные"""

fixprice_api/endpoints/catalog.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,12 @@
22

33
from __future__ import annotations
44

5+
import json
6+
from types import MethodType
7+
8+
from playwright.async_api import Response as PWResponse
59
from dataclasses import dataclass
6-
from typing import Optional, TYPE_CHECKING
10+
from typing import Optional, TYPE_CHECKING, overload
711
from .. import abstraction
812
from human_requests import autotest
913
from human_requests.abstraction import FetchResponse, HttpMethod
@@ -101,3 +105,84 @@ async def balance(
101105
url += "&inStock=true"
102106

103107
return await self._parent._request(HttpMethod.GET, url)
108+
109+
@overload
110+
async def info(self, *, url: str): ...
111+
112+
@overload
113+
async def info(self, *, category: str, product_id: int, slug: str): ...
114+
115+
@autotest
116+
async def info(
117+
self,
118+
*,
119+
url: str | None = None,
120+
category: str | None = None,
121+
product_id: int | None = None,
122+
slug: str | None = None,
123+
) -> PWResponse:
124+
"""
125+
Информация СПАРСИВАЕТСЯ (в отличии от других методов).
126+
Инфо о товаре со страницы типа
127+
https://fix-price.com/catalog/produkty-i-napitki/p-1902248-shokoladnye-konfety-inis-nickers-135-g
128+
129+
Либо предоставляете url напрямую, например `products[0]["url"]`
130+
131+
Данные карточки лежат в obj["data"][0]["categoryData"]["product"]
132+
"""
133+
134+
real_url = "https://fix-price.com/catalog/"
135+
if url is None:
136+
if category is None or product_id is None or slug is None:
137+
raise TypeError(
138+
"Either url or (category, product_id, slug) must be provided"
139+
)
140+
141+
real_url += f"{category}/p-{product_id}-{slug}"
142+
else:
143+
real_url += url
144+
145+
page = await self._parent.ctx.new_page()
146+
try:
147+
resp = await page.goto(real_url, wait_until="domcontentloaded")
148+
if resp is None:
149+
raise RuntimeError("page.goto() returned None")
150+
151+
raw_json = await page.evaluate("""
152+
() => {
153+
const marker = "window.__NUXT__=";
154+
155+
for (const s of document.scripts) {
156+
const txt = s.textContent || "";
157+
const idx = txt.indexOf(marker);
158+
159+
if (idx !== -1) {
160+
let expr = txt.slice(idx + marker.length).trim();
161+
162+
if (expr.endsWith(";")) {
163+
expr = expr.slice(0, -1);
164+
}
165+
166+
const obj = Function('"use strict"; return (' + expr + ')')();
167+
return JSON.stringify(obj);
168+
}
169+
}
170+
171+
return null;
172+
}
173+
""")
174+
175+
nuxt_data = (
176+
json.loads(raw_json)["data"][0]["categoryData"]["product"]
177+
if raw_json
178+
else None
179+
)
180+
181+
def _json(self):
182+
return nuxt_data
183+
184+
resp.json = MethodType(_json, resp)
185+
186+
return resp
187+
finally:
188+
await page.close()

fixprice_api/endpoints/general.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,11 @@ async def download_image(
2626
attempts=retry_attempts, start_timeout=3.0, max_timeout=timeout
2727
)
2828

29-
px = self._parent.proxy if isinstance(self._parent.proxy, Proxy) else Proxy(self._parent.proxy)
29+
px = (
30+
self._parent.proxy
31+
if isinstance(self._parent.proxy, Proxy)
32+
else Proxy(self._parent.proxy)
33+
)
3034
async with RetryClient(retry_options=retry_options) as retry_client:
3135
async with retry_client.get(
3236
url, raise_for_status=True, proxy=px.as_str()

fixprice_api/endpoints/geolocation.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ async def countries_list(self, alias: str = None) -> FetchResponse:
4444
async def regions_list(self, country_id: int = None) -> FetchResponse:
4545
"""Возвращает список всех регионов, их id и название. Если фильтр не применен - выдача всех регионов независимо от страны."""
4646
url = f"{self._parent.CATALOG_URL}/v1/location/region"
47-
if not country_id:
47+
if country_id:
4848
url += f"?countryId={country_id}"
4949

5050
return await self._parent._request(HttpMethod.GET, url=url)

0 commit comments

Comments
 (0)