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