amazon-orders 4.2.1__tar.gz → 4.3.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.2.1 → amazon_orders-4.3.0}/CHANGELOG.md +27 -1
  2. {amazon_orders-4.2.1/amazon_orders.egg-info → amazon_orders-4.3.0}/PKG-INFO +30 -12
  3. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/README.md +18 -8
  4. {amazon_orders-4.2.1 → amazon_orders-4.3.0/amazon_orders.egg-info}/PKG-INFO +30 -12
  5. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazon_orders.egg-info/SOURCES.txt +2 -0
  6. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazon_orders.egg-info/requires.txt +7 -2
  7. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/__init__.py +1 -1
  8. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/constants.py +56 -23
  9. amazon_orders-4.3.0/amazonorders/contrib/browser/playwright.py +412 -0
  10. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/contrib/waf/anticaptcha.py +1 -1
  11. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/contrib/waf/capsolver.py +1 -1
  12. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/contrib/waf/twocaptcha.py +1 -1
  13. amazon_orders-4.3.0/amazonorders/entity/__init__.py +0 -0
  14. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/transaction.py +11 -0
  15. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/forms.py +20 -3
  16. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/session.py +10 -3
  17. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/pyproject.toml +12 -3
  18. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/LICENSE +0 -0
  19. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/MANIFEST.in +0 -0
  20. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazon_orders.egg-info/dependency_links.txt +0 -0
  21. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazon_orders.egg-info/entry_points.txt +0 -0
  22. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazon_orders.egg-info/top_level.txt +0 -0
  23. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/banner.txt +0 -0
  24. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/cli.py +0 -0
  25. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/conf.py +0 -0
  26. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/contrib/__init__.py +0 -0
  27. {amazon_orders-4.2.1/amazonorders/contrib/waf → amazon_orders-4.3.0/amazonorders/contrib/browser}/__init__.py +0 -0
  28. {amazon_orders-4.2.1/amazonorders/entity → amazon_orders-4.3.0/amazonorders/contrib/waf}/__init__.py +0 -0
  29. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/contrib/waf/base.py +0 -0
  30. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/item.py +0 -0
  31. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/order.py +0 -0
  32. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/parsable.py +0 -0
  33. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/recipient.py +0 -0
  34. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/seller.py +0 -0
  35. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/entity/shipment.py +0 -0
  36. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/exception.py +0 -0
  37. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/orders.py +0 -0
  38. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/selectors.py +0 -0
  39. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/transactions.py +0 -0
  40. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/amazonorders/util.py +0 -0
  41. {amazon_orders-4.2.1 → amazon_orders-4.3.0}/setup.cfg +0 -0
  42. {amazon_orders-4.2.1 → amazon_orders-4.3.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.2.1...HEAD)
7
+ ## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.3.0...HEAD)
8
+
9
+ ## [4.3.0](https://github.com/alexdlaird/amazon-orders/compare/4.2.2...4.3.0) - 2026-06-07
10
+
11
+ ### Added
12
+
13
+ - `[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.
14
+ - `PlaywrightAcicForm` handles the ACIC challenge page. If an embedded AWS WAF CAPTCHA is present, it delegates automatically to any configured WAF solver extra.
15
+ - `PlaywrightJSAuthForm` is a best-effort handler for the JS robot-detection page.
16
+ - `PlaywrightManualWafForm` opens a **visible** browser window for manual WAF CAPTCHA solving — a free alternative to the paid `[waf]` extras for local/interactive use.
17
+ - `browser` config key and `AMAZON_BROWSER` environment variable to select between `chromium` (default) and `firefox` browser fingerprints.
18
+ - `Transaction.payment_method_last_4`, the masked card digits parsed from `payment_method`, mirroring the existing field on `Order`.
19
+
20
+ ### Changed
21
+
22
+ - JavaScript-based authentication challenge errors now direct users to the `[browser]` extra rather than reporting the challenge as unsolvable.
23
+ - All user-facing error messages that include install commands now wrap those commands in backticks.
24
+
25
+ ## [4.2.2](https://github.com/alexdlaird/amazon-orders/compare/4.2.1...4.2.2) - 2026-06-06
26
+
27
+ ### Fixed
28
+
29
+ - Improved error messaging for known JavaScript-based authentication challenges.
8
30
 
9
31
  ## [4.2.1](https://github.com/alexdlaird/amazon-orders/compare/4.2.0...4.2.1) - 2026-05-08
10
32
 
@@ -12,6 +34,10 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
12
34
 
13
35
  - WAF solver API keys can now also be set via `AmazonOrdersConfig` (under the lowercased env var name, e.g. `capsolver_api_key`), matching the precedence pattern used for other credentials.
14
36
 
37
+ ### Changed
38
+
39
+ - WAF solver API keys can now also be set via `AmazonOrdersConfig` (under the lowercased env var name, e.g. `capsolver_api_key`), matching the precedence pattern used for other credentials.
40
+
15
41
  ## [4.2.0](https://github.com/alexdlaird/amazon-orders/compare/4.1.0...4.2.0) - 2026-05-08
16
42
 
17
43
  ### Added
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.2.1
4
- Summary: A Python libray (and CLI) for Amazon order history
3
+ Version: 4.3.0
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>
7
7
  License-Expression: MIT
@@ -9,6 +9,7 @@ Project-URL: Changelog, https://github.com/alexdlaird/amazon-orders/blob/main/CH
9
9
  Project-URL: Documentation, https://amazon-orders.readthedocs.io
10
10
  Project-URL: Sponsor, https://github.com/sponsors/alexdlaird
11
11
  Project-URL: Source Code, https://github.com/alexdlaird/amazon-orders
12
+ Keywords: amazon,orders,order-history,scraper,web-scraping,receipts,cli
12
13
  Classifier: Development Status :: 5 - Production/Stable
13
14
  Classifier: Environment :: Console
14
15
  Classifier: Environment :: Web Environment
@@ -20,7 +21,10 @@ Classifier: Programming Language :: Python :: 3.10
20
21
  Classifier: Programming Language :: Python :: 3.11
21
22
  Classifier: Programming Language :: Python :: 3.12
22
23
  Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
25
+ Classifier: Topic :: Office/Business :: Financial
23
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Topic :: Text Processing :: Markup :: HTML
24
28
  Requires-Python: >=3.9
25
29
  Description-Content-Type: text/markdown
26
30
  License-File: LICENSE
@@ -28,8 +32,8 @@ Requires-Dist: click>=7.1
28
32
  Requires-Dist: requests>=2.23
29
33
  Requires-Dist: Pillow>=9.0.1
30
34
  Requires-Dist: beautifulsoup4>=4.12
31
- Requires-Dist: PyYAML>=5.1
32
- Requires-Dist: python-dateutil>=2.8
35
+ Requires-Dist: PyYAML>=6.0.3
36
+ Requires-Dist: python-dateutil>=2.9.0.post0
33
37
  Requires-Dist: pyotp>=2.9
34
38
  Provides-Extra: captcha
35
39
  Requires-Dist: amazoncaptcha>=0.4; extra == "captcha"
@@ -39,6 +43,8 @@ Provides-Extra: anticaptcha
39
43
  Requires-Dist: anticaptchaofficial; extra == "anticaptcha"
40
44
  Provides-Extra: 2captcha
41
45
  Requires-Dist: 2captcha-python; extra == "2captcha"
46
+ Provides-Extra: browser
47
+ Requires-Dist: playwright>=1.47.0; extra == "browser"
42
48
  Provides-Extra: lxml
43
49
  Requires-Dist: lxml; extra == "lxml"
44
50
  Provides-Extra: dev
@@ -63,9 +69,11 @@ Requires-Dist: types-beautifulsoup4; extra == "docs"
63
69
  Requires-Dist: types-Pillow; extra == "docs"
64
70
  Requires-Dist: types-PyYAML; extra == "docs"
65
71
  Requires-Dist: types-python-dateutil; extra == "docs"
72
+ Requires-Dist: sphinx-sitemap; extra == "docs"
73
+ Requires-Dist: sphinxext-opengraph; extra == "docs"
66
74
  Dynamic: license-file
67
75
 
68
- <p align="center"><img alt="amazon-orders - A Python libray (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
76
+ <p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
69
77
 
70
78
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
71
79
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
@@ -74,17 +82,17 @@ Dynamic: license-file
74
82
  [![Docs](https://img.shields.io/readthedocs/amazon-orders)](https://amazon-orders.readthedocs.io)
75
83
  [![GitHub License](https://img.shields.io/github/license/alexdlaird/amazon-orders)](https://github.com/alexdlaird/amazon-orders/blob/main/LICENSE)
76
84
 
77
- `amazon-orders` is an unofficial library that provides a Python API (and CLI) for Amazon order history.
78
-
79
- This package works by parsing data from Amazon's consumer-facing website. A periodic build validates functionality
80
- to ensure its stability, but as Amazon provides no official API to use, this package may break at any time (so check
81
- often to ensure you're on the latest version).
85
+ `amazon-orders` is an unofficial library that provides a Python API (and CLI) for Amazon order history, line items, and transactions.
82
86
 
83
87
  Only the English, `.com` version of Amazon is officially supported. Other Amazon domains can be
84
88
  targeted by passing `domain` to [`AmazonSession`](https://amazon-orders.readthedocs.io/api.html#amazonorders.session.AmazonSession)
85
89
  (or `--domain` on the CLI), and other English-based sites may work by chance — see
86
90
  [Known Limitations](https://amazon-orders.readthedocs.io/index.html#known-limitations) for details.
87
91
 
92
+ > **Note:** This package works by parsing data from Amazon's consumer-facing website. A periodic build validates
93
+ > functionality to ensure its stability, but as Amazon provides no official API to use, older versions of this
94
+ > package may break at any time, so it's recommended that you use the latest version.
95
+
88
96
  ## Installation
89
97
 
90
98
  `amazon-orders` is available on [PyPI](https://pypi.org/project/amazon-orders/) and can be installed and/or upgraded using `pip`:
@@ -95,7 +103,7 @@ pip install amazon-orders --upgrade
95
103
 
96
104
  That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
97
105
 
98
- If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.2.*`, not `==4.2.1`) to
106
+ If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.3.*`, not `==4.2.1`) to
99
107
  ensure you always get the latest stable release.
100
108
 
101
109
  ## Basic Usage
@@ -164,6 +172,16 @@ pip install amazon-orders[2captcha]
164
172
 
165
173
  See [Solving WAF Challenges](https://amazon-orders.readthedocs.io/waf.html) for details.
166
174
 
175
+ To enable **browser-based challenge handling** (ACIC and JavaScript bot-detection pages) via
176
+ a headless browser, install with the `browser` extra:
177
+
178
+ ```sh
179
+ pip install amazon-orders[browser]
180
+ playwright install chromium
181
+ ```
182
+
183
+ See [Browser Automation](https://amazon-orders.readthedocs.io/browser.html) for details.
184
+
167
185
  To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
168
186
  dependency), install with the `captcha` extra:
169
187
 
@@ -172,7 +190,7 @@ pip install amazon-orders[captcha]
172
190
  ```
