amazon-orders 4.4.5__tar.gz → 4.4.7__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.4.5 → amazon_orders-4.4.7}/CHANGELOG.md +13 -1
- {amazon_orders-4.4.5/amazon_orders.egg-info → amazon_orders-4.4.7}/PKG-INFO +2 -2
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/README.md +1 -1
- {amazon_orders-4.4.5 → amazon_orders-4.4.7/amazon_orders.egg-info}/PKG-INFO +2 -2
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/__init__.py +1 -1
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/constants.py +1 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/browser/playwright.py +58 -13
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/orders.py +28 -17
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/LICENSE +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/MANIFEST.in +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/SOURCES.txt +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/dependency_links.txt +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/entry_points.txt +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/requires.txt +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/top_level.txt +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/banner.txt +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/cli.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/conf.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/__init__.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/browser/__init__.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/__init__.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/anticaptcha.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/base.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/capsolver.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/twocaptcha.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/__init__.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/item.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/order.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/parsable.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/recipient.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/seller.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/shipment.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/transaction.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/exception.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/forms.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/selectors.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/session.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/transactions.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/util.py +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/pyproject.toml +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/setup.cfg +0 -0
- {amazon_orders-4.4.5 → amazon_orders-4.4.7}/tests/testcase.py +0 -0
|
@@ -4,7 +4,19 @@ 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.4.
|
|
7
|
+
## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.4.7...HEAD)
|
|
8
|
+
|
|
9
|
+
## [4.4.7](https://github.com/alexdlaird/amazon-orders/compare/4.4.6...4.4.7) - 2026-08-02
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- `get_order()` now fetches Whole Foods Market orders via their dedicated details page instead of raising `AmazonOrdersNotFoundError` on redirect.
|
|
14
|
+
|
|
15
|
+
## [4.4.6](https://github.com/alexdlaird/amazon-orders/compare/4.4.5...4.4.6) - 2026-07-20
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- `PlaywrightManualWafForm` alongside `PlaywrightAcicForm` now opens a visible browser to solve ACIC-embedded challenges manually.
|
|
8
20
|
|
|
9
21
|
## [4.4.5](https://github.com/alexdlaird/amazon-orders/compare/4.4.4...4.4.5) - 2026-07-15
|
|
10
22
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.4.
|
|
3
|
+
Version: 4.4.7
|
|
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>
|
|
@@ -72,7 +72,7 @@ Requires-Dist: sphinx-sitemap; extra == "docs"
|
|
|
72
72
|
Requires-Dist: sphinxext-opengraph; extra == "docs"
|
|
73
73
|
Dynamic: license-file
|
|
74
74
|
|
|
75
|
-
<p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
|
|
75
|
+
<p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/en/latest/_images/logo.png" /></p>
|
|
76
76
|
|
|
77
77
|
[](https://pypi.org/project/amazon-orders)
|
|
78
78
|
[](https://pypi.org/project/amazon-orders)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
|
|
1
|
+
<p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/en/latest/_images/logo.png" /></p>
|
|
2
2
|
|
|
3
3
|
[](https://pypi.org/project/amazon-orders)
|
|
4
4
|
[](https://pypi.org/project/amazon-orders)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.4.
|
|
3
|
+
Version: 4.4.7
|
|
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>
|
|
@@ -72,7 +72,7 @@ Requires-Dist: sphinx-sitemap; extra == "docs"
|
|
|
72
72
|
Requires-Dist: sphinxext-opengraph; extra == "docs"
|
|
73
73
|
Dynamic: license-file
|
|
74
74
|
|
|
75
|
-
<p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/_images/logo.png" /></p>
|
|
75
|
+
<p align="center"><img alt="amazon-orders - A Python library (and CLI) for Amazon order history" src="https://amazon-orders.readthedocs.io/en/latest/_images/logo.png" /></p>
|
|
76
76
|
|
|
77
77
|
[](https://pypi.org/project/amazon-orders)
|
|
78
78
|
[](https://pypi.org/project/amazon-orders)
|
|
@@ -113,6 +113,7 @@ class Constants:
|
|
|
113
113
|
ORDER_INVOICE_URL = f"{BASE_URL}/gp/css/summary/print.html"
|
|
114
114
|
HISTORY_FILTER_QUERY_PARAM = "timeFilter"
|
|
115
115
|
ORDER_FILTER_QUERY_PARAM = "orderFilter"
|
|
116
|
+
WHOLE_FOODS_DETAILS_ROUTES = ["/fopo/order-details", "/wholefoodsmarket/receipts/order/"]
|
|
116
117
|
|
|
117
118
|
##########################################################################
|
|
118
119
|
# URLs for Transactions
|
|
@@ -19,6 +19,7 @@ from amazonorders.forms import AuthForm
|
|
|
19
19
|
from amazonorders.util import AmazonSessionResponse
|
|
20
20
|
|
|
21
21
|
if TYPE_CHECKING:
|
|
22
|
+
from amazonorders.contrib.waf.base import AwsWafForm
|
|
22
23
|
from amazonorders.session import AmazonSession
|
|
23
24
|
|
|
24
25
|
logger = logging.getLogger(__name__)
|
|
@@ -44,6 +45,10 @@ class PlaywrightAuthForm(AuthForm):
|
|
|
44
45
|
#: Whether to launch the browser in headless mode. Defaults to ``True``.
|
|
45
46
|
#: Set to ``False`` in subclasses that require user interaction.
|
|
46
47
|
self.headless: bool = True
|
|
48
|
+
#: Whether this form solves challenges by handing off to a human in a visible
|
|
49
|
+
#: browser window (as opposed to an automated third-party solver). Forms with
|
|
50
|
+
#: this set act as the free, manual solver for an embedded ACIC challenge.
|
|
51
|
+
self.manual: bool = False
|
|
47
52
|
|
|
48
53
|
def fill_form(self,
|
|
49
54
|
additional_attrs: Optional[Dict[str, Any]] = None) -> None:
|
|
@@ -197,10 +202,16 @@ class PlaywrightAuthForm(AuthForm):
|
|
|
197
202
|
class PlaywrightAcicForm(PlaywrightAuthForm):
|
|
198
203
|
"""
|
|
199
204
|
Handles Amazon's ACIC (Amazon Challenge and Identity Component) JavaScript challenge
|
|
200
|
-
by running it in a headless browser. If an embedded AWS WAF challenge
|
|
201
|
-
the ACIC page, it will be solved automatically using the first
|
|
205
|
+
by running it in a headless browser. If an embedded AWS WAF challenge or visual grid
|
|
206
|
+
Puzzle is present on the ACIC page, it will be solved automatically using the first
|
|
202
207
|
:class:`~amazonorders.contrib.waf.base.AwsWafForm` found in ``auth_forms_classes``.
|
|
203
208
|
|
|
209
|
+
If no automated solver is registered but a manual solver
|
|
210
|
+
(:class:`~amazonorders.contrib.browser.playwright.PlaywrightManualWafForm`) is, a
|
|
211
|
+
**visible** browser window is opened instead so the user can solve the embedded
|
|
212
|
+
challenge themselves, for free. An automated solver takes precedence when both are
|
|
213
|
+
registered.
|
|
214
|
+
|
|
204
215
|
Detects the challenge via the ``#aa-challenge-page-captcha-container`` element and
|
|
205
216
|
waits for navigation away from ``/ax/aaut/verify/ap/challenge``.
|
|
206
217
|
|
|
@@ -224,9 +235,50 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
|
|
|
224
235
|
:return: ``True`` if an ACIC challenge was detected, ``False`` otherwise.
|
|
225
236
|
"""
|
|
226
237
|
self.amazon_session = amazon_session
|
|
227
|
-
|
|
238
|
+
detected = bool(parsed.select_one(self.config.selectors.ACIC_CHALLENGE_SELECTOR))
|
|
239
|
+
if detected:
|
|
240
|
+
self.headless = not self._manual_mode()
|
|
241
|
+
return detected
|
|
242
|
+
|
|
243
|
+
def _manual_mode(self) -> bool:
|
|
244
|
+
"""
|
|
245
|
+
Return ``True`` if the embedded challenge should be handed off to a human in a
|
|
246
|
+
visible browser window rather than an automated solver. This is the case when a
|
|
247
|
+
manual solver form is registered and no automated :class:`AwsWafForm` is; an
|
|
248
|
+
automated solver takes precedence when both are present, since it is
|
|
249
|
+
non-interactive.
|
|
250
|
+
"""
|
|
251
|
+
return self._find_manual_solver() is not None and self._find_waf_solver() is None
|
|
252
|
+
|
|
253
|
+
def _find_waf_solver(self) -> Optional["AwsWafForm"]:
|
|
254
|
+
if not self.amazon_session:
|
|
255
|
+
return None # pragma: no cover
|
|
256
|
+
from amazonorders.contrib.waf.base import AwsWafForm
|
|
257
|
+
return next(
|
|
258
|
+
(f for f in self.amazon_session.auth_forms if isinstance(f, AwsWafForm)),
|
|
259
|
+
None,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
def _find_manual_solver(self) -> Optional["PlaywrightAuthForm"]:
|
|
263
|
+
if not self.amazon_session:
|
|
264
|
+
return None # pragma: no cover
|
|
265
|
+
return next(
|
|
266
|
+
(f for f in self.amazon_session.auth_forms
|
|
267
|
+
if isinstance(f, PlaywrightAuthForm) and f.manual),
|
|
268
|
+
None,
|
|
269
|
+
)
|
|
228
270
|
|
|
229
271
|
def _on_challenge_page(self, page: Any, context: Any, output_dir: Optional[str]) -> None:
|
|
272
|
+
if self._manual_mode():
|
|
273
|
+
message = (
|
|
274
|
+
"Info: A browser window has opened — solve the challenge, then return "
|
|
275
|
+
"here when done."
|
|
276
|
+
)
|
|
277
|
+
logger.info(message)
|
|
278
|
+
if self.amazon_session:
|
|
279
|
+
self.amazon_session.io.echo(message)
|
|
280
|
+
return
|
|
281
|
+
|
|
230
282
|
max_solves = self.config.max_auth_attempts
|
|
231
283
|
solves = 0
|
|
232
284
|
while solves < max_solves:
|
|
@@ -281,11 +333,7 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
|
|
|
281
333
|
if not self.amazon_session:
|
|
282
334
|
return False # pragma: no cover
|
|
283
335
|
|
|
284
|
-
|
|
285
|
-
waf_form = next(
|
|
286
|
-
(f for f in self.amazon_session.auth_forms if isinstance(f, AwsWafForm)),
|
|
287
|
-
None,
|
|
288
|
-
)
|
|
336
|
+
waf_form = self._find_waf_solver()
|
|
289
337
|
if not waf_form:
|
|
290
338
|
logger.debug("No AwsWafForm configured, skipping WAF challenge.")
|
|
291
339
|
return False
|
|
@@ -339,11 +387,7 @@ class PlaywrightAcicForm(PlaywrightAuthForm):
|
|
|
339
387
|
if not self.amazon_session:
|
|
340
388
|
return False # pragma: no cover
|
|
341
389
|
|
|
342
|
-
|
|
343
|
-
waf_form = next(
|
|
344
|
-
(f for f in self.amazon_session.auth_forms if isinstance(f, AwsWafForm)),
|
|
345
|
-
None,
|
|
346
|
-
)
|
|
390
|
+
waf_form = self._find_waf_solver()
|
|
347
391
|
if not waf_form:
|
|
348
392
|
logger.debug("No AwsWafForm configured, skipping Puzzle.")
|
|
349
393
|
return False
|
|
@@ -502,6 +546,7 @@ class PlaywrightManualWafForm(PlaywrightAuthForm):
|
|
|
502
546
|
config: AmazonOrdersConfig) -> None:
|
|
503
547
|
super().__init__(config)
|
|
504
548
|
self.headless = False
|
|
549
|
+
self.manual = True
|
|
505
550
|
|
|
506
551
|
def select_form(self,
|
|
507
552
|
amazon_session: "AmazonSession",
|
|
@@ -63,7 +63,12 @@ class AmazonOrders:
|
|
|
63
63
|
f"{self.config.constants.ORDER_DETAILS_URL}?orderID={order_id}")
|
|
64
64
|
self.amazon_session.check_response(order_details_response, meta=meta)
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
response_url = order_details_response.response.url
|
|
67
|
+
if not response_url.startswith(self.config.constants.ORDER_DETAILS_URL):
|
|
68
|
+
if self._is_whole_foods_details_url(response_url):
|
|
69
|
+
# GET already followed the redirect, so order_details_response is the FOPO/receipt page.
|
|
70
|
+
return self._get_whole_foods_order(order_details_response, order_number=order_id, clone=clone)
|
|
71
|
+
|
|
67
72
|
raise AmazonOrdersNotFoundError(f"Amazon redirected, which likely means Order {order_id} was not found.",
|
|
68
73
|
meta=meta)
|
|
69
74
|
|
|
@@ -217,12 +222,12 @@ class AmazonOrders:
|
|
|
217
222
|
|
|
218
223
|
if full_details:
|
|
219
224
|
if order.is_whole_foods:
|
|
220
|
-
# Whole Foods orders are served by a dedicated details page (the in-store/FOPO page or the
|
|
221
|
-
# receipt page), never the standard order-details endpoint, so only fetch full details when
|
|
222
|
-
# the history page linked us to one of those routes.
|
|
223
225
|
link = order.order_details_link or ""
|
|
224
|
-
if
|
|
225
|
-
|
|
226
|
+
if self._is_whole_foods_details_url(link):
|
|
227
|
+
details_response = self.amazon_session.get(link)
|
|
228
|
+
self.amazon_session.check_response(details_response, meta={"index": order.index})
|
|
229
|
+
order = self._get_whole_foods_order(details_response, order_number=order.order_number,
|
|
230
|
+
clone=order)
|
|
226
231
|
else:
|
|
227
232
|
logger.warning(f"Order {order.order_number} is a Whole Foods Market order whose details "
|
|
228
233
|
f"page could not be located, so it was left partially populated.")
|
|
@@ -237,23 +242,29 @@ class AmazonOrders:
|
|
|
237
242
|
|
|
238
243
|
return order
|
|
239
244
|
|
|
240
|
-
def
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
"""Fetches the WFM dedicated details page and returns the order enriched with per-item details."""
|
|
244
|
-
details_response = self.amazon_session.get(url)
|
|
245
|
-
self.amazon_session.check_response(details_response, meta={"index": order.index})
|
|
245
|
+
def _is_whole_foods_details_url(self,
|
|
246
|
+
url: str) -> bool:
|
|
247
|
+
return any(route in url for route in self.config.constants.WHOLE_FOODS_DETAILS_ROUTES)
|
|
246
248
|
|
|
249
|
+
def _get_whole_foods_order(self,
|
|
250
|
+
details_response: util.AmazonSessionResponse,
|
|
251
|
+
order_number: Optional[str] = None,
|
|
252
|
+
clone: Optional[Order] = None) -> Order:
|
|
253
|
+
"""Builds an Order from an already-fetched Whole Foods Market details page response."""
|
|
247
254
|
details_tag = util.select_one(details_response.parsed,
|
|
248
255
|
self.config.selectors.ORDER_DETAILS_ENTITY_SELECTOR)
|
|
249
256
|
|
|
250
257
|
if not details_tag:
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
258
|
+
if clone:
|
|
259
|
+
logger.warning(f"Could not parse Whole Foods Market details for Order {clone.order_number}, "
|
|
260
|
+
f"so it was left partially populated.")
|
|
261
|
+
return clone
|
|
262
|
+
|
|
263
|
+
raise AmazonOrdersError(f"Could not parse Whole Foods Market details for Order {order_number}. "
|
|
264
|
+
f"Check if Amazon changed the HTML.")
|
|
254
265
|
|
|
255
|
-
return self.config.order_cls(details_tag, self.config, full_details=True, clone=
|
|
256
|
-
order_number=
|
|
266
|
+
return self.config.order_cls(details_tag, self.config, full_details=True, clone=clone,
|
|
267
|
+
order_number=order_number)
|
|
257
268
|
|
|
258
269
|
async def _async_wrapper(self,
|
|
259
270
|
func: Callable,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|