amazon-orders 4.3.1__tar.gz → 4.4.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 (42) hide show
  1. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/CHANGELOG.md +16 -1
  2. {amazon_orders-4.3.1/amazon_orders.egg-info → amazon_orders-4.4.0}/PKG-INFO +6 -7
  3. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/README.md +1 -1
  4. {amazon_orders-4.3.1 → amazon_orders-4.4.0/amazon_orders.egg-info}/PKG-INFO +6 -7
  5. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/requires.txt +0 -3
  6. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/__init__.py +1 -1
  7. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/conf.py +2 -0
  8. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/constants.py +2 -0
  9. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/browser/playwright.py +146 -18
  10. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/anticaptcha.py +1 -1
  11. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/base.py +19 -4
  12. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/capsolver.py +52 -2
  13. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/twocaptcha.py +1 -1
  14. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/order.py +1 -1
  15. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/selectors.py +7 -0
  16. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/pyproject.toml +17 -6
  17. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/LICENSE +0 -0
  18. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/MANIFEST.in +0 -0
  19. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/SOURCES.txt +0 -0
  20. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/dependency_links.txt +0 -0
  21. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/entry_points.txt +0 -0
  22. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/top_level.txt +0 -0
  23. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/banner.txt +0 -0
  24. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/cli.py +0 -0
  25. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/__init__.py +0 -0
  26. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/browser/__init__.py +0 -0
  27. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/__init__.py +0 -0
  28. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/__init__.py +0 -0
  29. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/item.py +0 -0
  30. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/parsable.py +0 -0
  31. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/recipient.py +0 -0
  32. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/seller.py +0 -0
  33. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/shipment.py +0 -0
  34. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/transaction.py +0 -0
  35. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/exception.py +0 -0
  36. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/forms.py +0 -0
  37. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/orders.py +0 -0
  38. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/session.py +0 -0
  39. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/transactions.py +0 -0
  40. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/util.py +0 -0
  41. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/setup.cfg +0 -0
  42. {amazon_orders-4.3.1 → amazon_orders-4.4.0}/tests/testcase.py +0 -0
@@ -4,7 +4,17 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
- ## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.3.1...HEAD)
7
+ ## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.4.0...HEAD)
8
+
9
+ ## [4.4.0](https://github.com/alexdlaird/amazon-orders/compare/4.3.1...4.4.0) - 2026-06-12
10
+
11
+ ### Added
12
+
13
+ - `PlaywrightAcicForm` now solves visual grid Puzzles (e.g. "Choose all the buckets") automatically when a WAF solver extra is configured. Puzzles are detected and solved in a loop alongside the existing WAF token challenge, handling any combination Amazon presents.
14
+ - `AwsWafForm._solve_visual_captcha()` subclass hook for Puzzle classification. `CapSolverWafForm` implements this via the `AwsWafClassification` task type.
15
+ - `browser_timeout` config key (defaults to 30s) controlling how long the browser waits for challenge elements to appear.
16
+ - `retry_failures` input on the integration test workflow for fail-fast CI runs.
17
+ - Build and stability improvements.
8
18
 
9
19
  ## [4.3.1](https://github.com/alexdlaird/amazon-orders/compare/4.3.0...4.3.1) - 2026-06-07
10
20
 
@@ -13,6 +23,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
13
23
  - `AmazonOrders.get_invoice()` to fetch an Order's print-friendly invoice page, returning the response (including its parsed HTML) for rendering or printing.
14
24
  - `AmazonTransactions.get_transactions()` `order_id` parameter to scope results to a single Order server-side via Amazon's `transactionTag` filter, bypassing the `days` window.
15
25
 
26
+ ### Added
27
+
28
+ - `AmazonOrders.get_invoice()` to fetch an Order's print-friendly invoice page, returning the response (including its parsed HTML) for rendering or printing.
29
+ - `AmazonTransactions.get_transactions()` `order_id` parameter to scope results to a single Order server-side via Amazon's `transactionTag` filter, bypassing the `days` window.
30
+
16
31
  ## [4.3.0](https://github.com/alexdlaird/amazon-orders/compare/4.2.2...4.3.0) - 2026-06-07
17
32
 
18
33
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.3.1
3
+ Version: 4.4.0
4
4
  Summary: A Python library (and CLI) for Amazon order history
