amazon-orders 4.3.0__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.0 → amazon_orders-4.4.0}/CHANGELOG.md +32 -1
  2. {amazon_orders-4.3.0/amazon_orders.egg-info → amazon_orders-4.4.0}/PKG-INFO +8 -11
  3. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/README.md +3 -5
  4. {amazon_orders-4.3.0 → amazon_orders-4.4.0/amazon_orders.egg-info}/PKG-INFO +8 -11
  5. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazon_orders.egg-info/requires.txt +0 -3
  6. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/__init__.py +1 -1
  7. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/conf.py +2 -0
  8. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/constants.py +4 -0
  9. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/browser/playwright.py +146 -18
  10. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/waf/anticaptcha.py +1 -1
  11. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/waf/base.py +19 -4
  12. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/waf/capsolver.py +52 -2
  13. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/waf/twocaptcha.py +1 -1
  14. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/order.py +1 -1
  15. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/orders.py +21 -0
  16. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/selectors.py +7 -0
  17. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/transactions.py +13 -7
  18. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/pyproject.toml +17 -6
  19. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/LICENSE +0 -0
  20. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/MANIFEST.in +0 -0
  21. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazon_orders.egg-info/SOURCES.txt +0 -0
  22. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazon_orders.egg-info/dependency_links.txt +0 -0
  23. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazon_orders.egg-info/entry_points.txt +0 -0
  24. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazon_orders.egg-info/top_level.txt +0 -0
  25. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/banner.txt +0 -0
  26. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/cli.py +0 -0
  27. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/__init__.py +0 -0
  28. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/browser/__init__.py +0 -0
  29. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/contrib/waf/__init__.py +0 -0
  30. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/__init__.py +0 -0
  31. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/item.py +0 -0
  32. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/parsable.py +0 -0
  33. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/recipient.py +0 -0
  34. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/seller.py +0 -0
  35. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/shipment.py +0 -0
  36. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/entity/transaction.py +0 -0
  37. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/exception.py +0 -0
  38. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/forms.py +0 -0
  39. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/session.py +0 -0
  40. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/amazonorders/util.py +0 -0
  41. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/setup.cfg +0 -0
  42. {amazon_orders-4.3.0 → amazon_orders-4.4.0}/tests/testcase.py +0 -0
@@ -4,7 +4,29 @@ 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.0...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.
18
+
19
+ ## [4.3.1](https://github.com/alexdlaird/amazon-orders/compare/4.3.0...4.3.1) - 2026-06-07
20
+
21
+ ### Added
22
+
23
+ - `AmazonOrders.get_invoice()` to fetch an Order's print-friendly invoice page, returning the response (including its parsed HTML) for rendering or printing.
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.
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.
8
30
 
9
31
  ## [4.3.0](https://github.com/alexdlaird/amazon-orders/compare/4.2.2...4.3.0) - 2026-06-07
10
32
 
@@ -22,6 +44,15 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
22
44
  - JavaScript-based authentication challenge errors now direct users to the `[browser]` extra rather than reporting the challenge as unsolvable.
23
45
  - All user-facing error messages that include install commands now wrap those commands in backticks.
24
46
 
