amazon-orders 4.9.3__tar.gz → 4.9.4__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 (49) hide show
  1. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/CHANGELOG.md +13 -2
  2. {amazon_orders-4.9.3/amazon_orders.egg-info → amazon_orders-4.9.4}/PKG-INFO +6 -4
  3. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/README.md +5 -3
  4. {amazon_orders-4.9.3 → amazon_orders-4.9.4/amazon_orders.egg-info}/PKG-INFO +6 -4
  5. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/SOURCES.txt +1 -0
  6. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/__init__.py +1 -1
  7. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/cli.py +38 -0
  8. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/constants.py +1 -0
  9. amazon_orders-4.9.4/amazonorders/entity/family_member.py +48 -0
  10. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/order.py +12 -1
  11. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/forms.py +89 -10
  12. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/orders.py +60 -6
  13. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/output.py +2 -2
  14. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/selectors.py +20 -6
  15. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/session.py +2 -1
  16. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/LICENSE +0 -0
  17. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/MANIFEST.in +0 -0
  18. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/dependency_links.txt +0 -0
  19. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/entry_points.txt +0 -0
  20. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/requires.txt +0 -0
  21. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/top_level.txt +0 -0
  22. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/banner.txt +0 -0
  23. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/conf.py +0 -0
  24. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/__init__.py +0 -0
  25. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/browser/__init__.py +0 -0
  26. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/browser/playwright.py +0 -0
  27. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/__init__.py +0 -0
  28. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/anticaptcha.py +0 -0
  29. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/base.py +0 -0
  30. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/capsolver.py +0 -0
  31. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/twocaptcha.py +0 -0
  32. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/__init__.py +0 -0
  33. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/gift_card_activity.py +0 -0
  34. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/item.py +0 -0
  35. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/parsable.py +0 -0
  36. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/recipient.py +0 -0
  37. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/seller.py +0 -0
  38. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/shipment.py +0 -0
  39. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/tracking.py +0 -0
  40. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/transaction.py +0 -0
  41. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/exception.py +0 -0
  42. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/gift_cards.py +0 -0
  43. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/paging.py +0 -0
  44. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/region_data.py +0 -0
  45. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/transactions.py +0 -0
  46. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/util.py +0 -0
  47. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/pyproject.toml +0 -0
  48. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/setup.cfg +0 -0
  49. {amazon_orders-4.9.3 → amazon_orders-4.9.4}/tests/testcase.py +0 -0
@@ -4,13 +4,24 @@ 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.9.3...HEAD)
7
+ ## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/4.9.4...HEAD)
8
+
9
+ ## [4.9.4](https://github.com/alexdlaird/amazon-orders/compare/4.9.3...4.9.4) - 2026-10-19
10
+
11
+ ### Added
12
+
13
+ - `FamilyMember` entity, `AmazonOrders.get_family_members()` and `parse_family_members()`, `family_member_id` on `get_order_history()` and `get_order_history_page()`, and the `family-members` CLI command and `history --family-member-id`, for the Orders of Amazon Household members (e.g. linked Teen accounts).
14
+ - Support for Amazon's new passkey auth flow, continuing with the password.
15
+
16
+ ### Fixed
17
+
18
+ - Bug fixes and stability improvements.
8
19
 
9
20
  ## [4.9.3](https://github.com/alexdlaird/amazon-orders/compare/4.9.2...4.9.3) - 2026-10-18
10
21
 
11
22
  ### Fixed
12
23
 
13
- - Bug and stability fixes.
24
+ - Bug fixes and stability improvements.
14
25
 
15
26
  ## [4.9.2](https://github.com/alexdlaird/amazon-orders/compare/4.9.1...4.9.2) - 2026-10-15
16
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.9.3
3
+ Version: 4.9.4
4
4
  Summary: A Python library (and CLI) for Amazon order history, line items, and transactions
5
5
  Author-email: Alex Laird <contact@alexlaird.com>
6
6
  Maintainer-email: Alex Laird <contact@alexlaird.com>
@@ -77,6 +77,7 @@ Dynamic: license-file
77
77
  <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>
78
78
 