5
5
  Author-email: Alex Laird <contact@alexlaird.com>
6
6
  Maintainer-email: Alex Laird <contact@alexlaird.com>
@@ -35,18 +35,18 @@ Requires-Dist: beautifulsoup4>=4.12
35
35
  Requires-Dist: PyYAML>=6.0.3
36
36
  Requires-Dist: python-dateutil>=2.9.0.post0
37
37
  Requires-Dist: pyotp>=2.9
38
- Provides-Extra: captcha
39
- Requires-Dist: amazoncaptcha>=0.4; extra == "captcha"
38
+ Provides-Extra: browser
39
+ Requires-Dist: playwright>=1.47.0; extra == "browser"
40
40
  Provides-Extra: capsolver
41
41
  Requires-Dist: capsolver; extra == "capsolver"
42
42
  Provides-Extra: anticaptcha
43
43
  Requires-Dist: anticaptchaofficial; extra == "anticaptcha"
44
44
  Provides-Extra: 2captcha
45
45
  Requires-Dist: 2captcha-python; extra == "2captcha"
46
- Provides-Extra: browser
47
- Requires-Dist: playwright>=1.47.0; extra == "browser"
48
46
  Provides-Extra: lxml
49
47
  Requires-Dist: lxml; extra == "lxml"
48
+ Provides-Extra: captcha
49
+ Requires-Dist: amazoncaptcha>=0.4; extra == "captcha"
50
50
  Provides-Extra: dev
51
51
  Requires-Dist: pytest; extra == "dev"
52
52
  Requires-Dist: coverage[toml]; extra == "dev"
@@ -55,7 +55,6 @@ Requires-Dist: flake8-pyproject; extra == "dev"
55
55
  Requires-Dist: pep8-naming; extra == "dev"
56
56
  Requires-Dist: responses; extra == "dev"
57
57
  Requires-Dist: lxml; extra == "dev"
58
- Requires-Dist: amazoncaptcha>=0.4; python_version < "3.13" and extra == "dev"
59
58
  Provides-Extra: integration
60
59
  Requires-Dist: pytest-rerunfailures; extra == "integration"
61
60
  Requires-Dist: parameterized; extra == "integration"
@@ -103,7 +102,7 @@ pip install amazon-orders --upgrade
103
102
 
104
103
  That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
105
104
 
106
- If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.3.*`, not `==4.2.1`) to
105
+ If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.4.*`, not `==4.2.1`) to
107
106
  ensure you always get the latest stable release.
108
107
 
109
108
  ## Basic Usage
@@ -28,7 +28,7 @@ pip install amazon-orders --upgrade
28
28
 
29
29
  That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
30
30
 
31
- If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.3.*`, not `==4.2.1`) to
31
+ If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.4.*`, not `==4.2.1`) to
32
32
  ensure you always get the latest stable release.
33
33
 
34
34
  ## Basic Usage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.3.1
3
+ Version: 4.4.0
4
4
  Summary: A Python library (and CLI) for Amazon order history
5
5
  Author-email: Alex Laird <contact@alexlaird.com>
6
6
  Maintainer-email: Alex Laird <contact@alexlaird.com>
@@ -35,18 +35,18 @@ Requires-Dist: beautifulsoup4>=4.12
35
35
  Requires-Dist: PyYAML>=6.0.3
36
36
  Requires-Dist: python-dateutil>=2.9.0.post0
37
37
  Requires-Dist: pyotp>=2.9
38
- Provides-Extra: captcha
39
- Requires-Dist: amazoncaptcha>=0.4; extra == "captcha"
38
+ Provides-Extra: browser
39
+ Requires-Dist: playwright>=1.47.0; extra == "browser"
40
40
  Provides-Extra: capsolver
41
41
  Requires-Dist: capsolver; extra == "capsolver"
42
42
  Provides-Extra: anticaptcha
43
43
  Requires-Dist: anticaptchaofficial; extra == "anticaptcha"
44
44
  Provides-Extra: 2captcha
45
45
  Requires-Dist: 2captcha-python; extra == "2captcha"
46
- Provides-Extra: browser
47
- Requires-Dist: playwright>=1.47.0; extra == "browser"
48
46
  Provides-Extra: lxml
49
47
  Requires-Dist: lxml; extra == "lxml"
