allegro-cli 0.1.0__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 (29) hide show
  1. allegro_cli-0.1.0/PKG-INFO +152 -0
  2. allegro_cli-0.1.0/README.md +135 -0
  3. allegro_cli-0.1.0/allegro_cli/__init__.py +1 -0
  4. allegro_cli-0.1.0/allegro_cli/api/__init__.py +0 -0
  5. allegro_cli-0.1.0/allegro_cli/api/client.py +332 -0
  6. allegro_cli-0.1.0/allegro_cli/api/models.py +88 -0
  7. allegro_cli-0.1.0/allegro_cli/commands/__init__.py +0 -0
  8. allegro_cli-0.1.0/allegro_cli/commands/cart.py +72 -0
  9. allegro_cli-0.1.0/allegro_cli/commands/config_cmd.py +40 -0
  10. allegro_cli-0.1.0/allegro_cli/commands/login.py +44 -0
  11. allegro_cli-0.1.0/allegro_cli/commands/packages.py +25 -0
  12. allegro_cli-0.1.0/allegro_cli/commands/search.py +51 -0
  13. allegro_cli-0.1.0/allegro_cli/config.py +42 -0
  14. allegro_cli-0.1.0/allegro_cli/cookie_import.py +24 -0
  15. allegro_cli-0.1.0/allegro_cli/main.py +207 -0
  16. allegro_cli-0.1.0/allegro_cli/output.py +90 -0
  17. allegro_cli-0.1.0/allegro_cli/scraper.py +572 -0
  18. allegro_cli-0.1.0/allegro_cli.egg-info/PKG-INFO +152 -0
  19. allegro_cli-0.1.0/allegro_cli.egg-info/SOURCES.txt +27 -0
  20. allegro_cli-0.1.0/allegro_cli.egg-info/dependency_links.txt +1 -0
  21. allegro_cli-0.1.0/allegro_cli.egg-info/entry_points.txt +2 -0
  22. allegro_cli-0.1.0/allegro_cli.egg-info/requires.txt +9 -0
  23. allegro_cli-0.1.0/allegro_cli.egg-info/top_level.txt +1 -0
  24. allegro_cli-0.1.0/pyproject.toml +41 -0
  25. allegro_cli-0.1.0/setup.cfg +4 -0
  26. allegro_cli-0.1.0/tests/test_cli.py +225 -0
  27. allegro_cli-0.1.0/tests/test_config.py +32 -0
  28. allegro_cli-0.1.0/tests/test_e2e.py +356 -0
  29. allegro_cli-0.1.0/tests/test_scraper.py +473 -0
