amazon-orders 4.9.2__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.
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/CHANGELOG.md +18 -1
- {amazon_orders-4.9.2/amazon_orders.egg-info → amazon_orders-4.9.4}/PKG-INFO +9 -4
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/README.md +8 -3
- {amazon_orders-4.9.2 → amazon_orders-4.9.4/amazon_orders.egg-info}/PKG-INFO +9 -4
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazon_orders.egg-info/SOURCES.txt +1 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/__init__.py +1 -1
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/cli.py +40 -1
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/constants.py +1 -0
- amazon_orders-4.9.4/amazonorders/entity/family_member.py +48 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/order.py +12 -1
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/forms.py +89 -10
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/orders.py +64 -8
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/output.py +2 -2
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/selectors.py +21 -6
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/session.py +5 -6
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/LICENSE +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/MANIFEST.in +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazon_orders.egg-info/dependency_links.txt +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazon_orders.egg-info/entry_points.txt +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazon_orders.egg-info/requires.txt +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazon_orders.egg-info/top_level.txt +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/banner.txt +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/conf.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/__init__.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/browser/__init__.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/browser/playwright.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/waf/__init__.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/waf/anticaptcha.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/waf/base.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/waf/capsolver.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/contrib/waf/twocaptcha.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/__init__.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/gift_card_activity.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/item.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/parsable.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/recipient.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/seller.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/shipment.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/tracking.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/entity/transaction.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/exception.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/gift_cards.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/paging.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/region_data.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/transactions.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/amazonorders/util.py +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/pyproject.toml +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/setup.cfg +0 -0
- {amazon_orders-4.9.2 → amazon_orders-4.9.4}/tests/testcase.py +0 -0
|
@@ -4,7 +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.
|
|
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.
|
|
19
|
+
|
|
20
|
+
## [4.9.3](https://github.com/alexdlaird/amazon-orders/compare/4.9.2...4.9.3) - 2026-10-18
|
|
21
|
+
|
|
22
|
+
### Fixed
|
|
23
|
+
|
|
24
|
+
- Bug fixes and stability improvements.
|
|
8
25
|
|
|
9
26
|
## [4.9.2](https://github.com/alexdlaird/amazon-orders/compare/4.9.1...4.9.2) - 2026-10-15
|
|
10
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.9.
|
|
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
|
[](https://pypi.org/project/amazon-orders)
|
|
80
|
+
[](https://pypistats.org/packages/amazon-orders)
|
|
80
81
|
[](https://pypi.org/project/amazon-orders)
|
|
81
82
|
[](https://codecov.io/gh/alexdlaird/amazon-orders)
|
|
82
83
|
[](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`,
|
|
155
|
-
`--output`, which renders Orders, Transactions,
|
|
156
|
-
`yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only
|
|
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
|
|
@@ -232,6 +234,9 @@ as shown above. Any MCP client gets the same as tools through
|
|
|
232
234
|
[`amazon-orders-mcp`](https://github.com/alexdlaird/amazon-orders-mcp#readme), a server built on `amazon-orders` — see
|
|
233
235
|
its README for setup.
|
|
234
236
|
|
|
237
|
+
The server signs you in with a browser, so it needs a display. Without one, use the CLI or library with
|
|
238
|
+
[Automating Authentication](#automating-authentication).
|
|
239
|
+
|
|
235
240
|
## Documentation
|
|
236
241
|
|
|
237
242
|
For more advanced usage, `amazon-orders`'s official documentation is available
|
|
@@ -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
|
[](https://pypi.org/project/amazon-orders)
|
|
4
|
+
[](https://pypistats.org/packages/amazon-orders)
|
|
4
5
|
[](https://pypi.org/project/amazon-orders)
|
|
5
6
|
[](https://codecov.io/gh/alexdlaird/amazon-orders)
|
|
6
7
|
[](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`,
|
|
79
|
-
`--output`, which renders Orders, Transactions,
|
|
80
|
-
`yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only
|
|
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
|
|
@@ -156,6 +158,9 @@ as shown above. Any MCP client gets the same as tools through
|
|
|
156
158
|
[`amazon-orders-mcp`](https://github.com/alexdlaird/amazon-orders-mcp#readme), a server built on `amazon-orders` — see
|
|
157
159
|
its README for setup.
|
|
158
160
|
|
|
161
|
+
The server signs you in with a browser, so it needs a display. Without one, use the CLI or library with
|
|
162
|
+
[Automating Authentication](#automating-authentication).
|
|
163
|
+
|
|
159
164
|
## Documentation
|
|
160
165
|
|
|
161
166
|
For more advanced usage, `amazon-orders`'s official documentation is available
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: amazon-orders
|
|
3
|
-
Version: 4.9.
|
|
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
|
[](https://pypi.org/project/amazon-orders)
|
|
80
|
+
[](https://pypistats.org/packages/amazon-orders)
|
|
80
81
|
[](https://pypi.org/project/amazon-orders)
|
|
81
82
|
[](https://codecov.io/gh/alexdlaird/amazon-orders)
|
|
82
83
|
[](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`,
|
|
155
|
-
`--output`, which renders Orders, Transactions,
|
|
156
|
-
`yaml`, or `csv`. Progress messages are written to `stderr`, so redirecting `stdout` captures only
|
|
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
|
|
@@ -232,6 +234,9 @@ as shown above. Any MCP client gets the same as tools through
|
|
|
232
234
|
[`amazon-orders-mcp`](https://github.com/alexdlaird/amazon-orders-mcp#readme), a server built on `amazon-orders` — see
|
|
233
235
|
its README for setup.
|
|
234
236
|
|
|
237
|
+
The server signs you in with a browser, so it needs a display. Without one, use the CLI or library with
|
|
238
|
+
[Automating Authentication](#automating-authentication).
|
|
239
|
+
|
|
235
240
|
## Documentation
|
|
236
241
|
|
|
237
242
|
For more advanced usage, `amazon-orders`'s official documentation is available
|
|
@@ -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
|
|
@@ -302,7 +302,11 @@ def amazon_orders_cli(ctx: Context,
|
|
|
302
302
|
help="Get the full details for each Order in the history. "
|
|
303
303
|
"This will execute an additional request per Order.")
|
|
304
304
|
@click.option("--order-filter", "order_filter", default=None,
|
|
305
|
-
help="The order type filter to use."
|
|
305
|
+
help="The order type filter to use, e.g. digital-orders for Kindle and other digital purchases, "
|
|
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.")
|
|
306
310
|
@click.option("-o", "--output", type=click.Choice(OutputFormatter.OUTPUT_FORMATS), default="text",
|
|
307
311
|
help="The output format. Defaults to text.")
|
|
308
312
|
def history(ctx: Context,
|
|
@@ -322,6 +326,7 @@ def history(ctx: Context,
|
|
|
322
326
|
single_page = kwargs["single_page"]
|
|
323
327
|
full_details = kwargs["full_details"]
|
|
324
328
|
order_filter = kwargs["order_filter"]
|
|
329
|
+
family_member_id = kwargs["family_member_id"]
|
|
325
330
|
output = kwargs["output"]
|
|
326
331
|
|
|
327
332
|
exclusive_flags = [year, last_3_months, last_30_days]
|
|
@@ -365,6 +370,7 @@ Order History for {filter_description}{optional_start_index}{optional_full_detai
|
|
|
365
370
|
keep_paging=not single_page,
|
|
366
371
|
time_filter=time_filter,
|
|
367
372
|
order_filter=order_filter,
|
|
373
|
+
family_member_id=family_member_id,
|
|
368
374
|
progress_with_total=progress)
|
|
369
375
|
end_time = time.time()
|
|
370
376
|
|
|
@@ -525,6 +531,39 @@ Transaction History for {days} days
|
|
|
525
531
|
ctx.fail(str(e))
|
|
526
532
|
|
|
527
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
|
+
|
|
528
567
|
@amazon_orders_cli.command()
|
|
529
568
|
@click.pass_context
|
|
530
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
|
-
|
|
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
|
|
230
|
-
|
|
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
|
-
|
|
263
|
-
|
|
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
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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
|
|
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
|
|
|
@@ -275,18 +308,21 @@ class AmazonOrders:
|
|
|
275
308
|
:param time_filter: The time filter to use. Supported values are ``"last30"`` (last 30 days),
|
|
276
309
|
``"months-3"`` (past 3 months), or ``"year-YYYY"`` (specific year). If provided, this takes
|
|
277
310
|
precedence over the ``year`` parameter.
|
|
278
|
-
:param order_filter: The order type filter to use.
|
|
311
|
+
:param order_filter: The order type filter to use, e.g. ``"digital-orders"`` for Kindle and other digital
|
|
312
|
+
purchases, which the default history doesn't include. If provided, appended alongside the time filter.
|
|
279
313
|
:param progress: Called after each page of history is fetched, with the number of Orders found so far
|
|
280
314
|
(before their full details are fetched, if ``full_details`` is ``True``).
|
|
281
315
|
:param progress_with_total: Like ``progress``, but also called with the number of Orders expected in total,
|
|
282
316
|
from the count Amazon shows on the history page. The total is ``None`` if Amazon doesn't show one, or if
|
|
283
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`.
|
|
284
320
|
:return: A list of the requested Orders.
|
|
285
321
|
"""
|
|
286
322
|
if not self.amazon_session.is_authenticated:
|
|
287
323
|
raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")
|
|
288
324
|
|
|
289
|
-
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)
|
|
290
326
|
current_index = int(start_index) if start_index else 0
|
|
291
327
|
|
|
292
328
|
orders, _, _ = asyncio.run(self._build_orders_async(next_page, keep_paging, full_details, current_index,
|
|
@@ -304,7 +340,8 @@ class AmazonOrders:
|
|
|
304
340
|
limit: Optional[int] = None,
|
|
305
341
|
cursor: Optional[Dict[str, Any]] = None,
|
|
306
342
|
progress: Optional[Callable[[int], None]] = None,
|
|
307
|
-
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
|
|
308
345
|
) -> PagedResults[Order]:
|
|
309
346
|
"""
|
|
310
347
|
Get a batch of the Amazon Order history for a given time period, with a cursor to fetch the next batch.
|
|
@@ -318,7 +355,8 @@ class AmazonOrders:
|
|
|
318
355
|
request per Order.
|
|
319
356
|
:param time_filter: The time filter to use, as in :func:`get_order_history`. Ignored if ``cursor`` is
|
|
320
357
|
provided.
|
|
321
|
-
:param order_filter: The order type filter to use
|
|
358
|
+
:param order_filter: The order type filter to use, as in :func:`get_order_history`. Ignored if ``cursor`` is
|
|
359
|
+
provided.
|
|
322
360
|
:param limit: Stop fetching history pages once at least this many Orders are collected. Whole pages are
|
|
323
361
|
fetched, so the batch can hold up to a page more than ``limit``. ``None`` fetches every page.
|
|
324
362
|
:param cursor: The :attr:`~amazonorders.paging.PagedResults.cursor` of the previous batch, to fetch the
|
|
@@ -327,6 +365,8 @@ class AmazonOrders:
|
|
|
327
365
|
this batch (before their full details are fetched, if ``full_details`` is ``True``).
|
|
328
366
|
:param progress_with_total: Like ``progress``, but also called with the number of Orders expected in this
|
|
329
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.
|
|
330
370
|
:return: The batch of Orders, and the cursor for the next one.
|
|
331
371
|
"""
|
|
332
372
|
if not self.amazon_session.is_authenticated:
|
|
@@ -337,9 +377,11 @@ class AmazonOrders:
|
|
|
337
377
|
start_index = cursor["start_index"]
|
|
338
378
|
time_filter = cursor["time_filter"]
|
|
339
379
|
order_filter = cursor["order_filter"]
|
|
380
|
+
family_member_id = cursor.get("family_member_id")
|
|
340
381
|
time_filter = self._history_filter_value(year, time_filter)
|
|
341
382
|
|
|
342
|
-
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)
|
|
343
385
|
current_index = int(start_index) if start_index else 0
|
|
344
386
|
|
|
345
387
|
orders, next_page, current_index = asyncio.run(
|
|
@@ -349,6 +391,7 @@ class AmazonOrders:
|
|
|
349
391
|
"start_index": current_index,
|
|
350
392
|
"time_filter": time_filter,
|
|
351
393
|
"order_filter": order_filter,
|
|
394
|
+
**({"family_member_id": family_member_id} if family_member_id else {}),
|
|
352
395
|
} if next_page else None
|
|
353
396
|
|
|
354
397
|
return PagedResults(orders, next_cursor)
|
|
@@ -383,7 +426,8 @@ class AmazonOrders:
|
|
|
383
426
|
year: Optional[int],
|
|
384
427
|
start_index: Optional[int],
|
|
385
428
|
time_filter: Optional[str],
|
|
386
|
-
order_filter: Optional[str]
|
|
429
|
+
order_filter: Optional[str],
|
|
430
|
+
family_member_id: Optional[str] = None) -> str:
|
|
387
431
|
constants = self.config.constants
|
|
388
432
|
filter_value = self._history_filter_value(year, time_filter)
|
|
389
433
|
page = int(start_index) // constants.ORDER_HISTORY_PAGE_SIZE if start_index else 0
|
|
@@ -396,16 +440,28 @@ class AmazonOrders:
|
|
|
396
440
|
f"&{constants.ORDER_FILTER_QUERY_PARAM}={quote(order_filter, safe='')}"
|
|
397
441
|
if order_filter else ""
|
|
398
442
|
)
|
|
443
|
+
optional_family_member = (
|
|
444
|
+
f"&{constants.FAMILY_MEMBER_QUERY_PARAM}={quote(family_member_id, safe='')}"
|
|
445
|
+
if family_member_id else ""
|
|
446
|
+
)
|
|
399
447
|
return (
|
|
400
|
-
"{url}?{query_param}={filter_value}{optional_order_filter}{optional_page}"
|
|
448
|
+
"{url}?{query_param}={filter_value}{optional_order_filter}{optional_family_member}{optional_page}"
|
|
401
449
|
).format(
|
|
402
450
|
url=constants.ORDER_HISTORY_URL,
|
|
403
451
|
query_param=constants.HISTORY_FILTER_QUERY_PARAM,
|
|
404
452
|
filter_value=filter_value,
|
|
405
453
|
optional_order_filter=optional_order_filter,
|
|
454
|
+
optional_family_member=optional_family_member,
|
|
406
455
|
optional_page=optional_page
|
|
407
456
|
)
|
|
408
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
|
+
|
|
409
465
|
@staticmethod
|
|
410
466
|
def _parse_order_count(order_count_tag: Optional[Tag],
|
|
411
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
|
|
|
@@ -95,19 +95,22 @@ class Selectors:
|
|
|
95
95
|
AWS_WAF_CHALLENGE_SCRIPT_SELECTOR = 'script[src*="awswaf.com"]'
|
|
96
96
|
|
|
97
97
|
SIGN_IN_FORM_SELECTOR = "form[name='signIn']"
|
|
98
|
+
NAV_GREETING_SELECTOR = "#nav-link-accountList-nav-line-1"
|
|
98
99
|
SIGN_IN_EMAIL_INPUT_SELECTOR = "input[name='email']"
|
|
99
|
-
CLAIM_FORM_SELECTOR = "form[name='signIn'].auth-validate-form"
|
|
100
|
+
CLAIM_FORM_SELECTOR = ["form[name='signIn'].auth-validate-form", "form#unified-password-form"]
|
|
100
101
|
INTENT_FORM_SELECTOR = "form#intent-confirmation-form"
|
|
101
102
|
INTENT_MESSAGE_SELECTOR = "div#intent-confirmation-container"
|
|
102
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']"
|
|
103
106
|
MFA_DEVICE_SELECT_INPUT_SELECTOR = "input[name='otpDeviceContext']"
|
|
104
107
|
MFA_DEVICE_SELECT_INPUT_SELECTOR_VALUE = "value"
|
|
105
108
|
MFA_DEVICE_SELECT_LABEL_SELECTOR = "span.a-label.a-radio-label"
|
|
106
|
-
MFA_FORM_SELECTOR = "form#auth-mfa-form"
|
|
109
|
+
MFA_FORM_SELECTOR = ["form#auth-mfa-form", "form#unified-otp-form"]
|
|
107
110
|
CAPTCHA_1_FORM_SELECTOR = "form.cvf-widget-form-captcha"
|
|
108
111
|
CAPTCHA_2_FORM_SELECTOR = ["form:has(input[id^='captchacharacters'])", "form[action$='validateCaptcha']"]
|
|
109
112
|
CAPTCHA_OTP_FORM_SELECTOR = "form#verification-code-form"
|
|
110
|
-
DEFAULT_ERROR_TAG_SELECTOR = "div#auth-error-message-box"
|
|
113
|
+
DEFAULT_ERROR_TAG_SELECTOR = "div#auth-error-message-box:not(.aok-hidden *)"
|
|
111
114
|
CAPTCHA_1_ERROR_SELECTOR = "div.cvf-widget-alert"
|
|
112
115
|
CAPTCHA_2_ERROR_SELECTOR = "div.a-alert-info"
|
|
113
116
|
|
|
@@ -117,6 +120,16 @@ class Selectors:
|
|
|
117
120
|
|
|
118
121
|
NEXT_PAGE_LINK_SELECTOR = "ul.a-pagination li.a-last a"
|
|
119
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
|
+
|
|
120
133
|
##########################################################################
|
|
121
134
|
# CSS selectors for Entities and Fields
|
|
122
135
|
#
|
|
@@ -249,10 +262,12 @@ class Selectors:
|
|
|
249
262
|
"div:is(.a-span3, .a-span12)"]
|
|
250
263
|
FIELD_ORDER_PLACED_DATE_SUFFIX = "Order #"
|
|
251
264
|
FIELD_ORDER_PAYMENT_METHOD_SELECTOR = ["[data-testid='payment-instrument-name']",
|
|
252
|
-
"img.pmts-payment-credit-card-instrument-logo"
|
|
265
|
+
"img.pmts-payment-credit-card-instrument-logo",
|
|
266
|
+
"[data-testid='payment-instrument-message']"]
|
|
253
267
|
FIELD_ORDER_PAYMENT_METHOD_LAST_4_SELECTOR = ["[data-testid='payment-instrument-number']",
|
|
254
|
-
"span:has(img.pmts-payment-credit-card-instrument-logo):last-child"
|
|
255
|
-
|
|
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+)"
|
|
256
271
|
FIELD_ORDER_SUBTOTALS_TAG_ITERATOR_SELECTOR = ["[data-component='orderSubtotals'] div.a-row",
|
|
257
272
|
"div#od-subtotals div.a-row",
|
|
258
273
|
"[data-component='chargeSummary'] div.od-line-item-row"]
|
|
@@ -17,8 +17,8 @@ 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)
|
|
21
|
-
from amazonorders.util import AmazonSessionResponse, load_class
|
|
20
|
+
SignInForm, ClaimForm, IntentForm, PasskeyForm)
|
|
21
|
+
from amazonorders.util import AmazonSessionResponse, load_class, select_one
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
24
24
|
|
|
@@ -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),
|
|
@@ -313,11 +314,9 @@ class AmazonSession:
|
|
|
313
314
|
form_found = False
|
|
314
315
|
attempts = 0
|
|
315
316
|
while not self.is_authenticated and attempts < self.config.max_auth_attempts:
|
|
316
|
-
|
|
317
|
-
# but we should find a better way to do this
|
|
317
|
+
greeting = select_one(last_response.parsed, self.config.selectors.NAV_GREETING_SELECTOR)
|
|
318
318
|
if self.auth_cookies_stored() or \
|
|
319
|
-
(self.config.constants.SIGNED_OUT_TEXT not in
|
|
320
|
-
"nav-item-signout" in last_response.response.text):
|
|
319
|
+
(greeting is not None and self.config.constants.SIGNED_OUT_TEXT not in greeting.get_text()):
|
|
321
320
|
self.is_authenticated = True
|
|
322
321
|
break
|
|
323
322
|
|
|
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
|