48
+ Provides-Extra: captcha
49
+ Requires-Dist: amazoncaptcha>=0.4; extra == "captcha"
50
50
  Provides-Extra: dev
51
51
  Requires-Dist: pytest; extra == "dev"
52
52
  Requires-Dist: coverage[toml]; extra == "dev"
@@ -55,7 +55,6 @@ Requires-Dist: flake8-pyproject; extra == "dev"
55
55
  Requires-Dist: pep8-naming; extra == "dev"
56
56
  Requires-Dist: responses; extra == "dev"
57
57
  Requires-Dist: lxml; extra == "dev"
58
- Requires-Dist: amazoncaptcha>=0.4; python_version < "3.13" and extra == "dev"
59
58
  Provides-Extra: integration
60
59
  Requires-Dist: pytest-rerunfailures; extra == "integration"
61
60
  Requires-Dist: parameterized; extra == "integration"
@@ -103,7 +102,7 @@ pip install amazon-orders --upgrade
103
102
 
104
103
  That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
105
104
 
106
- If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.3.*`, not `==4.2.1`) to
105
+ If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.4.*`, not `==4.2.1`) to
107
106
  ensure you always get the latest stable release.
108
107
 
109
108
  ## Basic Usage
@@ -30,9 +30,6 @@ pep8-naming
30
30
  responses
31
31
  lxml
32
32
 
33
- [dev:python_version < "3.13"]
34
- amazoncaptcha>=0.4
35
-
36
33
  [docs]
37
34
  Sphinx
38
35
  sphinx-notfound-page
@@ -1,3 +1,3 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
- __version__ = "4.3.1"
3
+ __version__ = "4.4.0"
@@ -58,6 +58,8 @@ class AmazonOrdersConfig:
58
58
  "item_class": "amazonorders.entity.item.Item",
59
59
  "bs4_parser": "html.parser",
60
60
  "auth_forms_classes": [],
61
+ # Timeout in seconds for browser-based challenge detection and resolution
62
+ "browser_timeout": 30,
61
63
  "thread_pool_size": (os.cpu_count() or 1) * 4,
62
64
  "connection_pool_size": thread_pool_size * 2,
63
65
  # The maximum number of failed attempts to allow before failing CLI authentication
@@ -148,6 +148,8 @@ class Constants:
148
148
 
149
149
  COOKIES_SET_WHEN_AUTHENTICATED = ["x-main"]
150
150
  JS_ROBOT_TEXT_REGEX = r"[.\s\S]*verify that you're not a robot[.\s\S]*Enable JavaScript[.\s\S]*"
151
+ GOKU_PROPS_REGEX = r"window\.gokuProps\s*=\s*(\{.*?\});"
152
+ ACIC_CHALLENGE_PATH = "/ax/aaut/verify/ap/challenge"
151
153
 
152
154
  ##########################################################################
153
155
  # Currency
@@ -1,6 +1,7 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
3
 
4
+ import base64
4
5
  import json
5
6
  import logging
6
7
  import os
@@ -13,7 +14,6 @@ from bs4 import Tag
13
14
  from requests import Response
14
15
 
15
16
  from amazonorders.conf import AmazonOrdersConfig
16
- from amazonorders.contrib.waf.base import _GOKU_PROPS_RE
17
17
  from amazonorders.exception import AmazonOrdersError
18
18
  from amazonorders.forms import AuthForm
19
19
  from amazonorders.util import AmazonSessionResponse
@@ -81,8 +81,6 @@ class PlaywrightAuthForm(AuthForm):
81
81
  ) from e
82
82
 
83
83
  debug = self.amazon_session.debug
84
- if debug:
85
- logger.setLevel(logging.DEBUG)
86
84
 
87
85
  message = "Info: A browser is handling a JavaScript authentication challenge."
88
86
  logger.info(message)
@@ -113,7 +111,7 @@ class PlaywrightAuthForm(AuthForm):
113
111
  try:
114
112
  page.wait_for_url(
115
113
  lambda url: not self._is_challenge_url(url, original_url),
116
- timeout=30000
114
+ timeout=self.config.browser_timeout * 1000
117
115
  )
118
116
  except PlaywrightTimeoutError as e:
119
117
  logger.debug(f"Browser timed out at URL: {page.url}")