@@ -0,0 +1,152 @@
1
+ Metadata-Version: 2.4
2
+ Name: allegro-cli
3
+ Version: 0.1.0
4
+ Summary: CLI for browsing Allegro offers, managing cart, and tracking packages — human-readable and LLM-agent friendly
5
+ Author: Piotr Konowrocki
6
+ Project-URL: Repository, https://github.com/pkonowrocki/allegro-cli
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: httpx>=0.27
10
+ Requires-Dist: beautifulsoup4>=4.12
11
+ Requires-Dist: lxml>=5.0
12
+ Requires-Dist: curl_cffi>=0.7
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=8.0; extra == "dev"
15
+ Requires-Dist: commitizen>=4.1; extra == "dev"
16
+ Requires-Dist: build>=1.0; extra == "dev"
17
+
18
+ # allegro-cli
19
+
20
+ [![CI](https://github.com/pkonowrocki/allegro-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pkonowrocki/allegro-cli/actions/workflows/ci.yml)
21
+
22
+ CLI for browsing [Allegro](https://allegro.pl) offers, managing your cart, and tracking packages. Designed to be both human-readable and LLM-agent friendly.
23
+
24
+ All output is available as aligned text tables, JSON, or TSV — pick what suits your workflow or pipe it into other tools.
25
+
26
+ ## Install
27
+
28
+ **From GitHub Releases** (recommended):
29
+
30
+ ```bash
31
+ pip install https://github.com/pkonowrocki/allegro-cli/releases/latest/download/allegro_cli-0.1.0-py3-none-any.whl
32
+ ```
33
+
34
+ **From source (latest)**:
35
+
36
+ ```bash
37
+ pip install git+https://github.com/pkonowrocki/allegro-cli.git
38
+ ```
39
+
40
+ **For development**:
41
+
42
+ ```bash
43
+ git clone https://github.com/pkonowrocki/allegro-cli.git
44
+ cd allegro-cli
45
+ pip install -e ".[dev]"
46
+ ```
47
+
48
+ ## Setup
49
+
50
+ Import cookies from your browser:
51
+
52
+ ```bash
53
+ allegro login
54
+ ```
55
+
56
+ Paste cookies from Chrome DevTools (Application > Cookies > allegro.pl). Both the DevTools table format and raw cookie header strings are accepted.
57
+
58
+ Alternatively, set cookies directly:
59
+
60
+ ```bash
61
+ allegro config set --cookies 'cookie1=value1; cookie2=value2'
62
+ ```
63
+
64
+ ## Usage
65
+
66
+ ### Search
67
+
68
+ ```bash
69
+ allegro search "laptop"
70
+ allegro search "laptop" --category 491
71
+ allegro search "laptop" --category laptopy-491 --sort pd --price-min 2000 --price-max 5000
72
+ allegro search "laptop" --columns "id,name,sellingMode.price.amount,parameters"
73
+ ```
74
+
75
+ | Flag | Description |
76
+ |------|-------------|
77
+ | `--category` | Category ID or slug (e.g. `491`, `laptopy-491`) |
78
+ | `--sort` | Sort order: `p` (price asc), `pd` (price desc), `m` (relevance), `n` (newest) |
79
+ | `--price-min` | Minimum price in PLN |
80
+ | `--price-max` | Maximum price in PLN |
81
+ | `--page` | Page number (default: 1) |
82
+ | `--columns` | Comma-separated columns to display |
83
+
84
+ ### Offer details
85
+
86
+ ```bash
87
+ allegro offer 12345678
88
+ allegro offer 12345678 --columns "name,sellingMode.price.amount,parameters"
89
+ ```
90
+
91
+ Offer pages include a `parameters` field with product specifications (e.g. processor, RAM, screen size) extracted automatically from the listing.
92
+
93
+ ### Cart
94
+
95
+ ```bash
96
+ allegro cart list
97
+ allegro cart add OFFER_ID SELLER_ID --quantity 2
98
+ allegro cart remove OFFER_ID SELLER_ID
99
+ ```
100
+
101
+ ### Packages
102
+
103
+ ```bash
104
+ allegro packages
105
+ ```
106
+
107
+ ### Configuration
108
+
109
+ ```bash
110
+ allegro config show
111
+ allegro config set --output-format json
112
+ allegro config set --flaresolverr-url http://localhost:8191/v1
113
+ ```
114
+
115
+ ## Output formats
116
+
117
+ All commands support `--format text` (default), `--format json`, and `--format tsv`.
118
+
119
+ ```bash
120
+ allegro search "laptop" --format json # full JSON array
121
+ allegro search "laptop" --format tsv # tab-separated, pipe-friendly
122
+ allegro search "laptop" # aligned text table (default)
123
+ allegro offer 12345678 --format json # full offer with parameters
124
+ ```
125
+
126
+ Use `--columns` to select specific fields (dot-notation supported):
127
+
128
+ ```bash
129
+ allegro search "laptop" --columns "id,name,sellingMode.price.amount"
130
+ allegro offer 12345678 --columns "name,parameters"
131
+ ```
132
+
133
+ Set a persistent default:
134
+
135
+ ```bash
136
+ allegro config set --output-format json
137
+ ```
138
+
139
+ ## Anti-bot handling
140
+
141
+ Allegro uses anti-bot protection (DataDome). The CLI first tries a direct request with your cookies via `curl_cffi` (Chrome TLS fingerprint). If that gets a 403, it falls back to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr):
142
+
143
+ ```bash
144
+ docker run -d --name flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
145
+ ```
146
+
147
+ ## Development
148
+
149
+ ```bash
150
+ pip install -e ".[dev]"
151
+ pytest
152
+ ```
@@ -0,0 +1,135 @@
1
+ # allegro-cli
2
+
3
+ [![CI](https://github.com/pkonowrocki/allegro-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pkonowrocki/allegro-cli/actions/workflows/ci.yml)
4
+
5
+ CLI for browsing [Allegro](https://allegro.pl) offers, managing your cart, and tracking packages. Designed to be both human-readable and LLM-agent friendly.
6
+
7
+ All output is available as aligned text tables, JSON, or TSV — pick what suits your workflow or pipe it into other tools.
8
+
9
+ ## Install
10
+
11
+ **From GitHub Releases** (recommended):
12
+
13
+ ```bash
14
+ pip install https://github.com/pkonowrocki/allegro-cli/releases/latest/download/allegro_cli-0.1.0-py3-none-any.whl
15
+ ```
16
+
17
+ **From source (latest)**:
18
+
19
+ ```bash
20
+ pip install git+https://github.com/pkonowrocki/allegro-cli.git
21
+ ```
22
+
23
+ **For development**:
24
+
25
+ ```bash
26
+ git clone https://github.com/pkonowrocki/allegro-cli.git
27
+ cd allegro-cli
28
+ pip install -e ".[dev]"
29
+ ```
30
+
31
+ ## Setup
32
+
33
+ Import cookies from your browser:
34
+
35
+ ```bash
36
+ allegro login
37
+ ```
38
+
39
+ Paste cookies from Chrome DevTools (Application > Cookies > allegro.pl). Both the DevTools table format and raw cookie header strings are accepted.
40
+
41
+ Alternatively, set cookies directly:
42
+
43
+ ```bash
44
+ allegro config set --cookies 'cookie1=value1; cookie2=value2'
45
+ ```
46
+
47
+ ## Usage
48
+
49
+ ### Search
50
+
51
+ ```bash
52
+ allegro search "laptop"
53
+ allegro search "laptop" --category 491
54
+ allegro search "laptop" --category laptopy-491 --sort pd --price-min 2000 --price-max 5000
55
+ allegro search "laptop" --columns "id,name,sellingMode.price.amount,parameters"
56
+ ```
57
+
58
+ | Flag | Description |
59
+ |------|-------------|
60
+ | `--category` | Category ID or slug (e.g. `491`, `laptopy-491`) |
61
+ | `--sort` | Sort order: `p` (price asc), `pd` (price desc), `m` (relevance), `n` (newest) |
62
+ | `--price-min` | Minimum price in PLN |
63
+ | `--price-max` | Maximum price in PLN |
64
+ | `--page` | Page number (default: 1) |
65
+ | `--columns` | Comma-separated columns to display |
66
+
67
+ ### Offer details
68
+
69
+ ```bash
70
+ allegro offer 12345678
71
+ allegro offer 12345678 --columns "name,sellingMode.price.amount,parameters"
72
+ ```
73
+
74
+ Offer pages include a `parameters` field with product specifications (e.g. processor, RAM, screen size) extracted automatically from the listing.
75
+
76
+ ### Cart
77
+
78
+ ```bash
79
+ allegro cart list
80
+ allegro cart add OFFER_ID SELLER_ID --quantity 2
81
+ allegro cart remove OFFER_ID SELLER_ID
82
+ ```
83
+
84
+ ### Packages
85
+
86
+ ```bash
87
+ allegro packages
88
+ ```
89
+
90
+ ### Configuration
91
+
92
+ ```bash
93
+ allegro config show
94
+ allegro config set --output-format json
95
+ allegro config set --flaresolverr-url http://localhost:8191/v1
96
+ ```
97
+
98
+ ## Output formats
99
+
100
+ All commands support `--format text` (default), `--format json`, and `--format tsv`.
101
+
102
+ ```bash
103
+ allegro search "laptop" --format json # full JSON array
104
+ allegro search "laptop" --format tsv # tab-separated, pipe-friendly
105
+ allegro search "laptop" # aligned text table (default)
106
+ allegro offer 12345678 --format json # full offer with parameters
107
+ ```
108
+
109
+ Use `--columns` to select specific fields (dot-notation supported):
110
+
111
+ ```bash
112
+ allegro search "laptop" --columns "id,name,sellingMode.price.amount"
113
+ allegro offer 12345678 --columns "name,parameters"
114
+ ```
115
+
116
+ Set a persistent default:
117
+
118
+ ```bash
119
+ allegro config set --output-format json
120
+ ```
121
+
122
+ ## Anti-bot handling
123
+
124
+ Allegro uses anti-bot protection (DataDome). The CLI first tries a direct request with your cookies via `curl_cffi` (Chrome TLS fingerprint). If that gets a 403, it falls back to [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr):
125
+
126
+ ```bash
127
+ docker run -d --name flaresolverr -p 8191:8191 ghcr.io/flaresolverr/flaresolverr:latest
128
+ ```
129
+
130
+ ## Development
131
+
132
+ ```bash
133
+ pip install -e ".[dev]"
134
+ pytest
135
+ ```
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
File without changes
@@ -0,0 +1,332 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ import time
5
+ from urllib.parse import urlencode
6
+
7
+ import httpx
8
+ from curl_cffi.requests import Session as CffiSession
9
+
10
+ from allegro_cli.api.models import (
11
+ AllegroCliError,
12
+ AuthenticationError,
13
+ Offer,
14
+ )
15
+ from allegro_cli.config import Config
16
+
17
+ _COMMON_HEADERS = {
18
+ "origin": "https://allegro.pl",
19
+ "referer": "https://allegro.pl/",
20
+ "sec-ch-ua-mobile": "?0",
21
+ "sec-ch-ua-platform": '"Windows"',
22
+ "sec-fetch-dest": "empty",
23
+ "sec-fetch-mode": "cors",
24
+ "sec-fetch-site": "same-site",
25
+ "accept-language": "pl-PL",
26
+ "user-agent": (
27
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
28
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
29
+ "Chrome/144.0.0.0 Safari/537.36"
30
+ ),
31
+ }
32
+
33
+
34
+ class AllegroClient:
35
+ def __init__(self, config: Config, verbose: bool = False):
36
+ self._config = config
37
+ self._verbose = verbose
38
+
39
+ # Edge client for cart/packages — only when cookies are present
40
+ self._edge: httpx.Client | None = None
41
+ self._web: CffiSession | None = None
42
+ if config.cookies:
43
+ self._edge = httpx.Client(
44
+ base_url=config.edgeBaseUrl,
45
+ headers={**_COMMON_HEADERS, "cookie": config.cookies},
46
+ timeout=30.0,
47
+ )
48
+ # curl_cffi session — impersonates Chrome TLS fingerprint to pass Cloudflare
49
+ self._web = CffiSession(impersonate="chrome")
50
+ self._web.headers.update({"cookie": config.cookies})
51
+
52
+ # --- Scrape (allegro.pl, cookie auth) ---
53
+
54
+ def scrape_search(
55
+ self,
56
+ phrase: str,
57
+ page: int = 1,
58
+ category: str | None = None,
59
+ sort: str | None = None,
60
+ price_min: str | None = None,
61
+ price_max: str | None = None,
62
+ ) -> list[Offer]:
63
+ if not self._config.cookies:
64
+ raise AuthenticationError(
65
+ "No cookies configured. Scrape requires browser cookies.\n"
66
+ "Run: allegro login"
67
+ )
68
+
69
+ from allegro_cli.scraper import parse_search_results
70
+
71
+ if category:
72
+ cat_match = re.search(r"(\d+)$", category)
73
+ if cat_match:
74
+ base_url = f"https://allegro.pl/kategoria/-{cat_match.group(1)}"
75
+ else:
76
+ base_url = f"https://allegro.pl/kategoria/{category}"
77
+ else:
78
+ base_url = "https://allegro.pl/listing"
79
+
80
+ params: dict[str, str] = {"string": phrase}
81
+ if page > 1:
82
+ params["p"] = str(page)
83
+ if sort:
84
+ params["order"] = sort
85
+ if price_min:
86
+ params["price_from"] = price_min
87
+ if price_max:
88
+ params["price_to"] = price_max
89
+
90
+ full_url = base_url + "?" + urlencode(params)
91
+
92
+ html = self._fetch_page(full_url)
93
+ return parse_search_results(html)
94
+
95
+ def scrape_offer(self, offer_id: str) -> Offer:
96
+ """Fetch and parse a single offer page by ID."""
97
+ if not self._config.cookies:
98
+ raise AuthenticationError(
99
+ "No cookies configured. Scrape requires browser cookies.\n"
100
+ "Run: allegro login"
101
+ )
102
+
103
+ from allegro_cli.scraper import (
104
+ extract_lazy_contexts,
105
+ parse_offer_page,
106
+ )
107
+
108
+ url = f"https://allegro.pl/oferta/-{offer_id}"
109
+ html = self._fetch_page(url)
110
+ offer = parse_offer_page(html, offer_id=offer_id)
111
+
112
+ # If we only got a few params, try lazy loading the rest
113
+ if len(offer.parameters) < 15:
114
+ contexts = extract_lazy_contexts(html)
115
+ if contexts:
116
+ lazy_params = self._fetch_lazy_parameters(url, contexts)
117
+ for k, v in lazy_params.items():
118
+ offer.parameters.setdefault(k, v)
119
+
120
+ return offer
121
+
122
+ def _fetch_lazy_parameters(
123
+ self, offer_url: str, contexts: list[dict],
124
+ ) -> dict[str, str]:
125
+ """Fetch lazy-loaded parameter groups via the opbox API."""
126
+ from allegro_cli.scraper import parse_opbox_parameters
127
+
128
+ result: dict[str, str] = {}
129
+ max_requests = 3
130
+ for ctx in contexts[:max_requests]:
131
+ lazy_url = f"{offer_url}?lazyContext={ctx['value']}"
132
+ self._log(f"GET {lazy_url} (lazy params)")
133
+ try:
134
+ resp = self._web.get(
135
+ lazy_url,
136
+ headers={
137
+ "Accept": "application/vnd.opbox-web.subtree+json",
138
+ },
139
+ timeout=15,
140
+ )
141
+ except Exception:
142
+ continue
143
+ if resp.status_code != 200:
144
+ continue
145
+ try:
146
+ data = resp.json()
147
+ except (ValueError, Exception):
148
+ continue
149
+ params = parse_opbox_parameters(data)
150
+ for k, v in params.items():
151
+ result.setdefault(k, v)
152
+ if len(result) > 15:
153
+ break
154
+ return result
155
+
156
+ def _fetch_page(self, url: str) -> str:
157
+ # Try direct curl_cffi first
158
+ if self._web:
159
+ self._log(f"GET {url} (direct)")
160
+ t0 = time.monotonic()
161
+ resp = self._web.get(url, timeout=30)
162
+ elapsed = time.monotonic() - t0
163
+ self._log(f"Response: {resp.status_code} ({elapsed:.1f}s)")
164
+
165
+ if resp.status_code == 200:
166
+ return resp.text
167
+
168
+ if resp.status_code == 401:
169
+ raise AuthenticationError("Session expired (401). Run: allegro login")
170
+
171
+ # 403 = DataDome challenge — fall through to FlareSolverr
172
+ if resp.status_code != 403:
173
+ raise AllegroCliError(
174
+ message=f"Scrape returned {resp.status_code}: {resp.text[:300]}",
175
+ code="ScrapeException",
176
+ userMessage=f"Could not fetch search page ({resp.status_code}).",
177
+ )
178
+ self._log("Direct fetch got 403 (DataDome), trying FlareSolverr...")
179
+
180
+ # Fall back to FlareSolverr
181
+ return self._fetch_via_flaresolverr(url)
182
+
183
+ def _fetch_via_flaresolverr(self, url: str) -> str:
184
+ fs_url = self._config.flareSolverrUrl
185
+ if not fs_url:
186
+ # Auto-detect on default port
187
+ fs_url = "http://localhost:8191/v1"
188
+
189
+ self._log(f"FlareSolverr POST {fs_url}")
190
+ t0 = time.monotonic()
191
+
192
+ try:
193
+ resp = httpx.post(
194
+ fs_url,
195
+ json={"cmd": "request.get", "url": url, "maxTimeout": 60000},
196
+ timeout=90.0,
197
+ )
198
+ except httpx.ConnectError:
199
+ raise AllegroCliError(
200
+ message=f"Cannot connect to FlareSolverr at {fs_url}",
201
+ code="FlareSolverrUnavailable",
202
+ userMessage=(
203
+ "Direct fetch blocked by anti-bot (403) and FlareSolverr "
204
+ "is not running.\n"
205
+ "Start it with:\n"
206
+ " docker run -d --name flaresolverr -p 8191:8191 "
207
+ "ghcr.io/flaresolverr/flaresolverr:latest\n"
208
+ "Or refresh your cookies:\n"
209
+ " allegro login"
210
+ ),
211
+ )
212
+
213
+ elapsed = time.monotonic() - t0
214
+ self._log(f"FlareSolverr response: {resp.status_code} ({elapsed:.1f}s)")
215
+
216
+ if resp.status_code != 200:
217
+ raise AllegroCliError(
218
+ message=f"FlareSolverr returned {resp.status_code}: {resp.text[:300]}",
219
+ code="FlareSolverrError",
220
+ userMessage="FlareSolverr returned an error.",
221
+ )
222
+
223
+ data = resp.json()
224
+ if data.get("status") != "ok":
225
+ raise AllegroCliError(
226
+ message=f"FlareSolverr error: {data.get('message', 'unknown')}",
227
+ code="FlareSolverrError",
228
+ userMessage=f"FlareSolverr: {data.get('message', 'unknown error')}",
229
+ )
230
+
231
+ solution = data.get("solution", {})
232
+ sol_status = solution.get("status", 0)
233
+ if sol_status >= 400:
234
+ raise AllegroCliError(
235
+ message=f"FlareSolverr got {sol_status} from target",
236
+ code="ScrapeException",
237
+ userMessage=f"Could not fetch search page ({sol_status}).",
238
+ )
239
+
240
+ return solution.get("response", "")
241
+
242
+ # --- Cart (edge.allegro.pl, cookie auth) ---
243
+
244
+ def _require_edge(self) -> httpx.Client:
245
+ if not self._edge:
246
+ raise AuthenticationError(
247
+ "No cookies configured. Cart/packages require browser cookies.\n"
248
+ "Run: allegro login"
249
+ )
250
+ return self._edge
251
+
252
+ def get_cart(self) -> dict:
253
+ resp = self._request(
254
+ "GET", "/carts",
255
+ accept="application/vnd.allegro.internal.v6+json",
256
+ )
257
+ return resp.json()
258
+
259
+ def change_cart_quantity(
260
+ self,
261
+ item_id: str,
262
+ delta: int,
263
+ seller_id: str,
264
+ nav_category_id: str | None = None,
265
+ ) -> None:
266
+ body = {
267
+ "items": [
268
+ {
269
+ "itemId": item_id,
270
+ "delta": delta,
271
+ "sellerId": seller_id,
272
+ **({"navCategoryId": nav_category_id} if nav_category_id else {}),
273
+ "navTree": "navigation-pl",
274
+ }
275
+ ]
276
+ }
277
+ self._request(
278
+ "POST",
279
+ "/carts/changeQuantityCommand",
280
+ json=body,
281
+ accept="application/vnd.allegro.public.v5+json",
282
+ content_type="application/vnd.allegro.public.v5+json",
283
+ )
284
+
285
+ # --- Packages / delivery ---
286
+
287
+ def get_packages_summary(self) -> dict:
288
+ resp = self._request(
289
+ "GET", "/packages/summary",
290
+ accept="application/vnd.allegro.internal.v1+json",
291
+ )
292
+ return resp.json()
293
+
294
+ # --- HTTP layer (edge API, cookie auth) ---
295
+
296
+ def _request(
297
+ self,
298
+ method: str,
299
+ path: str,
300
+ accept: str = "application/vnd.allegro.internal.v1+json",
301
+ content_type: str | None = None,
302
+ **kwargs,
303
+ ) -> httpx.Response:
304
+ edge = self._require_edge()
305
+ headers = {"accept": accept}
306
+ if content_type:
307
+ headers["content-type"] = content_type
308
+
309
+ resp = edge.request(method, path, headers=headers, **kwargs)
310
+
311
+ if resp.status_code == 401:
312
+ raise AuthenticationError(
313
+ "Session expired (401). Run: allegro login"
314
+ )
315
+ if resp.status_code == 403:
316
+ raise AllegroCliError(
317
+ message="Forbidden (403)",
318
+ code="ForbiddenException",
319
+ userMessage="Access denied. Your session cookies may have expired.",
320
+ )
321
+ if resp.status_code >= 400 and resp.status_code != 204:
322
+ raise AllegroCliError(
323
+ message=f"API returned {resp.status_code}: {resp.text[:300]}",
324
+ code="ApiException",
325
+ userMessage=f"Allegro API error ({resp.status_code}).",
326
+ )
327
+ return resp
328
+
329
+ def _log(self, msg: str) -> None:
330
+ if self._verbose:
331
+ import sys
332
+ print(msg, file=sys.stderr, flush=True)
@@ -0,0 +1,88 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ # --- Data models (Allegro REST API conventions) ---
7
+
8
+ @dataclass
9
+ class Price:
10
+ amount: str
11
+ currency: str = "PLN"
12
+
13
+
14
+ @dataclass
15
+ class Seller:
16
+ id: str
17
+ name: str
18
+
19
+
20
+ @dataclass
21
+ class Category:
22
+ id: str
23
+ name: str | None = None
24
+
25
+
26
+ @dataclass
27
+ class Image:
28
+ url: str
29
+
30
+
31
+ @dataclass
32
+ class SellingMode:
33
+ format: str # BUY_NOW, AUCTION, ADVERTISEMENT
34
+ price: Price
35
+ popularity: int | None = None
36
+
37
+
38
+ @dataclass
39
+ class DeliveryInfo:
40
+ lowestPrice: Price | None = None
41
+ availableForFree: bool = False
42
+
43
+
44
+ @dataclass
45
+ class Stock:
46
+ unit: str = "UNIT"
47
+ available: int = 0
48
+
49
+
50
+ @dataclass
51
+ class Offer:
52
+ id: str
53
+ name: str
54
+ seller: Seller
55
+ sellingMode: SellingMode
56
+ category: Category
57
+ images: list[Image] = field(default_factory=list)
58
+ delivery: DeliveryInfo | None = None
59
+ stock: Stock | None = None
60
+ parameters: dict[str, str] = field(default_factory=dict)
61
+
62
+
63
+ # --- Exceptions ---
64
+
65
+ class AllegroCliError(Exception):
66
+ def __init__(
67
+ self,
68
+ message: str,
69
+ code: str,
70
+ path: str | None = None,
71
+ userMessage: str | None = None,
72
+ ):
73
+ self.message = message
74
+ self.code = code
75
+ self.path = path
76
+ self.userMessage = userMessage or message
77
+ super().__init__(message)
78
+
79
+
80
+ class AuthenticationError(AllegroCliError):
81
+ def __init__(self, message: str = "Authentication failed"):
82
+ super().__init__(
83
+ message=message,
84
+ code="AuthenticationException",
85
+ userMessage="Could not authenticate. Check your client-id and client-secret.",
86
+ )
87
+
88
+