amazon-orders 4.3.1__tar.gz → 4.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/CHANGELOG.md +16 -1
- {amazon_orders-4.3.1/amazon_orders.egg-info → amazon_orders-4.4.0}/PKG-INFO +6 -7
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/README.md +1 -1
- {amazon_orders-4.3.1 → amazon_orders-4.4.0/amazon_orders.egg-info}/PKG-INFO +6 -7
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/requires.txt +0 -3
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/__init__.py +1 -1
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/conf.py +2 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/constants.py +2 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/browser/playwright.py +146 -18
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/anticaptcha.py +1 -1
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/base.py +19 -4
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/capsolver.py +52 -2
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/twocaptcha.py +1 -1
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/order.py +1 -1
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/selectors.py +7 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/pyproject.toml +17 -6
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/LICENSE +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/MANIFEST.in +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/SOURCES.txt +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/dependency_links.txt +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/entry_points.txt +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazon_orders.egg-info/top_level.txt +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/banner.txt +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/cli.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/__init__.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/browser/__init__.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/contrib/waf/__init__.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/__init__.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/item.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/parsable.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/recipient.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/seller.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/shipment.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/entity/transaction.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/exception.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/forms.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/orders.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/session.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/transactions.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/amazonorders/util.py +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/setup.cfg +0 -0
- {amazon_orders-4.3.1 → amazon_orders-4.4.0}/tests/testcase.py +0 -0
|
@@ -4,7 +4,17 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
-
## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.
|
|
7
|
+
## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.4.0...HEAD)
|
|
8
|
+
|
|
9
|
+
## [4.4.0](https://github.com/alexdlaird/amazon-orders/compare/4.3.1...4.4.0) - 2026-06-12
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `PlaywrightAcicForm` now solves visual grid Puzzles (e.g. "Choose all the buckets") automatically when a WAF solver extra is configured. Puzzles are detected and solved in a loop alongside the existing WAF token challenge, handling any combination Amazon presents.
|
|
14
|
+
- `AwsWafForm._solve_visual_captcha()` subclass hook for Puzzle classification. `CapSolverWafForm` implements this via the `AwsWafClassification` task type.
|
|
15
|
+
- `browser_timeout` config key (defaults to 30s) controlling how long the browser waits for challenge elements to appear.
|
|
16
|
+
- `retry_failures` input on the integration test workflow for fail-fast CI runs.
|
|
17
|
+
- Build and stability improvements.
|
|
8
18
|
|
|
9
19
|
## [4.3.1](https://github.com/alexdlaird/amazon-orders/compare/4.3.0...4.3.1) - 2026-06-07
|
|
10
20
|
|
|
@@ -13,6 +23,11 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
13
23
|
- `AmazonOrders.get_invoice()` to fetch an Order's print-friendly invoice page, returning the response (including its parsed HTML) for rendering or printing.
|
|
14
24
|
- `AmazonTransactions.get_transactions()` `order_id` parameter to scope results to a single Order server-side via Amazon's `transactionTag` filter, bypassing the `days` window.
|
|
15
25
|
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- `AmazonOrders.get_invoice()` to fetch an Order's print-friendly invoice page, returning the response (including its parsed HTML) for rendering or printing.
|
|
29
|
+
- `AmazonTransactions.get_transactions()` `order_id` parameter to scope results to a single Order server-side via Amazon's `transactionTag` filter, bypassing the `days` window.
|
|
30
|
+
|
|
16
31
|
## [4.3.0](https://github.com/alexdlaird/amazon-orders/compare/4.2.2...4.3.0) - 2026-06-07
|
|
17
32
|
|
|
18
33
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.
|
|
3
|
+
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -58,6 +58,8 @@ class AmazonOrdersConfig:
|
|
|
58
58
|
"item_class": "amazonorders.entity.item.Item",
|
|
59
59
|
"bs4_parser": "html.parser",
|
|
60
60
|
"auth_forms_classes": [],
|
|
61
|
+
# Timeout in seconds for browser-based challenge detection and resolution
|
|
62
|
+
"browser_timeout": 30,
|
|
61
63
|
"thread_pool_size": (os.cpu_count() or 1) * 4,
|
|
62
64
|
"connection_pool_size": thread_pool_size * 2,
|
|
63
65
|
# The maximum number of failed attempts to allow before failing CLI authentication
|
|
@@ -148,6 +148,8 @@ class Constants:
|
|
|
148
148
|
|
|
149
149
|
COOKIES_SET_WHEN_AUTHENTICATED = ["x-main"]
|
|
150
150
|
JS_ROBOT_TEXT_REGEX = r"[.\s\S]*verify that you're not a robot[.\s\S]*Enable JavaScript[.\s\S]*"
|
|
151
|
+
GOKU_PROPS_REGEX = r"window\.gokuProps\s*=\s*(\{.*?\});"
|
|
152
|
+
ACIC_CHALLENGE_PATH = "/ax/aaut/verify/ap/challenge"
|
|
151
153
|
|
|
152
154
|
##########################################################################
|
|
153
155
|
# Currency
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
__copyright__ = "Copyright (c) 2024-2025 Alex Laird"
|
|
2
2
|
__license__ = "MIT"
|
|
3
3
|
|
|
4
|
+
import base64
|
|
4
5
|
import json
|
|
5
6
|
import logging
|
|
6
7
|
import os
|
|
@@ -13,7 +14,6 @@ from bs4 import Tag
|
|
|
13
14
|
from requests import Response
|
|
14
15
|
|
|
15
16
|
from amazonorders.conf import AmazonOrdersConfig
|
|
16
|
-
from amazonorders.contrib.waf.base import _GOKU_PROPS_RE
|
|
17
17
|
from amazonorders.exception import AmazonOrdersError
|
|
18
18
|
from amazonorders.forms import AuthForm
|
|
19
19
|
from amazonorders.util import AmazonSessionResponse
|
|
@@ -81,8 +81,6 @@ class PlaywrightAuthForm(AuthForm):
|
|
|
81
81
|
) from e
|
|
82
82
|
|
|
83
83
|
debug = self.amazon_session.debug
|
|
84
|
-
if debug:
|
|
85
|
-
logger.setLevel(logging.DEBUG)
|
|
86
84
|
|
|
87
85
|
message = "Info: A browser is handling a JavaScript authentication challenge."
|
|
88
86
|
logger.info(message)
|
|
@@ -113,7 +111,7 @@ class PlaywrightAuthForm(AuthForm):
|
|
|
113
111
|
try:
|
|
114
112
|
page.wait_for_url(
|
|
115
113
|
lambda url: not self._is_challenge_url(url, original_url),
|
|
116
|
-
timeout=
|
|
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
|
|
@@ -44,6 +44,13 @@ class Selectors:
|
|
|
44
44
|
# CSS selectors for AuthForms
|
|
45
45
|
##########################################################################
|
|
46
46
|
|
|
47
|
+
ACIC_CHALLENGE_SELECTOR = "#aa-challenge-page-captcha-container"
|
|
48
|
+
ACIC_VISUAL_CAPTCHA_MODAL_SELECTOR = ".amzn-captcha-modal"
|
|
49
|
+
ACIC_VISUAL_CAPTCHA_CANVAS_SELECTOR = ".amzn-captcha-modal canvas"
|
|
50
|
+
ACIC_VISUAL_CAPTCHA_QUESTION_SELECTOR = ".amzn-captcha-modal em"
|
|
51
|
+
ACIC_VISUAL_CAPTCHA_VERIFY_SELECTOR = "#amzn-btn-verify-internal"
|
|
52
|
+
AWS_WAF_CHALLENGE_SCRIPT_SELECTOR = 'script[src*="awswaf.com"]'
|
|
53
|
+
|
|
47
54
|
SIGN_IN_FORM_SELECTOR = "form[name='signIn']"
|
|
48
55
|
CLAIM_FORM_SELECTOR = "form[name='signIn'].auth-validate-form"
|
|
49
56
|
INTENT_FORM_SELECTOR = "form#intent-confirmation-form"
|
|
@@ -36,8 +36,8 @@ classifiers = [
|
|
|
36
36
|
]
|
|
37
37
|
|
|
38
38
|
[project.optional-dependencies]
|
|
39
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|