@@ -137,7 +135,7 @@ class PlaywrightAuthForm(AuthForm):
137
135
  """
138
136
  Hook called after navigating to the challenge page and saving the initial
139
137
  snapshot, but before waiting for the challenge URL to resolve. Override in
140
- subclasses to take additional action (e.g. solving an embedded CAPTCHA).
138
+ subclasses to take additional action (e.g. solving an embedded Puzzle).
141
139
 
142
140
  :param page: The Playwright ``Page`` currently on the challenge URL.
143
141
  :param context: The Playwright ``BrowserContext``.
@@ -226,10 +224,20 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
226
224
  :return: ``True`` if an ACIC challenge was detected, ``False`` otherwise.
227
225
  """
228
226
  self.amazon_session = amazon_session
229
- return bool(parsed.find(id="aa-challenge-page-captcha-container"))
227
+ return bool(parsed.select_one(self.config.selectors.ACIC_CHALLENGE_SELECTOR))
230
228
 
231
229
  def _on_challenge_page(self, page: Any, context: Any, output_dir: Optional[str]) -> None:
232
- self._try_solve_embedded_waf(page, context, output_dir)
230
+ max_solves = self.config.max_auth_attempts
231
+ solves = 0
232
+ while solves < max_solves:
233
+ if not self._is_challenge_url(page.url, page.url):
234
+ break
235
+ if self._try_solve_embedded_waf(page, context, output_dir):
236
+ solves += 1
237
+ elif self._try_solve_visual_captcha(page, context, output_dir):
238
+ solves += 1
239
+ else:
240
+ break
233
241
 
234
242
  def _try_solve_embedded_waf(self, page: Any, context: Any, output_dir: Optional[str]) -> bool:
235
243
  """
@@ -249,9 +257,12 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
249
257
  return False # pragma: no cover
250
258
 
251
259
  try:
252
- page.wait_for_function("() => typeof window.gokuProps !== 'undefined'", timeout=8000)
260
+ page.wait_for_function(
261
+ "() => typeof window.gokuProps !== 'undefined'",
262
+ timeout=self.config.browser_timeout * 1000
263
+ )
253
264
  except PlaywrightTimeoutError:
254
- logger.debug("No window.gokuProps in ACIC page — no embedded WAF challenge to solve.")
265
+ logger.debug("No window.gokuProps detected in ACIC page.")
255
266
  return False
256
267
 
257
268
  try:
@@ -265,7 +276,6 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
265
276
  return False
266
277
 
267
278
  if not goku or not challenge_script:
268
- logger.debug("Incomplete WAF props in ACIC page — goku=%r, script=%r.", goku, challenge_script)
269
279
  return False
270
280
 
271
281
  if not self.amazon_session:
@@ -277,13 +287,13 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
277
287
  None,
278
288
  )
279
289
  if not waf_form:
280
- logger.debug("Embedded WAF challenge found but no AwsWafForm configured skipping solve.")
290
+ logger.debug("No AwsWafForm configured, skipping WAF challenge.")
281
291
  return False
282
292
 
283
293
  try:
284
294
  token = waf_form._solve_token(page.url, goku, challenge_script)
285
295
  except Exception:
286
- logger.debug("WAF solver raised an exception solving embedded ACIC CAPTCHA.", exc_info=True)
296
+ logger.info("WAF solver failed to solve embedded WAF challenge.", exc_info=True)
287
297
  return False
288
298
 
289
299
  message = f"Info: Solved embedded WAF challenge via {waf_form.PROVIDER_NAME}."
@@ -298,12 +308,129 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
298
308
  "path": "/",
299
309
  }])
300
310
  page.reload(wait_until="load")
301
- logger.debug("Reloaded ACIC page after WAF token injection.")
302
311
  self._save_debug_snapshot(page, output_dir, "browser-waf-injected")
303
312
  return True
304
313
 
314
+ def _try_solve_visual_captcha(self, page: Any, context: Any, output_dir: Optional[str]) -> bool:
315
+ """
316
+ If the ACIC challenge page contains a visual grid Puzzle rendered by
317
+ ``CaptchaScript.renderCaptcha``, extract the challenge images and question,
318
+ solve it via the configured :class:`~amazonorders.contrib.waf.base.AwsWafForm`,
319
+ and submit the answer.
320
+
321
+ :param page: The Playwright ``Page`` on the ACIC challenge URL.
322
+ :param context: The Playwright ``BrowserContext``.
323
+ :param output_dir: Directory for debug snapshots, or ``None``.
324
+ :return: ``True`` if the Puzzle was solved and submitted, ``False`` otherwise.
325
+ """
326
+ try:
327
+ from playwright.sync_api import TimeoutError as PlaywrightTimeoutError # type: ignore[import-not-found]
328
+ except ImportError:
329
+ return False # pragma: no cover
330
+
331
+ canvas_selector = self.config.selectors.ACIC_VISUAL_CAPTCHA_CANVAS_SELECTOR
332
+ try:
333
+ page.wait_for_selector(canvas_selector, state="attached",
334
+ timeout=self.config.browser_timeout * 1000)
335
+ except PlaywrightTimeoutError:
336
+ logger.debug("No Puzzle canvas detected on ACIC page.")
337
+ return False
338
+
339
+ if not self.amazon_session:
340
+ return False # pragma: no cover
341
+
342
+ from amazonorders.contrib.waf.base import AwsWafForm
343
+ waf_form = next(
344
+ (f for f in self.amazon_session.auth_forms if isinstance(f, AwsWafForm)),
345
+ None,
346
+ )
347
+ if not waf_form:
348
+ logger.debug("No AwsWafForm configured, skipping Puzzle.")
349
+ return False
350
+
351
+ question_selector = self.config.selectors.ACIC_VISUAL_CAPTCHA_QUESTION_SELECTOR
352
+ try:
353
+ canvas_locator = page.locator(canvas_selector).first
354
+ box = canvas_locator.bounding_box()
355
+ cols, rows = 3, 3
356
+ tw = box["width"] / cols
357
+ th = box["height"] / rows
358
+ challenge_images = []
359
+ for r in range(rows):
360
+ for c in range(cols):
361
+ tile_bytes = page.screenshot(
362
+ clip={"x": box["x"] + c * tw, "y": box["y"] + r * th,
363
+ "width": tw, "height": th}
364
+ )
365
+ challenge_images.append(base64.b64encode(tile_bytes).decode())
366
+
367
+ question = page.evaluate(
368
+ "(sel) => {"
369
+ " const el = document.querySelector(sel);"
370
+ " return el ? el.textContent.trim() : null;"
371
+ "}",
372
+ question_selector,
373
+ )
374
+ except Exception:
375
+ logger.debug("Failed to extract Puzzle data from page.", exc_info=True)
376
+ return False
377
+
378
+ if not challenge_images or not question:
379
+ return False
380
+
381
+ try:
382
+ answer = waf_form._solve_visual_captcha(page.url, challenge_images, question)
383
+ except Exception:
384
+ logger.info("WAF solver failed to solve Puzzle.", exc_info=True)
385
+ return False
386
+
387
+ if answer is None:
388
+ logger.info(f"{waf_form.PROVIDER_NAME} does not support Puzzle solving.")
389
+ return False
390
+
391
+ message = f"Info: Puzzle solved via {waf_form.PROVIDER_NAME}."
392
+ logger.info(message)
393
+ self.amazon_session.io.echo(message)
394
+
395
+ verify_selector = self.config.selectors.ACIC_VISUAL_CAPTCHA_VERIFY_SELECTOR
396
+ try:
397
+ indices = answer if isinstance(answer[0], int) else [
398
+ i for i, v in enumerate(answer) if v]
399
+
400
+ page.evaluate(
401
+ "([sel, indices]) => {"
402
+ " const canvas = document.querySelector(sel);"
403
+ " if (!canvas) return;"
404
+ " const rect = canvas.getBoundingClientRect();"
405
+ " const cols = 3, rows = 3;"
406
+ " const tw = rect.width / cols, th = rect.height / rows;"
407
+ " indices.forEach(idx => {"
408
+ " const r = Math.floor(idx / cols), c = idx % cols;"
409
+ " const x = c * tw + tw / 2, y = r * th + th / 2;"
410
+ " const evt = new MouseEvent('click', {"
411
+ " bubbles: true, clientX: rect.left + x, clientY: rect.top + y"
412
+ " });"
413
+ " canvas.dispatchEvent(evt);"
414
+ " });"
415
+ "}",
416
+ [canvas_selector, indices],
417
+ )
418
+
419
+ page.locator(verify_selector).click()
420
+ except Exception:
421
+ logger.debug("Failed to submit Puzzle answer.", exc_info=True)
422
+ return False
423
+
424
+ try:
425
+ page.wait_for_load_state("load", timeout=10000)
426
+ except PlaywrightTimeoutError:
427
+ pass
428
+
429
+ self._save_debug_snapshot(page, output_dir, "browser-visual-captcha-solved")
430
+ return True
431
+
305
432
  def _is_challenge_url(self, url: str, original_url: str) -> bool:
306
- return "/ax/aaut/verify/ap/challenge" in url
433
+ return self.config.constants.ACIC_CHALLENGE_PATH in url
307
434
 
308
435
 
309
436
  class PlaywrightJSAuthForm(PlaywrightAuthForm):
@@ -350,7 +477,7 @@ class PlaywrightJSAuthForm(PlaywrightAuthForm):
350
477
  class PlaywrightManualWafForm(PlaywrightAuthForm):
351
478
  """