47
+ ### Added
48
+
49
+ - `[browser]` optional extra (`pip install amazon-orders[browser]`) for handling JavaScript-based authentication challenges via a headless browser. See [the docs](https://amazon-orders.readthedocs.io/browser.html) for setup.
50
+ - `PlaywrightAcicForm` handles the ACIC challenge page. If an embedded WAF is present, it delegates automatically to any configured WAF solver extra.
51
+ - `PlaywrightJSAuthForm` is a best-effort handler for the JS bot-detection page.
52
+ - `PlaywrightManualWafForm` opens a **visible** browser window for you to solve the challenge yourself, suitable when a display is available.
53
+ - `browser` config key and `AMAZON_BROWSER` environment variable to select between `chromium` (default) and `firefox` browser user agents.
54
+ - `Transaction.payment_method_last_4`, the masked card digits parsed from `payment_method`, mirroring the existing field on `Order`.
55
+
25
56
  ## [4.2.2](https://github.com/alexdlaird/amazon-orders/compare/4.2.1...4.2.2) - 2026-06-06
26
57
 
27
58
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.3.0
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
@@ -182,15 +181,13 @@ playwright install chromium
182
181
 
183
182
  See [Browser Automation](https://amazon-orders.readthedocs.io/browser.html) for details.
184
183
 
185
- To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
186
- dependency), install with the `captcha` extra:
184
+ For **legacy Captcha auto-solve** on Python <=3.12, install with `captcha` extra:
187
185
 
188
186
  ```sh
189
187
  pip install amazon-orders[captcha]
190
188
  ```
191
189
 
192
- Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
193
- [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
190
+ See [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
194
191
 
195
192
  ## Documentation
196
193
 
@@ -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
@@ -107,15 +107,13 @@ playwright install chromium
107
107
 
108
108
  See [Browser Automation](https://amazon-orders.readthedocs.io/browser.html) for details.
109
109
 
110
- To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
111
- dependency), install with the `captcha` extra:
110
+ For **legacy Captcha auto-solve** on Python <=3.12, install with `captcha` extra:
112
111
 
113
112
  ```sh
114
113
  pip install amazon-orders[captcha]
115
114
  ```
116
115
 
117
- Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
118
- [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
116
+ See [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
119
117
 
120
118
  ## Documentation
121
119
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.3.0
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
@@ -182,15 +181,13 @@ playwright install chromium
182
181
 
183
182
  See [Browser Automation](https://amazon-orders.readthedocs.io/browser.html) for details.
184
183
 
185
- To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
186
- dependency), install with the `captcha` extra:
184
+ For **legacy Captcha auto-solve** on Python <=3.12, install with `captcha` extra:
187
185
 
188
186
  ```sh
189
187
  pip install amazon-orders[captcha]
190
188
  ```
191
189
 
192
- Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
193
- [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
190
+ See [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
194
191
 
195
192
  ## Documentation
196
193
 
@@ -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.0"
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
@@ -110,6 +110,7 @@ class Constants:
110
110
 
111
111
  ORDER_HISTORY_URL = f"{BASE_URL}/your-orders/orders"
112
112
  ORDER_DETAILS_URL = f"{BASE_URL}/gp/your-account/order-details"
113
+ ORDER_INVOICE_URL = f"{BASE_URL}/gp/css/summary/print.html"
113
114
  HISTORY_FILTER_QUERY_PARAM = "timeFilter"
114
115
 
115
116
  ##########################################################################
@@ -147,6 +148,8 @@ class Constants:
147
148
 
148
149
  COOKIES_SET_WHEN_AUTHENTICATED = ["x-main"]
149
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"
150
153
 
151
154
  ##########################################################################
152
155
  # Currency
@@ -215,6 +218,7 @@ class Constants:
215
218
  self.SIGN_OUT_URL = f"{base_url}/gp/flex/sign-out.html"
216
219
  self.ORDER_HISTORY_URL = f"{base_url}/your-orders/orders"
217
220
  self.ORDER_DETAILS_URL = f"{base_url}/gp/your-account/order-details"
221
+ self.ORDER_INVOICE_URL = f"{base_url}/gp/css/summary/print.html"
218
222
  self.TRANSACTION_HISTORY_URL = f"{base_url}{self.TRANSACTION_HISTORY_ROUTE}"
219
223
 
220
224
  host = urlparse(base_url).netloc.lower().split(":")[0]
@@ -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
@@ -77,6 +77,27 @@ class AmazonOrders:
77
77
 
78
78
  return order
79
79
 
80
+ def get_invoice(self,
81
+ order_id: str) -> util.AmazonSessionResponse:
82
+ """
83
+ Get the print-friendly invoice page for a given Amazon Order ID, returning the response
84
+ (including its parsed HTML) so callers can render or print the page.
85
+
86
+ :param order_id: The Amazon Order ID to lookup.
87
+ :return: The invoice page response.
88
+ """
89
+ if not self.amazon_session.is_authenticated:
90
+ raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")
91
+
92
+ invoice_response = self.amazon_session.get(
93
+ f"{self.config.constants.ORDER_INVOICE_URL}?orderID={order_id}")
94
+ self.amazon_session.check_response(invoice_response)
95
+
96
+ if not invoice_response.response.url.startswith(self.config.constants.ORDER_INVOICE_URL):
97
+ raise AmazonOrdersNotFoundError(f"Amazon redirected, which likely means Order {order_id} was not found.")
98
+
99
+ return invoice_response
100
+
80
101
  def get_order_history(self,
81
102
  year: Optional[int] = None,
82
103
  start_index: Optional[int] = None,
@@ -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"
@@ -85,28 +85,34 @@ class AmazonTransactions:
85
85
  def get_transactions(self,
86
86
  days: int = 365,
87
87
  next_page_data: Optional[Dict[str, Any]] = None,
88
- keep_paging: bool = True) -> List[Transaction]:
88
+ keep_paging: bool = True,
89
+ order_id: Optional[str] = None) -> List[Transaction]:
89
90
  """
90
- Get Amazon Transaction history for a given number of days.
91
+ Get Amazon Transaction history for a given number of days, or for a single Order.
91
92
 
92
- :param days: The number of days worth of Transactions to get.
93
+ :param days: The number of days worth of Transactions to get. Ignored when ``order_id`` is given.
93
94
  :param next_page_data: If a call to this method previously errored out, passing the exception's
94
95
  :attr:`~amazonorders.exception.AmazonOrdersError.meta` will continue paging where it left off.
95
96
  :param keep_paging: ``False`` if only one page should be fetched.
97
+ :param order_id: If given, only Transactions for this Amazon Order ID are returned, scoped
98
+ server-side via Amazon's ``transactionTag`` filter (the ``days`` window does not apply).
96
99
  :return: A list of the requested Transactions.
97
100
  """
98
101
  if not self.amazon_session.is_authenticated:
99
102
  raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")
100
103
 
101
- min_date = datetime.date.today() - datetime.timedelta(days=days)
104
+ url = self.config.constants.TRANSACTION_HISTORY_URL
105
+ if order_id:
106
+ url = f"{url}?transactionTag={order_id}"
107
+ else:
108
+ min_date = datetime.date.today() - datetime.timedelta(days=days)
102
109
 
103
110
  transactions: List[Transaction] = []
104
111
  first_page = True
105
112
  while first_page or keep_paging:
106
113
  first_page = False
107
114
 
108
- page_response = self.amazon_session.post(self.config.constants.TRANSACTION_HISTORY_URL,
109
- data=next_page_data)
115
+ page_response = self.amazon_session.post(url, data=next_page_data)
110
116
  self.amazon_session.check_response(page_response, meta=next_page_data)
111
117
 
112
118
  form_tag = util.select_one(page_response.parsed,
@@ -125,7 +131,7 @@ class AmazonTransactions:
125
131
  )
126
132
 
127
133
  for transaction in loaded_transactions:
128
- if transaction.completed_date >= min_date:
134
+ if order_id or transaction.completed_date >= min_date:
129
135
  transactions.append(transaction)
130
136
  else:
131
137
  next_page_data = None
@@ -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