amazon-orders 4.2.2__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.
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/CHANGELOG.md +17 -1
- {amazon_orders-4.2.2/amazon_orders.egg-info → amazon_orders-4.3.0}/PKG-INFO +15 -3
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/README.md +12 -2
- {amazon_orders-4.2.2 → amazon_orders-4.3.0/amazon_orders.egg-info}/PKG-INFO +15 -3
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazon_orders.egg-info/SOURCES.txt +2 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazon_orders.egg-info/requires.txt +3 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/__init__.py +1 -1
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/constants.py +56 -23
- amazon_orders-4.3.0/amazonorders/contrib/browser/playwright.py +412 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/contrib/waf/anticaptcha.py +1 -1
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/contrib/waf/capsolver.py +1 -1
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/contrib/waf/twocaptcha.py +1 -1
- amazon_orders-4.3.0/amazonorders/entity/__init__.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/transaction.py +11 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/forms.py +6 -4
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/session.py +8 -2
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/pyproject.toml +3 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/LICENSE +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/MANIFEST.in +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazon_orders.egg-info/dependency_links.txt +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazon_orders.egg-info/entry_points.txt +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazon_orders.egg-info/top_level.txt +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/banner.txt +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/cli.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/conf.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/contrib/__init__.py +0 -0
- {amazon_orders-4.2.2/amazonorders/contrib/waf → amazon_orders-4.3.0/amazonorders/contrib/browser}/__init__.py +0 -0
- {amazon_orders-4.2.2/amazonorders/entity → amazon_orders-4.3.0/amazonorders/contrib/waf}/__init__.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/contrib/waf/base.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/item.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/order.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/parsable.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/recipient.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/seller.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/entity/shipment.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/exception.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/orders.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/selectors.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/transactions.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/amazonorders/util.py +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/setup.cfg +0 -0
- {amazon_orders-4.2.2 → amazon_orders-4.3.0}/tests/testcase.py +0 -0
|
@@ -4,7 +4,23 @@ 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.
|
|
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.
|
|
8
24
|
|
|
9
25
|
## [4.2.2](https://github.com/alexdlaird/amazon-orders/compare/4.2.1...4.2.2) - 2026-06-06
|
|
10
26
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.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>
|
|
@@ -43,6 +43,8 @@ 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"
|
|
46
48
|
Provides-Extra: lxml
|
|
47
49
|
Requires-Dist: lxml; extra == "lxml"
|
|
48
50
|
Provides-Extra: dev
|
|
@@ -101,7 +103,7 @@ pip install amazon-orders --upgrade
|
|
|
101
103
|
|
|
102
104
|
That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
|
|
103
105
|
|
|
104
|
-
If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.
|
|
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
107
|
ensure you always get the latest stable release.
|
|
106
108
|
|
|
107
109
|
## Basic Usage
|
|
@@ -170,6 +172,16 @@ pip install amazon-orders[2captcha]
|
|
|
170
172
|
|
|
171
173
|
See [Solving WAF Challenges](https://amazon-orders.readthedocs.io/waf.html) for details.
|
|
172
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
|
+
|
|
173
185
|
To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
|
|
174
186
|
dependency), install with the `captcha` extra:
|
|
175
187
|
|
|
@@ -178,7 +190,7 @@ pip install amazon-orders[captcha]
|
|
|
178
190
|
```
|
|
179
191
|
|
|
180
192
|
Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
|
|
181
|
-
[
|
|
193
|
+
[Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
|
|
182
194
|
|
|
183
195
|
## Documentation
|
|
184
196
|
|
|
@@ -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.
|
|
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
|
-
[
|
|
118
|
+
[Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
|
|
109
119
|
|
|
110
120
|
## Documentation
|
|
111
121
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.3.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>
|
|
@@ -43,6 +43,8 @@ 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"
|
|
46
48
|
Provides-Extra: lxml
|
|
47
49
|
Requires-Dist: lxml; extra == "lxml"
|
|
48
50
|
Provides-Extra: dev
|
|
@@ -101,7 +103,7 @@ pip install amazon-orders --upgrade
|
|
|
101
103
|
|
|
102
104
|
That's it! `amazon-orders` is now available as a package to your Python projects and from the command line.
|
|
103
105
|
|
|
104
|
-
If pinning, be sure to use a wildcard for the [minor version](https://semver.org/) (ex. `==4.
|
|
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
107
|
ensure you always get the latest stable release.
|
|
106
108
|
|
|
107
109
|
## Basic Usage
|
|
@@ -170,6 +172,16 @@ pip install amazon-orders[2captcha]
|
|
|
170
172
|
|
|
171
173
|
See [Solving WAF Challenges](https://amazon-orders.readthedocs.io/waf.html) for details.
|
|
172
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
|
+
|
|
173
185
|
To enable **Captcha auto-solve** on Python <=3.12 (via the optional [`amazoncaptcha`](https://pypi.org/project/amazoncaptcha/)
|
|
174
186
|
dependency), install with the `captcha` extra:
|
|
175
187
|
|
|
@@ -178,7 +190,7 @@ pip install amazon-orders[captcha]
|
|
|
178
190
|
```
|
|
179
191
|
|
|
180
192
|
Without this extra, Captcha challenges fall back to manual entry. `amazoncaptcha` is not available on Python 3.13+; see
|
|
181
|
-
[
|
|
193
|
+
[Login Challenges](https://amazon-orders.readthedocs.io/troubleshooting.html#login-challenges) for details.
|
|
182
194
|
|
|
183
195
|
## Documentation
|
|
184
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
|
|
@@ -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
|
-
"
|
|
131
|
+
"Origin": BASE_URL,
|
|
120
132
|
"Referer": f"{SIGN_IN_URL}?{urlencode(SIGN_IN_QUERY_PARAMS)}",
|
|
121
|
-
"
|
|
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
|
|
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
|
-
#
|
|
172
|
-
#
|
|
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
|
|
@@ -461,8 +461,9 @@ class AcicAuthBlocker(AuthForm):
|
|
|
461
461
|
parsed: Tag) -> bool:
|
|
462
462
|
if parsed.find(id="aa-challenge-page-captcha-container"):
|
|
463
463
|
raise AmazonOrdersAuthError(
|
|
464
|
-
"Amazon returned a JavaScript-based authentication challenge
|
|
465
|
-
"
|
|
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.")
|
|
466
467
|
|
|
467
468
|
return False
|
|
468
469
|
|
|
@@ -483,7 +484,8 @@ class JSAuthBlocker(AuthForm):
|
|
|
483
484
|
|
|
484
485
|
if re.search(self.regex, parsed.text):
|
|
485
486
|
raise AmazonOrdersAuthError(
|
|
486
|
-
"Amazon returned a JavaScript-based authentication challenge
|
|
487
|
-
"
|
|
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.")
|
|
488
490
|
|
|
489
491
|
return False
|
|
@@ -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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{amazon_orders-4.2.2/amazonorders/entity → amazon_orders-4.3.0/amazonorders/contrib/waf}/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|