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.
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/CHANGELOG.md +13 -2
- {amazon_orders-4.9.3/amazon_orders.egg-info → amazon_orders-4.9.4}/PKG-INFO +6 -4
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/README.md +5 -3
- {amazon_orders-4.9.3 → amazon_orders-4.9.4/amazon_orders.egg-info}/PKG-INFO +6 -4
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/SOURCES.txt +1 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/__init__.py +1 -1
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/cli.py +38 -0
- {amazon_orders-4.9.3 → 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.3 → amazon_orders-4.9.4}/amazonorders/entity/order.py +12 -1
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/forms.py +89 -10
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/orders.py +60 -6
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/output.py +2 -2
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/selectors.py +20 -6
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/session.py +2 -1
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/LICENSE +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/MANIFEST.in +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/dependency_links.txt +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/entry_points.txt +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/requires.txt +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazon_orders.egg-info/top_level.txt +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/banner.txt +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/conf.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/__init__.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/browser/__init__.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/browser/playwright.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/__init__.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/anticaptcha.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/base.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/capsolver.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/contrib/waf/twocaptcha.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/__init__.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/gift_card_activity.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/item.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/parsable.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/recipient.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/seller.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/shipment.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/tracking.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/entity/transaction.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/exception.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/gift_cards.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/paging.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/region_data.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/transactions.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/amazonorders/util.py +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/pyproject.toml +0 -0
- {amazon_orders-4.9.3 → amazon_orders-4.9.4}/setup.cfg +0 -0
- {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.
|
|
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
|
|
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
|
+
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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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
|
|
|
@@ -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]
|
|
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
|
-
|
|
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
|
|
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
|