173
191
 
174
192
  Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
175
- [Captcha Blocking Login](https://amazon-orders.readthedocs.io/troubleshooting.html#captcha-blocking-login) for details.
193
+ [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
176
194
 
177
195
  ## Documentation
178
196
 
@@ -1,4 +1,4 @@
1
- <p align="center"><img alt="amazon-orders - A Python libray (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
1
+ <p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
2
2
 
3
3
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
4
4
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
@@ -7,17 +7,17 @@
7
7
  [![Docs](https://img.shields.io/readthedocs/amazon-orders)](https://amazon-orders.readthedocs.io)
8
8
  [![GitHub License](https://img.shields.io/github/license/alexdlaird/amazon-orders)](https://github.com/alexdlaird/amazon-orders/blob/main/LICENSE)
9
9
 
10
- `amazon-orders` is an unofficial library that provides a Python API (and CLI) for Amazon order history.
11
-
12
- This package works by parsing data from Amazon's consumer-facing website. A periodic build validates functionality
13
- to ensure its stability, but as Amazon provides no official API to use, this package may break at any time (so check
14
- often to ensure you're on the latest version).
10
+ `amazon-orders` is an unofficial library that provides a Python API (and CLI) for Amazon order history, line items, and transactions.
15
11
 
16
12
  Only the English, `.com` version of Amazon is officially supported. Other Amazon domains can be
17
13
  targeted by passing `domain` to [`AmazonSession`](https://amazon-orders.readthedocs.io/api.html#amazonorders.session.AmazonSession)
18
14
  (or `--domain` on the CLI), and other English-based sites may work by chance — see
19
15
  [Known Limitations](https://amazon-orders.readthedocs.io/index.html#known-limitations) for details.
20
16
 
17
+ > **Note:** This package works by parsing data from Amazon's consumer-facing website. A periodic build validates
18
+ > functionality to ensure its stability, but as Amazon provides no official API to use, older versions of this
19
+ > package may break at any time, so it's recommended that you use the latest version.
20
+
21
21
  ## Installation
22
22
 
23
23
  `amazon-orders` is available on [PyPI](https://pypi.org/project/amazon-orders/) and can be installed and/or upgraded using `pip`:
@@ -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.2.*`, not `==4.2.1`) to
31
+ If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.3.*`, not `==4.2.1`) to
32
32
  ensure you always get the latest stable release.
33
33
 
34
34
  ## Basic Usage
@@ -97,6 +97,16 @@ pip install amazon-orders[2captcha]
97
97
 
98
98
  See [Solving WAF Challenges](https://amazon-orders.readthedocs.io/waf.html) for details.
99
99
 
100
+ To enable **browser-based challenge handling** (ACIC and JavaScript bot-detection pages) via
101
+ a headless browser, install with the `browser` extra:
102
+
103
+ ```sh
104
+ pip install amazon-orders[browser]
105
+ playwright install chromium
106
+ ```
107
+
108
+ See [Browser Automation](https://amazon-orders.readthedocs.io/browser.html) for details.
109
+
100
110
  To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
101
111
  dependency), install with the `captcha` extra:
102
112
 
@@ -105,7 +115,7 @@ pip install amazon-orders[captcha]
105
115
  ```
106
116
 
107
117
  Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
108
- [Captcha Blocking Login](https://amazon-orders.readthedocs.io/troubleshooting.html#captcha-blocking-login) for details.
118
+ [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
109
119
 
110
120
  ## Documentation
111
121
 
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.2.1
4
- Summary: A Python libray (and CLI) for Amazon order history
3
+ Version: 4.3.0
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>
7
7
  License-Expression: MIT
@@ -9,6 +9,7 @@ Project-URL: Changelog, https://github.com/alexdlaird/amazon-orders/blob/main/CH
9
9
  Project-URL: Documentation, https://amazon-orders.readthedocs.io
10
10
  Project-URL: Sponsor, https://github.com/sponsors/alexdlaird
11
11
  Project-URL: Source Code, https://github.com/alexdlaird/amazon-orders
12
+ Keywords: amazon,orders,order-history,scraper,web-scraping,receipts,cli
12
13
  Classifier: Development Status :: 5 - Production/Stable
13
14
  Classifier: Environment :: Console
14
15
  Classifier: Environment :: Web Environment
@@ -20,7 +21,10 @@ Classifier: Programming Language :: Python :: 3.10
20
21
  Classifier: Programming Language :: Python :: 3.11
21
22
  Classifier: Programming Language :: Python :: 3.12
22
23
  Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
25
+ Classifier: Topic :: Office/Business :: Financial
23
26
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Topic :: Text Processing :: Markup :: HTML
24
28
  Requires-Python: >=3.9
25
29
  Description-Content-Type: text/markdown
26
30
  License-File: LICENSE
@@ -28,8 +32,8 @@ Requires-Dist: click>=7.1
28
32
  Requires-Dist: requests>=2.23
29
33
  Requires-Dist: Pillow>=9.0.1
30
34
  Requires-Dist: beautifulsoup4>=4.12
31
- Requires-Dist: PyYAML>=5.1
32
- Requires-Dist: python-dateutil>=2.8
35
+ Requires-Dist: PyYAML>=6.0.3
36
+ Requires-Dist: python-dateutil>=2.9.0.post0
33
37
  Requires-Dist: pyotp>=2.9
34
38
  Provides-Extra: captcha
35
39
  Requires-Dist: amazoncaptcha>=0.4; extra == "captcha"
@@ -39,6 +43,8 @@ Provides-Extra: anticaptcha
39
43
  Requires-Dist: anticaptchaofficial; extra == "anticaptcha"
40
44
  Provides-Extra: 2captcha
41
45
  Requires-Dist: 2captcha-python; extra == "2captcha"
46
+ Provides-Extra: browser
47
+ Requires-Dist: playwright>=1.47.0; extra == "browser"
42
48
  Provides-Extra: lxml
43
49
  Requires-Dist: lxml; extra == "lxml"
44
50
  Provides-Extra: dev
@@ -63,9 +69,11 @@ Requires-Dist: types-beautifulsoup4; extra == "docs"
63
69
  Requires-Dist: types-Pillow; extra == "docs"
64
70
  Requires-Dist: types-PyYAML; extra == "docs"
65
71
  Requires-Dist: types-python-dateutil; extra == "docs"
72
+ Requires-Dist: sphinx-sitemap; extra == "docs"
73
+ Requires-Dist: sphinxext-opengraph; extra == "docs"
66
74
  Dynamic: license-file
67
75
 
68
- <p align="center"><img alt="amazon-orders - A Python libray (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
76
+ <p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
69
77
 
70
78
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
71
79
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
@@ -74,17 +82,17 @@ Dynamic: license-file
74
82
  [![Docs](https://img.shields.io/readthedocs/amazon-orders)](https://amazon-orders.readthedocs.io)
75
83
  [![GitHub License](https://img.shields.io/github/license/alexdlaird/amazon-orders)](https://github.com/alexdlaird/amazon-orders/blob/main/LICENSE)
76
84
 
77
- `amazon-orders` is an unofficial library that provides a Python API (and CLI) for Amazon order history.
78
-
79
- This package works by parsing data from Amazon's consumer-facing website. A periodic build validates functionality
80
- to ensure its stability, but as Amazon provides no official API to use, this package may break at any time (so check
81
- often to ensure you're on the latest version).
85
+ `amazon-orders` is an unofficial library that provides a Python API (and CLI) for Amazon order history, line items, and transactions.
82
86
 
83
87
  Only the English, `.com` version of Amazon is officially supported. Other Amazon domains can be
84
88
  targeted by passing `domain` to [`AmazonSession`](https://amazon-orders.readthedocs.io/api.html#amazonorders.session.AmazonSession)
85
89
  (or `--domain` on the CLI), and other English-based sites may work by chance — see
86
90
  [Known Limitations](https://amazon-orders.readthedocs.io/index.html#known-limitations) for details.
87
91
 
92
+ > **Note:** This package works by parsing data from Amazon's consumer-facing website. A periodic build validates
93
+ > functionality to ensure its stability, but as Amazon provides no official API to use, older versions of this
94
+ > package may break at any time, so it's recommended that you use the latest version.
95
+
88
96
  ## Installation
89
97
 
90
98
  `amazon-orders` is available on [PyPI](https://pypi.org/project/amazon-orders/) and can be installed and/or upgraded using `pip`:
@@ -95,7 +103,7 @@ pip install amazon-orders --upgrade
95
103
 
96
104
  That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
97
105
 
98
- If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.2.*`, not `==4.2.1`) to
106
+ If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.3.*`, not `==4.2.1`) to
99
107
  ensure you always get the latest stable release.
100
108
 
101
109
  ## Basic Usage
@@ -164,6 +172,16 @@ pip install amazon-orders[2captcha]
164
172
 
165
173
  See [Solving WAF Challenges](https://amazon-orders.readthedocs.io/waf.html) for details.
166
174
 
175
+ To enable **browser-based challenge handling** (ACIC and JavaScript bot-detection pages) via
176
+ a headless browser, install with the `browser` extra:
177
+
178
+ ```sh
179
+ pip install amazon-orders[browser]
180
+ playwright install chromium
181
+ ```
182
+
183
+ See [Browser Automation](https://amazon-orders.readthedocs.io/browser.html) for details.
184
+
167
185
  To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
168
186
  dependency), install with the `captcha` extra:
169
187
 
@@ -172,7 +190,7 @@ pip install amazon-orders[captcha]
172
190
  ```
173
191
 
174
192
  Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
175
- [Captcha Blocking Login](https://amazon-orders.readthedocs.io/troubleshooting.html#captcha-blocking-login) for details.
193
+ [Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
176
194
 
177
195
  ## Documentation
178
196
 
@@ -22,6 +22,8 @@ amazonorders/session.py
22
22
  amazonorders/transactions.py
23
23
  amazonorders/util.py
24
24
  amazonorders/contrib/__init__.py
25
+ amazonorders/contrib/browser/__init__.py
26
+ amazonorders/contrib/browser/playwright.py
25
27
  amazonorders/contrib/waf/__init__.py
26
28
  amazonorders/contrib/waf/anticaptcha.py
27
29
  amazonorders/contrib/waf/base.py
@@ -2,8 +2,8 @@ click>=7.1
2
2
  requests>=2.23
3
3
  Pillow>=9.0.1
4
4
  beautifulsoup4>=4.12
5
- PyYAML>=5.1
6
- python-dateutil>=2.8
5
+ PyYAML>=6.0.3
6
+ python-dateutil>=2.9.0.post0
7
7
  pyotp>=2.9
8
8
 
9
9
  [2captcha]
@@ -12,6 +12,9 @@ pyotp>=2.9
12
12
  [anticaptcha]
13
13
  anticaptchaofficial
14
14
 
15
+ [browser]
16
+ playwright>=1.47.0
17
+
15
18
  [capsolver]
16
19
  capsolver
17
20
 
@@ -40,6 +43,8 @@ types-beautifulsoup4
40
43
  types-Pillow
41
44
  types-PyYAML
42
45
  types-python-dateutil
46
+ sphinx-sitemap
47
+ sphinxext-opengraph
43
48
 
44
49
  [integration]
45
50
  pytest-rerunfailures
@@ -1,3 +1,3 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
- __version__ = "4.2.1"
3
+ __version__ = "4.3.0"
@@ -1,13 +1,32 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
3
 
4
+ import logging
4
5
  import os
5
- from typing import Optional, TYPE_CHECKING
6
+ from typing import Dict, Optional, TYPE_CHECKING
6
7
  from urllib.parse import urlencode, urlparse
7
8
 
8
9
  if TYPE_CHECKING:
9
10
  from amazonorders.conf import AmazonOrdersConfig
10
11
 
12
+ logger = logging.getLogger(__name__)
13
+
14
+ #: Browser-specific header overrides applied on top of the class-level ``BASE_HEADERS``
15
+ #: (which already reflects the Chromium fingerprint). A ``None`` value removes the key
16
+ #: (used to strip headers absent in that engine). ``Accept-Language`` here is the browser
17
+ #: default; domain-specific TLD overrides still apply on top via :func:`~Constants._apply_domain`.
18
+ _BROWSER_PRESETS: Dict[str, Dict[str, Optional[str]]] = {
19
+ "chromium": {}, # BASE_HEADERS already reflects the Chromium fingerprint; no overrides needed.
20
+ "firefox": {
21
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
22
+ "Accept-Language": "en-US,en;q=0.5",
23
+ "Sec-Ch-Ua": None,
24
+ "Sec-Ch-Ua-Mobile": None,
25
+ "Sec-Ch-Ua-Platform": None,
26
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:146.0) Gecko/20100101 Firefox/146.0",
27
+ },
28
+ }
29
+
11
30
  #: ``Accept-Language`` values for English-locale Amazon sites, keyed by the TLD suffix that
12
31
  #: follows ``amazon.``. Looked up dynamically from the user-supplied domain; unknown TLDs keep
13
32
  #: the base ``en-US`` value. This map only governs the ``Accept-Language`` header — it is not
@@ -105,35 +124,21 @@ class Constants:
105
124
  ##########################################################################
106
125
 
107
126
  BASE_HEADERS = {
108
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,"
109
- "application/signed-exchange;v=b3;q=0.7",
127
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", # noqa: E501
110
128
  "Accept-Encoding": "gzip, deflate, br, zstd",
111
129
  "Accept-Language": "en-US,en;q=0.9",
112
- "Cache-Control": "max-age=0",
113
- "Device-Memory": "8",
114
- "Downlink": "10",
115
- "Dpr": "2",
116
- "Ect": "4g",
117
- "Origin": BASE_URL,
118
130
  "Host": urlparse(BASE_URL).netloc,
119
- "Priority": "u=0, i",
131
+ "Origin": BASE_URL,
120
132
  "Referer": f"{SIGN_IN_URL}?{urlencode(SIGN_IN_QUERY_PARAMS)}",
121
- "Rtt": "0",
122
- "Sec-Ch-Device-Memory": "8",
123
- "Sec-Ch-Dpr": "2",
124
- "Sec-Ch-Ua": "Chromium\";v=\"140\", \"Not=A?Brand\";v=\"24\", \"Google Chrome\";v=\"140",
133
+ "Sec-Ch-Ua": '"Chromium";v="149", "Google Chrome";v="149", "Not.A/Brand";v="24"',
125
134
  "Sec-Ch-Ua-Mobile": "?0",
126
- "Sec-Ch-Ua-Platform": "macOS",
127
- "Sec-Ch-Ua-Platform-Version": "15.6.1",
128
- "Sec-Ch-Viewport-Width": "1512",
135
+ "Sec-Ch-Ua-Platform": '"macOS"',
129
136
  "Sec-Fetch-Dest": "document",
130
137
  "Sec-Fetch-Mode": "navigate",
131
138
  "Sec-Fetch-Site": "none",
132
139
  "Sec-Fetch-User": "?1",
133
140
  "Upgrade-Insecure-Requests": "1",
134
- "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6_1) AppleWebKit/537.36 (KHTML, like Gecko) "
135
- "Chrome/140.0.0.0 Safari/537.36",
136
- "Viewport-Width": "1512"
141
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/149.0.0.0 Safari/537.36", # noqa: E501
137
142
  }
138
143
 
139
144
  ##########################################################################
@@ -152,13 +157,41 @@ class Constants:
152
157
  def __init__(self,
153
158
  config: Optional["AmazonOrdersConfig"] = None) -> None:
154
159
  domain = None
160
+ browser = None
155
161
  if config is not None:
156
162
  domain = config._data.get("domain")
163
+ browser = config._data.get("browser")
157
164
  if not domain:
158
165
  domain = os.environ.get("AMAZON_BASE_URL")
166
+ if not browser:
167
+ browser = os.environ.get("AMAZON_BROWSER")
168
+ self._apply_browser(browser or "chromium")
159
169
  if domain:
160
170
  self._apply_domain(domain)
161
171
 
172
+ def _apply_browser(self,
173
+ browser: str) -> None:
174
+ """
175
+ Apply browser-specific header overrides for the given browser engine.
176
+
177
+ :param browser: Browser engine name — ``"firefox"`` or ``"chromium"``. Unknown values
178
+ log a warning and leave ``BASE_HEADERS`` unchanged.
179
+ """
180
+ preset = _BROWSER_PRESETS.get(browser)
181
+ if preset is None:
182
+ logger.warning(
183
+ f"Unknown browser value {browser!r}; "
184
+ f"valid values are: {', '.join(_BROWSER_PRESETS)}. Using default headers."
185
+ )
186
+ return
187
+ headers = dict(type(self).BASE_HEADERS)
188
+ for key, value in preset.items():
189
+ if value is None:
190
+ headers.pop(key, None)
191
+ else:
192
+ headers[key] = value
193
+ self.BASE_HEADERS = headers
194
+
162
195
  def _apply_domain(self,
163
196
  domain: str) -> None:
164
197
  """
@@ -168,8 +201,8 @@ class Constants:
168
201
  """
169
202
  base_url = _normalize_base_url(domain)
170
203
 
171
- # Read non-URL fields from the class so subclass-level overrides (assoc_handle,
172
- # Accept-Language, etc.) are preserved; only rewrite the URL-shaped values.
204
+ # Build from the instance-level BASE_HEADERS if _apply_browser has already set it;
205
+ # otherwise fall back to the class-level definition.
173
206
  sign_in_query_params = dict(type(self).SIGN_IN_QUERY_PARAMS)
174
207
  sign_in_query_params["openid.return_to"] = f"{base_url}/?ref_=nav_custrec_signin"
175
208
 
@@ -189,7 +222,7 @@ class Constants:
189
222
  host = host[len("www."):]
190
223
  tld = host[len("amazon."):] if host.startswith("amazon.") else ""
191
224
 
192
- headers = dict(type(self).BASE_HEADERS)
225
+ headers = dict(vars(self).get("BASE_HEADERS", type(self).BASE_HEADERS))
193
226
  headers["Origin"] = base_url
194
227
  headers["Host"] = urlparse(base_url).netloc
195
228
  headers["Referer"] = f"{sign_in_url}?{urlencode(sign_in_query_params)}"
@@ -0,0 +1,412 @@
1
+ __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
+ __license__ = "MIT"
3
+
4
+ import json
5
+ import logging
6
+ import os
7
+ import re
8
+ from abc import abstractmethod
9
+ from typing import Any, Dict, Optional, TYPE_CHECKING
10
+ from urllib.parse import urlparse
11
+
12
+ from bs4 import Tag
13
+ from requests import Response
14
+
15
+ from amazonorders.conf import AmazonOrdersConfig
16
+ from amazonorders.contrib.waf.base import _GOKU_PROPS_RE
17
+ from amazonorders.exception import AmazonOrdersError
18
+ from amazonorders.forms import AuthForm
19
+ from amazonorders.util import AmazonSessionResponse
20
+
21
+ if TYPE_CHECKING:
22
+ from amazonorders.session import AmazonSession
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class PlaywrightAuthForm(AuthForm):
28
+ """
29
+ Shared base for Playwright-based JavaScript challenge solvers. Subclasses implement
30
+ :func:`select_form` to detect the challenge page and :func:`_is_challenge_url` to
31
+ signal when navigation has completed.
32
+
33
+ This base class handles the Playwright browser lifecycle, bidirectional cookie bridging
34
+ between :mod:`requests` and the Playwright browser context, and re-fetching the final
35
+ URL once the challenge resolves.
36
+
37
+ Requires the ``[browser]`` extra: ``pip install amazon-orders[browser]``,
38
+ then ``playwright install chromium``.
39
+ """
40
+
41
+ def __init__(self,
42
+ config: AmazonOrdersConfig) -> None:
43
+ super().__init__(config, selector=None)
44
+ #: Whether to launch the browser in headless mode. Defaults to ``True``.
45
+ #: Set to ``False`` in subclasses that require user interaction.
46
+ self.headless: bool = True
47
+
48
+ def fill_form(self,
49
+ additional_attrs: Optional[Dict[str, Any]] = None) -> None:
50
+ """JavaScript challenge pages have no ``<form>`` to populate; no-op override."""
51
+ pass
52
+
53
+ def submit(self,
54
+ last_response: Response) -> AmazonSessionResponse:
55
+ """
56
+ Launch a headless browser, bridge the current session cookies into it,
57
+ navigate to the challenge URL, wait for the challenge to resolve, harvest the
58
+ resulting cookies back into the session, and re-fetch the final URL.
59
+
60
+ :param last_response: The response that returned the JavaScript challenge page.
61
+ :return: The :class:`~amazonorders.util.AmazonSessionResponse` from re-fetching
62
+ the URL after the challenge resolves.
63
+ :raises AmazonOrdersError: if the ``playwright`` package is not installed, if
64
+ :func:`select_form` was not called first, or if the challenge does not resolve
65
+ within the timeout.
66
+ """
67
+ if not self.amazon_session:
68
+ raise AmazonOrdersError(
69
+ "Call PlaywrightAuthForm.select_form() first."
70
+ ) # pragma: no cover
71
+
72
+ try:
73
+ from playwright.sync_api import ( # type: ignore[import-not-found]
74
+ sync_playwright,
75
+ TimeoutError as PlaywrightTimeoutError,
76
+ )
77
+ except ImportError as e:
78
+ raise AmazonOrdersError(
79
+ f"{type(self).__name__} requires the [browser] extra. "
80
+ "Install it with: `pip install amazon-orders[browser]`, then: `playwright install chromium`"
81
+ ) from e
82
+
83
+ debug = self.amazon_session.debug
84
+ if debug:
85
+ logger.setLevel(logging.DEBUG)
86
+
87
+ message = "Info: A browser is handling a JavaScript authentication challenge."
88
+ logger.info(message)
89
+ self.amazon_session.io.echo(message)
90
+ output_dir = self.config.output_dir if debug else None
91
+ original_url = last_response.url
92
+
93
+ browser_name = self.config.browser or "chromium"
94
+ with sync_playwright() as pw:
95
+ browser_launcher = getattr(pw, browser_name, None)
96
+ if browser_launcher is None:
97
+ raise AmazonOrdersError(
98
+ f"Unsupported browser: {browser_name!r}. "
99
+ f"Valid values: firefox, chromium."
100
+ )
101
+ browser = browser_launcher.launch(headless=self.headless)
102
+ context = browser.new_context()
103
+
104
+ self._inject_cookies(context, original_url)
105
+
106
+ page = context.new_page()
107
+ page.goto(original_url)
108
+ logger.debug(f"Browser navigated to challenge URL: {page.url}")
109
+
110
+ self._save_debug_snapshot(page, output_dir, "browser-challenge")
111
+ self._on_challenge_page(page, context, output_dir)
112
+
113
+ try:
114
+ page.wait_for_url(
115
+ lambda url: not self._is_challenge_url(url, original_url),
116
+ timeout=30000
117
+ )
118
+ except PlaywrightTimeoutError as e:
119
+ logger.debug(f"Browser timed out at URL: {page.url}")
120
+ self._save_debug_snapshot(page, output_dir, "browser-timeout")
121
+ browser.close()
122
+ raise AmazonOrdersError(
123
+ "Browser timed out waiting for the JavaScript challenge to resolve."
124
+ ) from e
125
+
126
+ final_url = page.url
127
+ logger.debug(f"Browser challenge resolved, final URL: {final_url}")
128
+ self._save_debug_snapshot(page, output_dir, "browser-resolved")
129
+ self._harvest_cookies(context)
130
+ browser.close()
131
+
132
+ response = self.amazon_session.get(final_url, persist_cookies=True)
133
+ self.clear_form()
134
+ return response
135
+
136
+ def _on_challenge_page(self, page: Any, context: Any, output_dir: Optional[str]) -> None:
137
+ """
138
+ Hook called after navigating to the challenge page and saving the initial
139
+ snapshot, but before waiting for the challenge URL to resolve. Override in
140
+ subclasses to take additional action (e.g. solving an embedded CAPTCHA).
141
+
142
+ :param page: The Playwright ``Page`` currently on the challenge URL.
143
+ :param context: The Playwright ``BrowserContext``.
144
+ :param output_dir: Directory for debug snapshots, or ``None`` when not in debug mode.
145
+ """
146
+ pass
147
+
148
+ @abstractmethod
149
+ def _is_challenge_url(self, url: str, original_url: str) -> bool:
150
+ """
151
+ Return ``True`` if ``url`` is still on the challenge page; ``False`` once
152
+ the challenge has resolved and navigation may stop.
153
+
154
+ :param url: The current browser URL.
155
+ :param original_url: The URL of the page that first showed the challenge.
156
+ :return: ``True`` while the challenge is active.
157
+ """
158
+ raise NotImplementedError # pragma: no cover
159
+
160
+ def _inject_cookies(self, context: Any, url: str) -> None:
161
+ if not self.amazon_session:
162
+ return # pragma: no cover
163
+ domain = urlparse(url).netloc
164
+ playwright_cookies = []
165
+ for cookie in self.amazon_session.session.cookies:
166
+ playwright_cookies.append({
167
+ "name": cookie.name,
168
+ "value": cookie.value or "",
169
+ "domain": cookie.domain or domain,
170
+ "path": cookie.path or "/",
171
+ })
172
+ if playwright_cookies:
173
+ context.add_cookies(playwright_cookies)
174
+
175
+ def _harvest_cookies(self, context: Any) -> None:
176
+ if not self.amazon_session:
177
+ return # pragma: no cover
178
+ for pw_cookie in context.cookies():
179
+ self.amazon_session.session.cookies.set(
180
+ pw_cookie["name"],
181
+ pw_cookie["value"],
182
+ domain=pw_cookie.get("domain"),
183
+ path=pw_cookie.get("path", "/"),
184
+ )
185
+
186
+ def _save_debug_snapshot(self, page: Any, output_dir: Optional[str], name: str) -> None:
187
+ if not output_dir:
188
+ return
189
+ try:
190
+ os.makedirs(output_dir, exist_ok=True)
191
+ page.screenshot(path=os.path.join(output_dir, f"{name}.png"))
192
+ with open(os.path.join(output_dir, f"{name}.html"), "w", encoding="utf-8") as f:
193
+ f.write(page.content())
194
+ logger.debug(f"Debug snapshot saved: {name} (url={page.url})")
195
+ except Exception:
196
+ logger.debug(f"Debug snapshot failed to save: {name}", exc_info=True)
197
+
198
+
199
+ class PlaywrightAcicForm(PlaywrightAuthForm):
200
+ """
201
+ Handles Amazon's ACIC (Amazon Challenge and Identity Component) JavaScript challenge
202
+ by running it in a headless browser. If an embedded AWS WAF challenge is present on
203
+ the ACIC page, it will be solved automatically using the first
204
+ :class:`~amazonorders.contrib.waf.base.AwsWafForm` found in ``auth_forms_classes``.
205
+
206
+ Detects the challenge via the ``#aa-challenge-page-captcha-container`` element and
207
+ waits for navigation away from ``/ax/aaut/verify/ap/challenge``.
208
+
209
+ Register via ``auth_forms_classes`` in :class:`~amazonorders.conf.AmazonOrdersConfig`:
210
+
211
+ .. code-block:: yaml
212
+
213
+ auth_forms_classes:
214
+ - "amazonorders.contrib.browser.playwright.PlaywrightAcicForm"
215
+ """
216
+
217
+ def select_form(self,
218
+ amazon_session: "AmazonSession",
219
+ parsed: Tag) -> bool:
220
+ """
221
+ Detect an ACIC challenge page by the presence of
222
+ ``#aa-challenge-page-captcha-container``.
223
+
224
+ :param amazon_session: The ``AmazonSession`` on which to submit the form.
225
+ :param parsed: The ``Tag`` for the page being inspected.
226
+ :return: ``True`` if an ACIC challenge was detected, ``False`` otherwise.
227
+ """
228
+ self.amazon_session = amazon_session
229
+ return bool(parsed.find(id="aa-challenge-page-captcha-container"))
230
+
231
+ def _on_challenge_page(self, page: Any, context: Any, output_dir: Optional[str]) -> None:
232
+ self._try_solve_embedded_waf(page, context, output_dir)
233
+
234
+ def _try_solve_embedded_waf(self, page: Any, context: Any, output_dir: Optional[str]) -> bool:
235
+ """
236
+ If the ACIC challenge page contains an embedded AWS WAF challenge, solve it
237
+ using the first :class:`~amazonorders.contrib.waf.base.AwsWafForm` found in
238
+ ``amazon_session.auth_forms``, inject the resulting ``aws-waf-token`` cookie
239
+ into the browser context, and reload the page.
240
+
241
+ :param page: The Playwright ``Page`` on the ACIC challenge URL.
242
+ :param context: The Playwright ``BrowserContext``.
243
+ :param output_dir: Directory for debug snapshots, or ``None``.
244
+ :return: ``True`` if a WAF token was obtained and injected, ``False`` otherwise.
245
+ """
246
+ try:
247
+ from playwright.sync_api import TimeoutError as PlaywrightTimeoutError # type: ignore[import-not-found]
248
+ except ImportError:
249
+ return False # pragma: no cover
250
+
251
+ try:
252
+ page.wait_for_function("() => typeof window.gokuProps !== 'undefined'", timeout=8000)
253
+ except PlaywrightTimeoutError:
254
+ logger.debug("No window.gokuProps in ACIC page — no embedded WAF challenge to solve.")
255
+ return False
256
+
257
+ try:
258
+ goku = page.evaluate("() => window.gokuProps")
259
+ challenge_script = page.evaluate(
260
+ "() => (Array.from(document.querySelectorAll('script[src]'))"
261
+ ".find(s => s.src.includes('awswaf.com')) || {}).src || null"
262
+ )
263
+ except Exception:
264
+ logger.debug("Failed to extract WAF props from ACIC page.", exc_info=True)
265
+ return False
266
+
267
+ if not goku or not challenge_script:
268
+ logger.debug("Incomplete WAF props in ACIC page — goku=%r, script=%r.", goku, challenge_script)
269
+ return False
270
+
271
+ if not self.amazon_session:
272
+ return False # pragma: no cover
273
+
274
+ from amazonorders.contrib.waf.base import AwsWafForm
275
+ waf_form = next(
276
+ (f for f in self.amazon_session.auth_forms if isinstance(f, AwsWafForm)),
277
+ None,
278
+ )
279
+ if not waf_form:
280
+ logger.debug("Embedded WAF challenge found but no AwsWafForm configured — skipping solve.")
281
+ return False
282
+
283
+ try:
284
+ token = waf_form._solve_token(page.url, goku, challenge_script)
285
+ except Exception:
286
+ logger.debug("WAF solver raised an exception solving embedded ACIC CAPTCHA.", exc_info=True)
287
+ return False
288
+
289
+ message = f"Info: Solved embedded WAF challenge via {waf_form.PROVIDER_NAME}."
290
+ logger.info(message)
291
+ self.amazon_session.io.echo(message)
292
+
293
+ domain = urlparse(page.url).netloc
294
+ context.add_cookies([{
295
+ "name": "aws-waf-token",
296
+ "value": token,
297
+ "domain": f".{domain}",
298
+ "path": "/",
299
+ }])
300
+ page.reload(wait_until="load")
301
+ logger.debug("Reloaded ACIC page after WAF token injection.")
302
+ self._save_debug_snapshot(page, output_dir, "browser-waf-injected")
303
+ return True
304
+
305
+ def _is_challenge_url(self, url: str, original_url: str) -> bool:
306
+ return "/ax/aaut/verify/ap/challenge" in url
307
+
308
+
309
+ class PlaywrightJSAuthForm(PlaywrightAuthForm):
310
+ """
311
+ Handles Amazon's JavaScript bot-detection challenge page by running it in a
312
+ headless browser. This is a best-effort form; effectiveness
313
+ depends on whether the challenge can be resolved by a real browser without a visual puzzle.
314
+
315
+ Detects the challenge via :attr:`~amazonorders.constants.Constants.JS_ROBOT_TEXT_REGEX`
316
+ and waits for navigation away from the original challenge URL path.
317
+
318
+ Register via ``auth_forms_classes`` in :class:`~amazonorders.conf.AmazonOrdersConfig`:
319
+
320
+ .. code-block:: yaml
321
+
322
+ auth_forms_classes:
323
+ - "amazonorders.contrib.browser.playwright.PlaywrightJSAuthForm"
324
+ """
325
+
326
+ def __init__(self,
327
+ config: AmazonOrdersConfig) -> None:
328
+ super().__init__(config)
329
+ #: The regex used to detect the JavaScript bot-detection page text.
330
+ self.regex: str = config.constants.JS_ROBOT_TEXT_REGEX
331
+
332
+ def select_form(self,
333
+ amazon_session: "AmazonSession",
334
+ parsed: Tag) -> bool:
335
+ """
336
+ Detect a JavaScript bot-detection page by matching
337
+ :attr:`~amazonorders.constants.Constants.JS_ROBOT_TEXT_REGEX` against the page text.
338
+
339
+ :param amazon_session: The ``AmazonSession`` on which to submit the form.
340
+ :param parsed: The ``Tag`` for the page being inspected.
341
+ :return: ``True`` if a JavaScript bot challenge was detected, ``False`` otherwise.
342
+ """
343
+ self.amazon_session = amazon_session
344
+ return bool(re.search(self.regex, parsed.text))
345
+
346
+ def _is_challenge_url(self, url: str, original_url: str) -> bool:
347
+ return url.split("?")[0] == original_url.split("?")[0]
348
+
349
+
350
+ class PlaywrightManualWafForm(PlaywrightAuthForm):
351
+ """
352
+ 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
354
+ resolves and the browser navigates away, cookies are harvested back into the
355
+ session automatically.
356
+
357
+ Because it opens a browser window it requires a display and a user at the
358
+ keyboard, making it suitable for local/interactive use but not for headless
359
+ servers or CI.
360
+
361
+ Detects the challenge via the ``window.gokuProps`` blob and the
362
+ ``challenge.js`` script tag (same signals as
363
+ :class:`~amazonorders.contrib.waf.base.AwsWafForm`), and waits for navigation
364
+ away from the original challenge URL path.
365
+
366
+ Register via ``auth_forms_classes`` in :class:`~amazonorders.conf.AmazonOrdersConfig`:
367
+
368
+ .. code-block:: yaml
369
+
370
+ auth_forms_classes:
371
+ - amazonorders.contrib.browser.playwright.PlaywrightManualWafForm
372
+ """
373
+
374
+ def __init__(self,
375
+ config: AmazonOrdersConfig) -> None:
376
+ super().__init__(config)
377
+ self.headless = False
378
+
379
+ def select_form(self,
380
+ amazon_session: "AmazonSession",
381
+ parsed: Tag) -> bool:
382
+ """
383
+ Detect an AWS WAF challenge page by matching the ``window.gokuProps``
384
+ blob and the ``challenge.js`` script tag.
385
+
386
+ :param amazon_session: The ``AmazonSession`` on which to submit the form.
387
+ :param parsed: The ``Tag`` for the page being inspected.
388
+ :return: ``True`` if a WAF challenge was detected, ``False`` otherwise.
389
+ """
390
+ self.amazon_session = amazon_session
391
+
392
+ match = _GOKU_PROPS_RE.search(str(parsed))
393
+ if not match:
394
+ return False
395
+ try:
396
+ json.loads(match.group(1))
397
+ except (json.JSONDecodeError, ValueError):
398
+ return False
399
+
400
+ challenge_tag = parsed.select_one('script[src*="awswaf.com"]')
401
+ return challenge_tag is not None and isinstance(challenge_tag.get("src"), str)
402
+
403
+ def _on_challenge_page(self, page: Any, context: Any, output_dir: Optional[str]) -> None:
404
+ message = (
405
+ "Info: A browser window has opened — solve the CAPTCHA, then return here when done."
406
+ )
407
+ logger.info(message)
408
+ if self.amazon_session:
409
+ self.amazon_session.io.echo(message)
410
+
411
+ def _is_challenge_url(self, url: str, original_url: str) -> bool:
412
+ return url.split("?")[0] == original_url.split("?")[0]
@@ -40,7 +40,7 @@ class AntiCaptchaWafForm(AwsWafForm):
40
40
  except ImportError as e:
41
41
  raise AmazonOrdersError(
42
42
  "AntiCaptchaWafForm requires the 'anticaptchaofficial' package. "
43
- "Install it with: pip install amazon-orders[anticaptcha]"
43
+ "Install it with: `pip install amazon-orders[anticaptcha]`"
44
44
  ) from e
45
45
 
46
46
  solver = amazonProxyless()
@@ -40,7 +40,7 @@ class CapSolverWafForm(AwsWafForm):
40
40
  except ImportError as e:
41
41
  raise AmazonOrdersError(
42
42
  "CapSolverWafForm requires the 'capsolver' package. "
43
- "Install it with: pip install amazon-orders[capsolver]"
43
+ "Install it with: `pip install amazon-orders[capsolver]`"
44
44
  ) from e
45
45
 
46
46
  capsolver.api_key = self.api_key
@@ -43,7 +43,7 @@ class TwoCaptchaWafForm(AwsWafForm):
43
43
  except ImportError as e:
44
44
  raise AmazonOrdersError(
45
45
  "TwoCaptchaWafForm requires the '2captcha-python' package. "
46
- "Install it with: pip install amazon-orders[2captcha]"
46
+ "Install it with: `pip install amazon-orders[2captcha]`"
47
47
  ) from e
48
48
 
49
49
  solver = TwoCaptcha(self.api_key)
File without changes
@@ -32,6 +32,9 @@ class Transaction(Parsable):
32
32
  self.payment_method: str = self.safe_simple_parse(
33
33
  selector=self.config.selectors.FIELD_TRANSACTION_PAYMENT_METHOD_SELECTOR
34
34
  )
35
+ #: The Transaction payment method's last digits, parsed from :attr:`payment_method`.
36
+ #: ``None`` if no masked digits.
37
+ self.payment_method_last_4: Optional[str] = self.safe_parse(self._parse_payment_method_last_4)
35
38
  #: The Transaction grand total.
36
39
  self.grand_total: float = self.safe_parse(self._parse_grand_total)
37
40
  #: The Transaction was a refund or not.
@@ -91,3 +94,11 @@ class Transaction(Parsable):
91
94
  value = f"{self.config.constants.ORDER_DETAILS_URL}?orderID={self.order_number}"
92
95
 
93
96
  return value
97
+
98
+ def _parse_payment_method_last_4(self) -> Optional[str]:
99
+ if not self.payment_method:
100
+ return None
101
+
102
+ match = re.search(r"\*+(\d+)$", self.payment_method)
103
+
104
+ return match.group(1) if match else None
@@ -451,6 +451,23 @@ class CaptchaForm(AuthForm):
451
451
  self.data.update(additional_attrs)
452
452
 
453
453
 
454
+ class AcicAuthBlocker(AuthForm):
455
+ def __init__(self,
456
+ config: AmazonOrdersConfig) -> None:
457
+ super().__init__(config, None)
458
+
459
+ def select_form(self,
460
+ amazon_session: "AmazonSession",
461
+ parsed: Tag) -> bool:
462
+ if parsed.find(id="aa-challenge-page-captcha-container"):
463
+ raise AmazonOrdersAuthError(
464
+ "Amazon returned a JavaScript-based authentication challenge. Install the [browser] extra to "
465
+ "handle this automatically: `pip install amazon-orders[browser]`, then `playwright install chromium`. "
466
+ "See https://amazon-orders.readthedocs.io/browser.html for details.")
467
+
468
+ return False
469
+
470
+
454
471
  class JSAuthBlocker(AuthForm):
455
472
  def __init__(self,
456
473
  config: AmazonOrdersConfig,
@@ -467,8 +484,8 @@ class JSAuthBlocker(AuthForm):
467
484
 
468
485
  if re.search(self.regex, parsed.text):
469
486
  raise AmazonOrdersAuthError(
470
- "A JavaScript-based authentication challenge page has been found. This library cannot solve these "
471
- "challenges. See "
472
- "https://amazon-orders.readthedocs.io/troubleshooting.html#captcha-blocking-login for more details.")
487
+ "Amazon returned a JavaScript-based authentication challenge. Install the [browser] extra to "
488
+ "handle this automatically: `pip install amazon-orders[browser]`, then `playwright install chromium`. "
489
+ "See https://amazon-orders.readthedocs.io/browser.html for details.")
473
490
 
474
491
  return False
@@ -15,7 +15,7 @@ from requests.utils import dict_from_cookiejar
15
15
 
16
16
  from amazonorders.conf import AmazonOrdersConfig, config_file_lock, cookies_file_lock, debug_output_file_lock
17
17
  from amazonorders.exception import AmazonOrdersAuthError, AmazonOrdersError, AmazonOrdersAuthRedirectError
18
- from amazonorders.forms import (AuthForm, CaptchaForm, JSAuthBlocker, MfaDeviceSelectForm, MfaForm,
18
+ from amazonorders.forms import (AuthForm, AcicAuthBlocker, CaptchaForm, JSAuthBlocker, MfaDeviceSelectForm, MfaForm,
19
19
  SignInForm, ClaimForm, IntentForm)
20
20
  from amazonorders.util import AmazonSessionResponse, load_class
21
21
 
@@ -82,6 +82,7 @@ class AmazonSession:
82
82
  config.set_domain(domain)
83
83
  if not auth_forms:
84
84
  auth_forms = AmazonSession.default_auth_forms(config)
85
+ custom_forms = []
85
86
  for path in config.auth_forms_classes or []:
86
87
  try:
87
88
  module_path, class_name = path.rsplit(".", 1)
@@ -97,8 +98,13 @@ class AmazonSession:
97
98
  )
98
99
  # AuthForm subclasses registered via auth_forms_classes are expected to take
99
100
  # only ``config`` (e.g. AwsWafForm subclasses); the base AuthForm signature
100
- # additionally requires ``selector``
101
- auth_forms.insert(-1, cls(config)) # type: ignore[call-arg]
101
+ # additionally requires ``selector``.
102
+ custom_forms.append(cls(config)) # type: ignore[call-arg]
103
+ # Insert as a block before the blocker pair (AcicAuthBlocker, JSAuthBlocker)
104
+ # so that solver forms run first while preserving auth_forms_classes order.
105
+ insert_pos = len(auth_forms) - 2
106
+ for offset, form_instance in enumerate(custom_forms):
107
+ auth_forms.insert(insert_pos + offset, form_instance)
102
108
 
103
109
  #: An Amazon username. Environment variable ``AMAZON_USERNAME`` will override passed in or config value.
104
110
  self.username: Optional[str] = os.environ.get("AMAZON_USERNAME") or username or config.username
@@ -164,6 +170,7 @@ class AmazonSession:
164
170
  "field-keywords"),
165
171
  MfaForm(config,
166
172
  config.selectors.CAPTCHA_OTP_FORM_SELECTOR),
173
+ AcicAuthBlocker(config),
167
174
  JSAuthBlocker(config,
168
175
  config.constants.JS_ROBOT_TEXT_REGEX)]
169
176
 
@@ -1,9 +1,10 @@
1
1
  [project]
2
2
  name = "amazon-orders"
3
3
  dynamic = ["version"]
4
- description = "A Python libray (and CLI) for Amazon order history"
4
+ description = "A Python library (and CLI) for Amazon order history"
5
5
  readme = "README.md"
6
6
  license = "MIT"
7
+ keywords = ["amazon", "orders", "order-history", "scraper", "web-scraping", "receipts", "cli"]
7
8
  authors = [{ name = "Alex Laird", email = "contact@alexlaird.com" }]
8
9
  maintainers = [{ name = "Alex Laird", email = "contact@alexlaird.com" }]
9
10
  requires-python = ">=3.9"
@@ -12,8 +13,8 @@ dependencies = [
12
13
  "requests>=2.23",
13
14
  "Pillow>=9.0.1",
14
15
  "beautifulsoup4>=4.12",
15
- "PyYAML>=5.1",
16
- "python-dateutil>=2.8",
16
+ "PyYAML>=6.0.3",
17
+ "python-dateutil>=2.9.0.post0",
17
18
  "pyotp>=2.9",
18
19
  ]
19
20
  classifiers = [
@@ -28,7 +29,10 @@ classifiers = [
28
29
  "Programming Language :: Python :: 3.11",
29
30
  "Programming Language :: Python :: 3.12",
30
31
  "Programming Language :: Python :: 3.13",
32
+ "Topic :: Internet :: WWW/HTTP :: Browsers",
33
+ "Topic :: Office/Business :: Financial",
31
34
  "Topic :: Software Development :: Libraries :: Python Modules",
35
+ "Topic :: Text Processing :: Markup :: HTML",
32
36
  ]
33
37
 
34
38
  [project.optional-dependencies]
@@ -44,6 +48,9 @@ anticaptcha = [
44
48
  2captcha = [
45
49
  "2captcha-python",
46
50
  ]
51
+ browser = [
52
+ "playwright>=1.47.0",
53
+ ]
47
54
  lxml = [
48
55
  "lxml",
49
56
  ]
@@ -71,6 +78,8 @@ docs = [
71
78
  "types-Pillow",
72
79
  "types-PyYAML",
73
80
  "types-python-dateutil",
81
+ "sphinx-sitemap",
82
+ "sphinxext-opengraph",
74
83
  ]
75
84
 
76
85
  [project.scripts]
File without changes
File without changes
File without changes