79
79
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
80
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/amazon-orders)](https://pypistats.org/packages/amazon-orders)
80
81
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
81
82
  [![Coverage](https://img.shields.io/codecov/c/github/alexdlaird/amazon-orders)](https://codecov.io/gh/alexdlaird/amazon-orders)
82
83
  [![Build](https://img.shields.io/github/actions/workflow/status/alexdlaird/amazon-orders/build.yml)](https://github.com/alexdlaird/amazon-orders/actions/workflows/build.yml)
@@ -151,9 +152,10 @@ amazon-orders history --last-3-months
151
152
 
152
153
  ### Output Formats
153
154
 
154
- The `history`, `order`, `transactions`, `order-transactions`, and `gift-card-activity` commands accept
155
- `--output`, which renders Orders, Transactions, and Gift Card activity as `text` (the default), `json`,
156
- `yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only the data.
155
+ The `history`, `order`, `transactions`, `order-transactions`, `gift-card-activity`, and `family-members` commands
156
+ accept `--output`, which renders Orders, Transactions, Gift Card activity, and Amazon Household members as `text` (the
157
+ default), `json`, `yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only
158
+ the data.
157
159
 
158
160
  ```sh
159
161
  amazon-orders history --year 2023 --output json > orders.json
@@ -1,6 +1,7 @@
1
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>
2
2
 
3
3
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
4
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/amazon-orders)](https://pypistats.org/packages/amazon-orders)
4
5
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
5
6
  [![Coverage](https://img.shields.io/codecov/c/github/alexdlaird/amazon-orders)](https://codecov.io/gh/alexdlaird/amazon-orders)
6
7
  [![Build](https://img.shields.io/github/actions/workflow/status/alexdlaird/amazon-orders/build.yml)](https://github.com/alexdlaird/amazon-orders/actions/workflows/build.yml)
@@ -75,9 +76,10 @@ amazon-orders history --last-3-months
75
76
 
76
77
  ### Output Formats
77
78
 
78
- The `history`, `order`, `transactions`, `order-transactions`, and `gift-card-activity` commands accept
79
- `--output`, which renders Orders, Transactions, and Gift Card activity as `text` (the default), `json`,
80
- `yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only the data.
79
+ The `history`, `order`, `transactions`, `order-transactions`, `gift-card-activity`, and `family-members` commands
80
+ accept `--output`, which renders Orders, Transactions, Gift Card activity, and Amazon Household members as `text` (the
81
+ default), `json`, `yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only
82
+ the data.
81
83
 
82
84
  ```sh
83
85
  amazon-orders history --year 2023 --output json > orders.json
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: amazon-orders
3
- Version: 4.9.3
3
+ Version: 4.9.4
4
4
  Summary: A Python library (and CLI) for Amazon order history, line items, and transactions
5
5
  Author-email: Alex Laird <contact@alexlaird.com>
6
6
  Maintainer-email: Alex Laird <contact@alexlaird.com>
@@ -77,6 +77,7 @@ Dynamic: license-file
77
77
  <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>
78
78
 
79
79
  [![Version](https://img.shields.io/pypi/v/amazon-orders)](https://pypi.org/project/amazon-orders)
80
+ [![PyPI - Downloads](https://img.shields.io/pypi/dm/amazon-orders)](https://pypistats.org/packages/amazon-orders)
80
81
  [![Python Versions](https://img.shields.io/pypi/pyversions/amazon-orders.svg)](https://pypi.org/project/amazon-orders)
81
82
  [![Coverage](https://img.shields.io/codecov/c/github/alexdlaird/amazon-orders)](https://codecov.io/gh/alexdlaird/amazon-orders)
82
83
  [![Build](https://img.shields.io/github/actions/workflow/status/alexdlaird/amazon-orders/build.yml)](https://github.com/alexdlaird/amazon-orders/actions/workflows/build.yml)
@@ -151,9 +152,10 @@ amazon-orders history --last-3-months
151
152
 
152
153
  ### Output Formats
153
154
 
154
- The `history`, `order`, `transactions`, `order-transactions`, and `gift-card-activity` commands accept
155
- `--output`, which renders Orders, Transactions, and Gift Card activity as `text` (the default), `json`,
156
- `yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only the data.
155
+ The `history`, `order`, `transactions`, `order-transactions`, `gift-card-activity`, and `family-members` commands
156
+ accept `--output`, which renders Orders, Transactions, Gift Card activity, and Amazon Household members as `text` (the
157
+ default), `json`, `yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only
158
+ the data.
157
159
 
158
160
  ```sh
159
161
  amazon-orders history --year 2023 --output json > orders.json
@@ -34,6 +34,7 @@ amazonorders/contrib/waf/base.py
34
34
  amazonorders/contrib/waf/capsolver.py
35
35
  amazonorders/contrib/waf/twocaptcha.py
36
36
  amazonorders/entity/__init__.py
37
+ amazonorders/entity/family_member.py
37
38
  amazonorders/entity/gift_card_activity.py
38
39
  amazonorders/entity/item.py
39
40
  amazonorders/entity/order.py
@@ -1,3 +1,3 @@
1
1
  __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
2
  __license__ = "MIT"
3
- __version__ = "4.9.3"
3
+ __version__ = "4.9.4"
@@ -304,6 +304,9 @@ def amazon_orders_cli(ctx: Context,
304
304
  @click.option("--order-filter", "order_filter", default=None,
305
305
  help="The order type filter to use, e.g. digital-orders for Kindle and other digital purchases, "
306
306
  "which the default history doesn't include.")
307
+ @click.option("--family-member-id", "family_member_id", default=None,
308
+ help="Get the Orders of this Amazon Household member, such as a linked Teen account, instead. "
309
+ "See the family-members command for IDs.")
307
310
  @click.option("-o", "--output", type=click.Choice(OutputFormatter.OUTPUT_FORMATS), default="text",
308
311
  help="The output format. Defaults to text.")
309
312
  def history(ctx: Context,
@@ -323,6 +326,7 @@ def history(ctx: Context,
323
326
  single_page = kwargs["single_page"]
324
327
  full_details = kwargs["full_details"]
325
328
  order_filter = kwargs["order_filter"]
329
+ family_member_id = kwargs["family_member_id"]
326
330
  output = kwargs["output"]
327
331
 
328
332
  exclusive_flags = [year, last_3_months, last_30_days]
@@ -366,6 +370,7 @@ Order History for {filter_description}{optional_start_index}{optional_full_detai
366
370
  keep_paging=not single_page,
367
371
  time_filter=time_filter,
368
372
  order_filter=order_filter,
373
+ family_member_id=family_member_id,
369
374
  progress_with_total=progress)
370
375
  end_time = time.time()
371
376
 
@@ -526,6 +531,39 @@ Transaction History for {days} days
526
531
  ctx.fail(str(e))
527
532
 
528
533
 
534
+ @amazon_orders_cli.command("family-members")
535
+ @click.pass_context
536
+ @click.option("-o", "--output", type=click.Choice(OutputFormatter.OUTPUT_FORMATS), default="text",
537
+ help="The output format. Defaults to text.")
538
+ def family_members(ctx: Context, **kwargs: Any) -> None:
539
+ """
540
+ Get the Amazon Household members whose Orders this account can view, such as linked Teen accounts.
541
+ """
542
+ amazon_session = ctx.obj["amazon_session"]
543
+
544
+ try:
545
+ CliAuth(amazon_session).authenticate()
546
+
547
+ config = ctx.obj["conf"]
548
+ amazon_orders = AmazonOrders(amazon_session,
549
+ config=config)
550
+
551
+ start_time = time.time()
552
+ members = amazon_orders.get_family_members()
553
+ end_time = time.time()
554
+
555
+ click.echo(config.output_cls(config).format(members, kwargs["output"]))
556
+
557
+ click.echo(
558
+ "... {total} Amazon Household members parsed in {time} seconds.\n".format(
559
+ total=len(members), time=int(end_time - start_time)), err=True)
560
+ except AmazonOrdersAuthRedirectError:
561
+ IOClick.echo_session_expired()
562
+ except AmazonOrdersError as e:
563
+ logger.debug("An error occurred.", exc_info=True)
564
+ ctx.fail(str(e))
565
+
566
+
529
567
  @amazon_orders_cli.command()
530
568
  @click.pass_context
531
569
  def gift_card_balance(ctx: Context) -> None:
@@ -69,6 +69,7 @@ class Constants:
69
69
  ORDER_INVOICE_URL = f"{BASE_URL}/gp/css/summary/print.html"
70
70
  HISTORY_FILTER_QUERY_PARAM = "timeFilter"
71
71
  ORDER_FILTER_QUERY_PARAM = "orderFilter"
72
+ FAMILY_MEMBER_QUERY_PARAM = "familyMemberId"
72
73
  HISTORY_PAGE_QUERY_PARAM = "page"
73
74
  HISTORY_START_INDEX_QUERY_PARAM = "startIndex"
74
75
  ORDER_ID_QUERY_PARAM = "orderID"
@@ -0,0 +1,48 @@
1
+ __copyright__ = "Copyright (c) 2024-2025 Alex Laird"
2
+ __license__ = "MIT"
3
+
4
+ import logging
5
+ from typing import Optional
6
+
7
+ from bs4 import Tag
8
+
9
+ from amazonorders.conf import AmazonOrdersConfig
10
+ from amazonorders.entity.parsable import Parsable
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class FamilyMember(Parsable):
16
+ """
17
+ A member of the Amazon Household whose Orders the account can view, such as a linked Teen account. Pass its
18
+ :attr:`id` as ``family_member_id`` to :func:`~amazonorders.orders.AmazonOrders.get_order_history` to get their
19
+ Orders.
20
+ """
21
+
22
+ def __init__(self,
23
+ parsed: Tag,
24
+ config: AmazonOrdersConfig) -> None:
25
+ super().__init__(parsed, config)
26
+
27
+ #: The FamilyMember ID, Amazon's ``familyMemberId``.
28
+ self.id: Optional[str] = self.safe_parse(self._parse_id)
29
+ #: The FamilyMember name.
30
+ self.name: Optional[str] = self.safe_parse(self._parse_name)
31
+
32
+ def __repr__(self) -> str:
33
+ return f"<FamilyMember {self.id}: \"{self.name}\">"
34
+
35
+ def __str__(self) -> str: # pragma: no cover
36
+ return f"FamilyMember {self.id}: {self.name}"
37
+
38
+ def _parse_id(self) -> Optional[str]:
39
+ value = self.parsed.get(self.config.selectors.FAMILY_MEMBER_ID_ATTR)
40
+ return str(value).strip() if value else None
41
+
42
+ def _parse_name(self) -> Optional[str]:
43
+ # Amazon renders a NUL byte between the name and the suffix, e.g. "Jane Doe\x00's Orders"
44
+ value = self.parsed.text.replace("\x00", " ").strip()
45
+ suffix = self.config.selectors.FIELD_FAMILY_MEMBER_NAME_SUFFIX
46
+ if suffix and suffix in value:
47
+ value = value.split(suffix)[0]
48
+ return value.strip() or None
@@ -252,7 +252,18 @@ class Order(Parsable):
252
252
  tag = util.select_one(self.parsed, self.config.selectors.FIELD_ORDER_PAYMENT_METHOD_SELECTOR)
253
253
  if tag is None:
254
254
  return None
255
- return tag.attrs.get("alt") or tag.text.strip()
255
+ return tag.attrs.get("alt") or self._without_masked_digits(tag.text).strip()
256
+
257
+ def _without_masked_digits(self,
258
+ text: str) -> str:
259
+ """
260
+ The payment method's name, when Amazon shows it in one line with the masked digits that follow it.
261
+ """
262
+ match = re.search(self.config.selectors.FIELD_ORDER_PAYMENT_METHOD_LAST_4_REGEX, text)
263
+ if match and match.start() > 0:
264
+ return text[:match.start()]
265
+
266
+ return text
256
267
 
257
268
  def _parse_masked_digits(self,
258
269
  selector: Union[str, list],
@@ -6,7 +6,7 @@ import re
6
6
  from abc import ABC
7
7
  from io import BytesIO
8
8
  from typing import Any, Dict, NoReturn, Optional, TYPE_CHECKING, Union
9
- from urllib.parse import urlparse
9
+ from urllib.parse import urljoin, urlparse
10
10
 
11
11
  import pyotp
12
12
  from PIL import Image
@@ -190,7 +190,8 @@ class AuthForm(ABC):
190
190
 
191
191
  error_tag = util.select_one(form_response.parsed, self.error_selector)
192
192
  if error_tag:
193
- error_msg = f"Error from Amazon: {util.cleanup_html_text(error_tag.text)}"
193
+ error_text = util.cleanup_html_text(error_tag.get_text("\n"))
194
+ error_msg = f"Error from Amazon: {error_text}"
194
195
 
195
196
  if self.critical:
196
197
  raise AmazonOrdersAuthError(error_msg)
@@ -226,10 +227,23 @@ class SignInForm(AuthForm):
226
227
  "Check if Amazon changed their login flow."
227
228
  ) # pragma: no cover
228
229
 
229
- additional_attrs.update({self.solution_attr_key: self.amazon_session.username,
230
- "password": self.amazon_session.password})
230
+ additional_attrs[self.solution_attr_key] = self.amazon_session.username
231
+ if not self._collects_email_only():
232
+ additional_attrs["password"] = self.amazon_session.password
231
233
  self.data.update(additional_attrs)
232
234
 
235
+ def _collects_email_only(self) -> bool:
236
+ """
237
+ Whether the ``<form>`` is the email-first step of sign-in, which posts to the claim URL and asks for the
238
+ password on the page that follows. A browser sends only the email here, and Amazon's current sign-in rejects
239
+ a password sent along with it.
240
+ """
241
+ if not self.form:
242
+ return False
243
+
244
+ action_path = urlparse(str(self.form.get("action", ""))).path
245
+ return action_path == urlparse(self.config.constants.SIGN_IN_CLAIM_URL).path
246
+
233
247
 
234
248
  class ClaimForm(AuthForm):
235
249
  def __init__(self,
@@ -259,10 +273,64 @@ class ClaimForm(AuthForm):
259
273
  "Check if Amazon changed their login flow."
260
274
  ) # pragma: no cover
261
275
 
262
- additional_attrs.update({self.solution_attr_key: self.amazon_session.username,
263
- "password": self.amazon_session.password})
276
+ if self._asks_for_email():
277
+ additional_attrs[self.solution_attr_key] = self.amazon_session.username
278
+ additional_attrs["password"] = self.amazon_session.password
264
279
  self.data.update(additional_attrs)
265
280
 
281
+ def _asks_for_email(self) -> bool:
282
+ """
283
+ Whether the ``<form>`` has a field for the email. The older sign-in asks for it again with the password, while
284
+ the newer one already has it from the step before and asks for the password alone.
285
+ """
286
+ return bool(self.form and self.form.find("input", attrs={"name": self.solution_attr_key}))
287
+
288
+
289
+ class PasskeyForm(AuthForm):
290
+ """
291
+ The page that offers a passkey before the password, when the account has one. A passkey needs the user's device,
292
+ so this declines it and follows the page's link to the password instead.
293
+ """
294
+
295
+ def __init__(self,
296
+ config: AmazonOrdersConfig,
297
+ selector: Optional[str] = None) -> None:
298
+ if not selector:
299
+ selector = config.selectors.PASSKEY_FORM_SELECTOR
300
+
301
+ super().__init__(config, selector, critical=True)
302
+
303
+ #: The link to the password instead of the passkey.
304
+ self.password_challenge_link: Optional[Tag] = None
305
+
306
+ def select_form(self,
307
+ amazon_session: "AmazonSession",
308
+ parsed: Tag) -> bool:
309
+ if not super().select_form(amazon_session, parsed):
310
+ return False
311
+
312
+ self.password_challenge_link = util.select_one(parsed,
313
+ self.config.selectors.PASSKEY_PASSWORD_CHALLENGE_LINK_SELECTOR)
314
+ return self.password_challenge_link is not None
315
+
316
+ def submit(self, last_response: Response) -> AmazonSessionResponse:
317
+ if not self.amazon_session or not self.password_challenge_link:
318
+ raise AmazonOrdersError(
319
+ "Call AuthForm.select_form() first."
320
+ ) # pragma: no cover
321
+
322
+ url = urljoin(last_response.url, str(self.password_challenge_link["href"]))
323
+ password_response = self.amazon_session.get(url, persist_cookies=True)
324
+
325
+ self._handle_errors(password_response)
326
+ self.clear_form()
327
+
328
+ return password_response
329
+
330
+ def clear_form(self) -> None:
331
+ super().clear_form()
332
+ self.password_challenge_link = None
333
+
266
334
 
267
335
  class IntentForm(AuthForm):
268
336
  def __init__(self,
@@ -392,12 +460,23 @@ class MfaForm(AuthForm):
392
460
  otp = self.amazon_session.io.prompt("Enter the one-time passcode from your preferred 2FA method")
393
461
  self.amazon_session.io.echo("")
394
462
 
395
- additional_attrs.update({self.solution_attr_key: otp,
396
- "rememberDevice": ""})
463
+ additional_attrs[self.solution_attr_key] = otp
464
+ if not self._posts_to_claim():
465
+ additional_attrs["rememberDevice"] = ""
466
+ if "deviceId" not in self.data:
467
+ additional_attrs["deviceId"] = ""
397
468
  self.data.update(additional_attrs)
398
469
 
399
- if "deviceId" not in self.data:
400
- self.data["deviceId"] = ""
470
+ def _posts_to_claim(self) -> bool:
471
+ """
472
+ Whether the ``<form>`` is part of the newer sign-in that posts under the claim URL, which takes the code alone,
473
+ without the older form's device fields.
474
+ """
475
+ if not self.form:
476
+ return False
477
+
478
+ action_path = urlparse(str(self.form.get("action", ""))).path
479
+ return action_path.startswith(urlparse(self.config.constants.SIGN_IN_CLAIM_URL).path)
401
480
 
402
481
 
403
482
  class CaptchaForm(AuthForm):
@@ -12,6 +12,7 @@ from bs4 import BeautifulSoup, Tag
12
12
 
13
13
  from amazonorders import util
14
14
  from amazonorders.conf import AmazonOrdersConfig
15
+ from amazonorders.entity.family_member import FamilyMember
15
16
  from amazonorders.entity.order import Order
16
17
  from amazonorders.entity.tracking import Tracking
17
18
  from amazonorders.exception import AmazonOrdersError, AmazonOrdersNotFoundError, AmazonOrdersUnavailableError
@@ -96,6 +97,20 @@ class AmazonOrders:
96
97
  raise AmazonOrdersError("Could not parse Order details. Check if Amazon changed the HTML.")
97
98
  return order
98
99
 
100
+ @classmethod
101
+ def parse_family_members(cls,
102
+ html: str,
103
+ config: AmazonOrdersConfig) -> List[FamilyMember]:
104
+ """
105
+ Parse the Amazon Household members from an already-fetched Order history page, without a session driving
106
+ the fetch. Useful for parsing HTML obtained elsewhere and for network-free testing.
107
+
108
+ :param html: The Order history page HTML to parse.
109
+ :param config: The config providing the selectors used for parsing.
110
+ :return: The parsed FamilyMembers, or an empty list if the page lists none.
111
+ """
112
+ return cls._parse_family_members(BeautifulSoup(html, config.bs4_parser), config)
113
+
99
114
  def get_order(self,
100
115
  order_id: str,
101
116
  clone: Optional[Order] = None) -> Order:
@@ -251,6 +266,23 @@ class AmazonOrders:
251
266
 
252
267
  return invoice_response
253
268
 
269
+ def get_family_members(self) -> List[FamilyMember]:
270
+ """
271
+ Get the members of the Amazon Household whose Orders this account can view, such as linked Teen accounts,
272
+ from the Order history's member filter. Pass a member's
273
+ :attr:`~amazonorders.entity.family_member.FamilyMember.id` as ``family_member_id`` to
274
+ :func:`get_order_history` to get their Orders.
275
+
276
+ :return: The FamilyMembers, or an empty list if the account has none.
277
+ """
278
+ if not self.amazon_session.is_authenticated:
279
+ raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")
280
+
281
+ page_response = self.amazon_session.get(self.config.constants.ORDER_HISTORY_URL)
282
+ self.amazon_session.check_response(page_response)
283
+
284
+ return self._parse_family_members(page_response.parsed, self.config)
285
+
254
286
  def get_order_history(self,
255
287
  year: Optional[int] = None,
256
288
  start_index: Optional[int] = None,
@@ -259,7 +291,8 @@ class AmazonOrders:
259
291
  time_filter: Optional[str] = None,
260
292
  order_filter: Optional[str] = None,
261
293
  progress: Optional[Callable[[int], None]] = None,
262
- progress_with_total: Optional[Callable[[int, Optional[int]], None]] = None) -> List[Order]:
294
+ progress_with_total: Optional[Callable[[int, Optional[int]], None]] = None,
295
+ family_member_id: Optional[str] = None) -> List[Order]:
263
296
  """
264
297
  Get the Amazon Order history for a given time period.
265
298
 
@@ -282,12 +315,14 @@ class AmazonOrders:
282
315
  :param progress_with_total: Like ``progress``, but also called with the number of Orders expected in total,
283
316
  from the count Amazon shows on the history page. The total is ``None`` if Amazon doesn't show one, or if
284
317
  ``order_filter`` is provided, since Amazon's count may not reflect it.
318
+ :param family_member_id: The ID of an Amazon Household member whose Orders to get, such as a linked Teen
319
+ account, from :func:`get_family_members`.
285
320
  :return: A list of the requested Orders.
286
321
  """
287
322
  if not self.amazon_session.is_authenticated:
288
323
  raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")
289
324
 
290
- next_page = self._order_history_url(year, start_index, time_filter, order_filter)
325
+ next_page = self._order_history_url(year, start_index, time_filter, order_filter, family_member_id)
291
326
  current_index = int(start_index) if start_index else 0
292
327
 
293
328
  orders, _, _ = asyncio.run(self._build_orders_async(next_page, keep_paging, full_details, current_index,
@@ -305,7 +340,8 @@ class AmazonOrders:
305
340
  limit: Optional[int] = None,
306
341
  cursor: Optional[Dict[str, Any]] = None,
307
342
  progress: Optional[Callable[[int], None]] = None,
308
- progress_with_total: Optional[Callable[[int, Optional[int]], None]] = None
343
+ progress_with_total: Optional[Callable[[int, Optional[int]], None]] = None,
344
+ family_member_id: Optional[str] = None
309
345
  ) -> PagedResults[Order]:
310
346
  """
311
347
  Get a batch of the Amazon Order history for a given time period, with a cursor to fetch the next batch.
@@ -329,6 +365,8 @@ class AmazonOrders:
329
365
  this batch (before their full details are fetched, if ``full_details`` is ``True``).
330
366
  :param progress_with_total: Like ``progress``, but also called with the number of Orders expected in this
331
367
  batch, as in :func:`get_order_history`, and at most ``limit`` until the batch ends.
368
+ :param family_member_id: The ID of an Amazon Household member whose Orders to get, as in
369
+ :func:`get_order_history`. Ignored if ``cursor`` is provided.
332
370
  :return: The batch of Orders, and the cursor for the next one.
333
371
  """
334
372
  if not self.amazon_session.is_authenticated:
@@ -339,9 +377,11 @@ class AmazonOrders:
339
377
  start_index = cursor["start_index"]
340
378
  time_filter = cursor["time_filter"]
341
379
  order_filter = cursor["order_filter"]
380
+ family_member_id = cursor.get("family_member_id")
342
381
  time_filter = self._history_filter_value(year, time_filter)
343
382
 
344
- next_page: Optional[str] = self._order_history_url(None, start_index, time_filter, order_filter)
383
+ next_page: Optional[str] = self._order_history_url(None, start_index, time_filter, order_filter,
384
+ family_member_id)
345
385
  current_index = int(start_index) if start_index else 0
346
386
 
347
387
  orders, next_page, current_index = asyncio.run(
@@ -351,6 +391,7 @@ class AmazonOrders:
351
391
  "start_index": current_index,
352
392
  "time_filter": time_filter,
353
393
  "order_filter": order_filter,
394
+ **({"family_member_id": family_member_id} if family_member_id else {}),
354
395
  } if next_page else None
355
396
 
356
397
  return PagedResults(orders, next_cursor)
@@ -385,7 +426,8 @@ class AmazonOrders:
385
426
  year: Optional[int],
386
427
  start_index: Optional[int],
387
428
  time_filter: Optional[str],
388
- order_filter: Optional[str]) -> str:
429
+ order_filter: Optional[str],
430
+ family_member_id: Optional[str] = None) -> str:
389
431
  constants = self.config.constants
390
432
  filter_value = self._history_filter_value(year, time_filter)
391
433
  page = int(start_index) // constants.ORDER_HISTORY_PAGE_SIZE if start_index else 0
@@ -398,16 +440,28 @@ class AmazonOrders:
398
440
  f"&{constants.ORDER_FILTER_QUERY_PARAM}={quote(order_filter, safe='')}"
399
441
  if order_filter else ""
400
442
  )
443
+ optional_family_member = (
444
+ f"&{constants.FAMILY_MEMBER_QUERY_PARAM}={quote(family_member_id, safe='')}"
445
+ if family_member_id else ""
446
+ )
401
447
  return (
402
- "{url}?{query_param}={filter_value}{optional_order_filter}{optional_page}"
448
+ "{url}?{query_param}={filter_value}{optional_order_filter}{optional_family_member}{optional_page}"
403
449
  ).format(
404
450
  url=constants.ORDER_HISTORY_URL,
405
451
  query_param=constants.HISTORY_FILTER_QUERY_PARAM,
406
452
  filter_value=filter_value,
407
453
  optional_order_filter=optional_order_filter,
454
+ optional_family_member=optional_family_member,
408
455
  optional_page=optional_page
409
456
  )
410
457
 
458
+ @staticmethod
459
+ def _parse_family_members(parsed: Tag,
460
+ config: AmazonOrdersConfig) -> List[FamilyMember]:
461
+ members = [FamilyMember(tag, config)
462
+ for tag in util.select(parsed, config.selectors.FAMILY_MEMBER_ENTITY_SELECTOR)]
463
+ return [member for member in members if member.id]
464
+
411
465
  @staticmethod
412
466
  def _parse_order_count(order_count_tag: Optional[Tag],
413
467
  config: AmazonOrdersConfig) -> Optional[int]:
@@ -105,7 +105,7 @@ Order #{order_number}
105
105
 
106
106
  order_str += f"\n Shipments: {order.shipments}"
107
107
  order_str += f"\n Order Details Link: {order.order_details_link}"
108
- if order.grand_total:
108
+ if order.grand_total is not None:
109
109
  order_str += f"\n Grand Total: {self.config.constants.format_currency(order.grand_total)}"
110
110
  order_str += f"\n Order Placed Date: {order.order_placed_date}"
111
111
  if order.recipient:
@@ -147,7 +147,7 @@ Order #{order_number}
147
147
  """
148
148
  transaction_str = f"Transaction: {transaction.completed_date}"
149
149
  transaction_str += f"\n Order #{transaction.order_number}"
150
- if transaction.grand_total:
150
+ if transaction.grand_total is not None:
151
151
  transaction_str += f"\n Grand Total: {self.config.constants.format_currency(transaction.grand_total)}"
152
152
  transaction_str += f"\n Order Details Link: {transaction.order_details_link}"
153
153
 
@@ -97,18 +97,20 @@ class Selectors:
97
97
  SIGN_IN_FORM_SELECTOR = "form[name='signIn']"
98
98
  NAV_GREETING_SELECTOR = "#nav-link-accountList-nav-line-1"
99
99
  SIGN_IN_EMAIL_INPUT_SELECTOR = "input[name='email']"
100
- CLAIM_FORM_SELECTOR = "form[name='signIn'].auth-validate-form"
100
+ CLAIM_FORM_SELECTOR = ["form[name='signIn'].auth-validate-form", "form#unified-password-form"]
101
101
  INTENT_FORM_SELECTOR = "form#intent-confirmation-form"
102
102
  INTENT_MESSAGE_SELECTOR = "div#intent-confirmation-container"
103
103
  MFA_DEVICE_SELECT_FORM_SELECTOR = "form#auth-select-device-form"
104
+ PASSKEY_FORM_SELECTOR = "form#unified-passkey-form"
105
+ PASSKEY_PASSWORD_CHALLENGE_LINK_SELECTOR = "a[href*='selectedChallengeType=password']"
104
106
  MFA_DEVICE_SELECT_INPUT_SELECTOR = "input[name='otpDeviceContext']"
105
107
  MFA_DEVICE_SELECT_INPUT_SELECTOR_VALUE = "value"
106
108
  MFA_DEVICE_SELECT_LABEL_SELECTOR = "span.a-label.a-radio-label"
107
- MFA_FORM_SELECTOR = "form#auth-mfa-form"
109
+ MFA_FORM_SELECTOR = ["form#auth-mfa-form", "form#unified-otp-form"]
108
110
  CAPTCHA_1_FORM_SELECTOR = "form.cvf-widget-form-captcha"
109
111
  CAPTCHA_2_FORM_SELECTOR = ["form:has(input[id^='captchacharacters'])", "form[action$='validateCaptcha']"]
110
112
  CAPTCHA_OTP_FORM_SELECTOR = "form#verification-code-form"
111
- DEFAULT_ERROR_TAG_SELECTOR = "div#auth-error-message-box"
113
+ DEFAULT_ERROR_TAG_SELECTOR = "div#auth-error-message-box:not(.aok-hidden *)"
112
114
  CAPTCHA_1_ERROR_SELECTOR = "div.cvf-widget-alert"
113
115
  CAPTCHA_2_ERROR_SELECTOR = "div.a-alert-info"
114
116
 
@@ -118,6 +120,16 @@ class Selectors:
118
120
 
119
121
  NEXT_PAGE_LINK_SELECTOR = "ul.a-pagination li.a-last a"
120
122
 
123
+ ##########################################################################
124
+ # CSS selectors for Amazon Household family members
125
+ ##########################################################################
126
+
127
+ #: The Order history's "My Orders" / "<name>'s Orders" dropdown; the account's own entry has an empty value
128
+ FAMILY_MEMBER_ENTITY_SELECTOR = ["select#family-member-filter option[value]:not([value=''])",
129
+ "select[name='familyMemberId'] option[value]:not([value=''])"]
130
+ FAMILY_MEMBER_ID_ATTR = "value"
131
+ FIELD_FAMILY_MEMBER_NAME_SUFFIX = "'s Orders"
132
+
121
133
  ##########################################################################
122
134
  # CSS selectors for Entities and Fields
123
135
  #
@@ -250,10 +262,12 @@ class Selectors:
250
262
  "div:is(.a-span3, .a-span12)"]
251
263
  FIELD_ORDER_PLACED_DATE_SUFFIX = "Order #"
252
264
  FIELD_ORDER_PAYMENT_METHOD_SELECTOR = ["[data-testid='payment-instrument-name']",
253
- "img.pmts-payment-credit-card-instrument-logo"]
265
+ "img.pmts-payment-credit-card-instrument-logo",
266
+ "[data-testid='payment-instrument-message']"]
254
267
  FIELD_ORDER_PAYMENT_METHOD_LAST_4_SELECTOR = ["[data-testid='payment-instrument-number']",
255
- "span:has(img.pmts-payment-credit-card-instrument-logo):last-child"]
256
- FIELD_ORDER_PAYMENT_METHOD_LAST_4_REGEX = r"(?:ending in\s+|^\s*)(\d+)"
268
+ "span:has(img.pmts-payment-credit-card-instrument-logo):last-child",
269
+ "[data-testid='payment-instrument-message']"]
270
+ FIELD_ORDER_PAYMENT_METHOD_LAST_4_REGEX = r"(?:ending in\s+|^\s*|•+\s*)(\d+)"
257
271
  FIELD_ORDER_SUBTOTALS_TAG_ITERATOR_SELECTOR = ["[data-component='orderSubtotals'] div.a-row",
258
272
  "div#od-subtotals div.a-row",
259
273
  "[data-component='chargeSummary'] div.od-line-item-row"]
@@ -17,7 +17,7 @@ from requests.utils import dict_from_cookiejar
17
17
  from amazonorders.conf import AmazonOrdersConfig, cookies_file_lock, debug_output_file_lock
18
18
  from amazonorders.exception import AmazonOrdersAuthError, AmazonOrdersError, AmazonOrdersAuthRedirectError
19
19
  from amazonorders.forms import (AuthForm, AcicAuthBlocker, CaptchaForm, JSAuthBlocker, MfaDeviceSelectForm, MfaForm,
20
- SignInForm, ClaimForm, IntentForm)
20
+ SignInForm, ClaimForm, IntentForm, PasskeyForm)
21
21
  from amazonorders.util import AmazonSessionResponse, load_class, select_one
22
22
 
23
23
  logger = logging.getLogger(__name__)
@@ -169,6 +169,7 @@ class AmazonSession:
169
169
  return [ClaimForm(config),
170
170
  IntentForm(config),
171
171
  SignInForm(config),
172
+ PasskeyForm(config),
172
173
  MfaDeviceSelectForm(config),
173
174
  MfaForm(config),
174
175
  CaptchaForm(config),
File without changes
File without changes
File without changes