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.
Files changed (42) hide show
  1. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/CHANGELOG.md +13 -1
  2. {amazon_orders-4.4.5/amazon_orders.egg-info → amazon_orders-4.4.7}/PKG-INFO +2 -2
  3. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/README.md +1 -1
  4. {amazon_orders-4.4.5 → amazon_orders-4.4.7/amazon_orders.egg-info}/PKG-INFO +2 -2
  5. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/__init__.py +1 -1
  6. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/constants.py +1 -0
  7. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/browser/playwright.py +58 -13
  8. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/orders.py +28 -17
  9. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/LICENSE +0 -0
  10. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/MANIFEST.in +0 -0
  11. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/SOURCES.txt +0 -0
  12. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/dependency_links.txt +0 -0
  13. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/entry_points.txt +0 -0
  14. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/requires.txt +0 -0
  15. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazon_orders.egg-info/top_level.txt +0 -0
  16. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/banner.txt +0 -0
  17. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/cli.py +0 -0
  18. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/conf.py +0 -0
  19. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/__init__.py +0 -0
  20. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/browser/__init__.py +0 -0
  21. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/__init__.py +0 -0
  22. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/anticaptcha.py +0 -0
  23. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/base.py +0 -0
  24. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/capsolver.py +0 -0
  25. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/contrib/waf/twocaptcha.py +0 -0
  26. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/__init__.py +0 -0
  27. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/item.py +0 -0
  28. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/order.py +0 -0
  29. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/parsable.py +0 -0
  30. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/recipient.py +0 -0
  31. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/seller.py +0 -0
  32. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/shipment.py +0 -0
  33. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/entity/transaction.py +0 -0
  34. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/exception.py +0 -0
  35. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/forms.py +0 -0
  36. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/selectors.py +0 -0
  37. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/session.py +0 -0
  38. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/transactions.py +0 -0
  39. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/amazonorders/util.py +0 -0
  40. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/pyproject.toml +0 -0
  41. {amazon_orders-4.4.5 → amazon_orders-4.4.7}/setup.cfg +0 -0
  42. {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.5...HEAD)
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.5
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
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
78
78
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](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
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
4
4
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.4.5
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
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
78
78
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
@@ -1,3 +1,3 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
- __version__ = "4.4.5"
3
+ __version__ = "4.4.7"
@@ -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 is present on
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
- return bool(parsed.select_one(self.config.selectors.ACIC_CHALLENGE_SELECTOR))
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
- from amazonorders.contrib.waf.base import AwsWafForm
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
- 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
- )
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
- if not order_details_response.response.url.startswith(self.config.constants.ORDER_DETAILS_URL):
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 "/fopo/order-details" in link or "/wholefoodsmarket/receipts/order/" in link:
225
- order = self._get_whole_foods_order(order, link)
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 _get_whole_foods_order(self,
241
- order: Order,
242
- url: str) -> Order:
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
- logger.warning(f"Could not parse Whole Foods Market details for Order {order.order_number}, "
252
- f"so it was left partially populated.")
253
- return order
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=order,
256
- order_number=order.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