352
479
  Handles Amazon's AWS WAF JavaScript challenge by opening a **visible** browser
353
- window so the user can solve the CAPTCHA manually. Once the challenge
480
+ window so the user can solve the Puzzle manually. Once the challenge
354
481
  resolves and the browser navigates away, cookies are harvested back into the
355
482
  session automatically.
356
483
 
@@ -389,7 +516,8 @@ class PlaywrightManualWafForm(PlaywrightAuthForm):
389
516
  """
390
517
  self.amazon_session = amazon_session
391
518
 
392
- match = _GOKU_PROPS_RE.search(str(parsed))
519
+ goku_re = re.compile(self.config.constants.GOKU_PROPS_REGEX, re.DOTALL)
520
+ match = goku_re.search(str(parsed))
393
521
  if not match:
394
522
  return False
395
523
  try:
@@ -397,12 +525,12 @@ class PlaywrightManualWafForm(PlaywrightAuthForm):
397
525
  except (json.JSONDecodeError, ValueError):
398
526
  return False
399
527
 
400
- challenge_tag = parsed.select_one('script[src*="awswaf.com"]')
528
+ challenge_tag = parsed.select_one(self.config.selectors.AWS_WAF_CHALLENGE_SCRIPT_SELECTOR)
401
529
  return challenge_tag is not None and isinstance(challenge_tag.get("src"), str)
402
530
 
403
531
  def _on_challenge_page(self, page: Any, context: Any, output_dir: Optional[str]) -> None:
404
532
  message = (
405
- "Info: A browser window has opened — solve the CAPTCHA, then return here when done."
533
+ "Info: A browser window has opened — solve the Puzzle, then return here when done."
406
534
  )
407
535
  logger.info(message)
408
536
  if self.amazon_session:
@@ -36,7 +36,7 @@ class AntiCaptchaWafForm(AwsWafForm):
36
36
  not installed, or if Anti-Captcha returns no token.
37
37
  """
38
38
  try:
39
- from anticaptchaofficial.amazonproxyless import amazonProxyless # type: ignore[import-untyped]
39
+ from anticaptchaofficial.amazonproxyless import amazonProxyless
40
40
  except ImportError as e:
41
41
  raise AmazonOrdersError(
42
42
  "AntiCaptchaWafForm requires the 'anticaptchaofficial' package. "
@@ -21,8 +21,6 @@ if TYPE_CHECKING:
21
21
 
22
22
  logger = logging.getLogger(__name__)
23
23
 
24
- _GOKU_PROPS_RE = re.compile(r"window\.gokuProps\s*=\s*(\{.*?\});", re.DOTALL)
25
-
26
24
 
27
25
  class AwsWafForm(AuthForm):
28
26
  """
@@ -77,7 +75,8 @@ class AwsWafForm(AuthForm):
77
75
  """
78
76
  self.amazon_session = amazon_session
79
77
 
80
- match = _GOKU_PROPS_RE.search(str(parsed))
78
+ goku_re = re.compile(self.config.constants.GOKU_PROPS_REGEX, re.DOTALL)
79
+ match = goku_re.search(str(parsed))
81
80
  if not match:
82
81
  return False
83
82
  try:
@@ -85,7 +84,7 @@ class AwsWafForm(AuthForm):
85
84
  except json.JSONDecodeError:
86
85
  return False
87
86
 
88
- challenge_tag = parsed.select_one('script[src*="awswaf.com"]')
87
+ challenge_tag = parsed.select_one(self.config.selectors.AWS_WAF_CHALLENGE_SCRIPT_SELECTOR)
89
88
  if not challenge_tag:
90
89
  return False
91
90
  src = challenge_tag.get("src")
@@ -147,3 +146,19 @@ class AwsWafForm(AuthForm):
147
146
  :raises NotImplementedError: if a subclass does not override this method.
148
147
  """
149
148
  raise NotImplementedError
149
+
150
+ def _solve_visual_captcha(self,
151
+ url: str,
152
+ image_data: list,
153
+ question: str) -> Optional[list]:
154
+ """
155
+ Subclass hook. Solve a visual grid Puzzle (e.g. "Choose all the buckets")
156
+ and return the indices of the correct grid cells.
157
+
158
+ :param url: The URL of the page containing the Puzzle.
159
+ :param image_data: List of base64-encoded data URLs, one per grid tile.
160
+ :param question: The object to identify (e.g. ``"the buckets"``).
161
+ :return: A list of zero-based grid cell indices to select, or ``None`` if
162
+ this solver does not support Puzzle classification.
163
+ """
164
+ return None
@@ -1,7 +1,7 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
3
 
4
- from typing import Any, Dict
4
+ from typing import Any, Dict, List, Optional
5
5
 
6
6
  from amazonorders.contrib.waf.base import AwsWafForm
7
7
  from amazonorders.exception import AmazonOrdersError
@@ -36,7 +36,7 @@ class CapSolverWafForm(AwsWafForm):
36
36
  ``cookie`` field.
37
37
  """
38
38
  try:
39
- import capsolver # type: ignore[import-untyped]
39
+ import capsolver
40
40
  except ImportError as e:
41
41
  raise AmazonOrdersError(
42
42
  "CapSolverWafForm requires the 'capsolver' package. "
@@ -65,3 +65,53 @@ class CapSolverWafForm(AwsWafForm):
65
65
  raise AmazonOrdersError(
66
66
  f"Unexpected CapSolver response (missing 'cookie'): {solution!r}"
67
67
  ) from e
68
+
69
+ def _solve_visual_captcha(self,
70
+ url: str,
71
+ image_data: List[str],
72
+ question: str) -> Optional[List[int]]:
73
+ """
74
+ Solve a visual grid Puzzle via CapSolver's ``AwsWafClassification`` task
75
+ type and return the indices of the correct grid cells.
76
+
77
+ :param url: The URL of the page containing the Puzzle.
78
+ :param image_data: List of base64-encoded data URLs, one per grid tile.
79
+ :param question: The object to identify (e.g. ``"the buckets"``).
80
+ :return: A list of zero-based grid cell indices to select.
81
+ :raises AmazonOrdersError: if the ``capsolver`` package is not installed,
82
+ or if CapSolver's response does not contain the expected fields.
83
+ """
84
+ try:
85
+ import capsolver
86
+ except ImportError as e:
87
+ raise AmazonOrdersError(
88
+ "CapSolverWafForm requires the 'capsolver' package. "
89
+ "Install it with: `pip install amazon-orders[capsolver]`"
90
+ ) from e
91
+
92
+ capsolver.api_key = self.api_key
93
+
94
+ images_b64 = []
95
+ for img in image_data:
96
+ images_b64.append(img.split(",", 1)[1] if "," in img else img)
97
+
98
+ question_id = f"aws:grid:{question.replace('the ', '').rstrip('s')}"
99
+
100
+ try:
101
+ solution = capsolver.solve({
102
+ "type": "AwsWafClassification",
103
+ "websiteURL": url,
104
+ "images": images_b64,
105
+ "question": question_id,
106
+ })
107
+ except Exception as e:
108
+ raise AmazonOrdersError(
109
+ f"CapSolver failed to solve Puzzle: {e}"
110
+ ) from e
111
+
112
+ try:
113
+ return solution["objects"]
114
+ except (KeyError, TypeError) as e:
115
+ raise AmazonOrdersError(
116
+ f"Unexpected CapSolver Puzzle response (missing 'objects'): {solution!r}"
117
+ ) from e
@@ -39,7 +39,7 @@ class TwoCaptchaWafForm(AwsWafForm):
39
39
  expected ``existing_token`` field.
40
40
  """
41
41
  try:
42
- from twocaptcha import TwoCaptcha # type: ignore[import-untyped]
42
+ from twocaptcha import TwoCaptcha
43
43
  except ImportError as e:
44
44
  raise AmazonOrdersError(
45
45
  "TwoCaptchaWafForm requires the '2captcha-python' package. "
@@ -40,7 +40,7 @@ class Order(Parsable):
40
40
  #: If the Orders full details were populated from its details page.
41
41
  self.full_details: bool = full_details
42
42
 
43
- #: Where the Order appeared in the history when it was queried. This will inevitably change (ex. when a new
43
+ #: Where the Order appeared in the history when it was queried. This will inevitably change (e.g. when a new
44
44
  #: Order is placed, all indexes will then be off by one), but is still captured as it may be applicable in
45
45
  #: various use-cases. Populated when the Order was fetched through
46
46
  #: :func:`~amazonorders.orders.AmazonOrders.get_order_history` (use ``start_index`` to correlate), or when
@@ -44,6 +44,13 @@ class Selectors:
44
44
  # CSS selectors for AuthForms
45
45
  ##########################################################################
46
46
 
47
+ ACIC_CHALLENGE_SELECTOR = "#aa-challenge-page-captcha-container"
48
+ ACIC_VISUAL_CAPTCHA_MODAL_SELECTOR = ".amzn-captcha-modal"
49
+ ACIC_VISUAL_CAPTCHA_CANVAS_SELECTOR = ".amzn-captcha-modal canvas"
50
+ ACIC_VISUAL_CAPTCHA_QUESTION_SELECTOR = ".amzn-captcha-modal em"
51
+ ACIC_VISUAL_CAPTCHA_VERIFY_SELECTOR = "#amzn-btn-verify-internal"
52
+ AWS_WAF_CHALLENGE_SCRIPT_SELECTOR = 'script[src*="awswaf.com"]'
53
+
47
54
  SIGN_IN_FORM_SELECTOR = "form[name='signIn']"
48
55
  CLAIM_FORM_SELECTOR = "form[name='signIn'].auth-validate-form"
49
56
  INTENT_FORM_SELECTOR = "form#intent-confirmation-form"
@@ -36,8 +36,8 @@ classifiers = [
36
36
  ]
37
37
 
38
38
  [project.optional-dependencies]
39
- captcha = [
40
- "amazoncaptcha>=0.4",
39
+ browser = [
40
+ "playwright>=1.47.0",
41
41
  ]
42
42
  capsolver = [
43
43
  "capsolver",
@@ -48,12 +48,12 @@ anticaptcha = [
48
48
  2captcha = [
49
49
  "2captcha-python",
50
50
  ]
51
- browser = [
52
- "playwright>=1.47.0",
53
- ]
54
51
  lxml = [
55
52
  "lxml",
56
53
  ]
54
+ captcha = [
55
+ "amazoncaptcha>=0.4",
56
+ ]
57
57
  dev = [
58
58
  "pytest",
59
59
  "coverage[toml]",
@@ -62,7 +62,6 @@ dev = [
62
62
  "pep8-naming",
63
63
  "responses",
64
64
  "lxml",
65
- "amazoncaptcha>=0.4; python_version < '3.13'",
66
65
  ]
67
66
  integration = [
68
67
  "pytest-rerunfailures",
@@ -143,3 +142,15 @@ exclude = "scripts/*,docs/*,venv/*,build/*,dist/*,.egg-info/*,.mypy_cache/*"
143
142
  [[tool.mypy.overrides]]
144
143
  module = "amazoncaptcha.*"
145
144
  ignore_missing_imports = true
145
+
146
+ [[tool.mypy.overrides]]
147
+ module = "capsolver.*"
148
+ ignore_missing_imports = true
149
+
150
+ [[tool.mypy.overrides]]
151
+ module = "twocaptcha.*"
152
+ ignore_missing_imports = true
153
+
154
+ [[tool.mypy.overrides]]
155
+ module = "anticaptchaofficial.*"
156
+ ignore_missing_imports = true
File without changes
File without changes
File without changes