amazon-orders 2.0.1__tar.gz → 2.0.2__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-2.0.1 → amazon_orders-2.0.2}/CHANGELOG.md +15 -1
- {amazon_orders-2.0.1/amazon_orders.egg-info → amazon_orders-2.0.2}/PKG-INFO +1 -1
- {amazon_orders-2.0.1 → amazon_orders-2.0.2/amazon_orders.egg-info}/PKG-INFO +1 -1
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/__init__.py +1 -1
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/cli.py +5 -1
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/conf.py +3 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/order.py +20 -29
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/shipment.py +6 -1
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/selectors.py +2 -2
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_conf.py +1 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_orders.py +6 -1
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/testcase.py +10 -7
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/LICENSE +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/MANIFEST.in +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/README.md +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/SOURCES.txt +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/dependency_links.txt +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/entry_points.txt +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/requires.txt +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/top_level.txt +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/banner.txt +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/constants.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/__init__.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/item.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/parsable.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/recipient.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/seller.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/exception.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/forms.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/orders.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/session.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/util.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/pyproject.toml +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/setup.cfg +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_cli.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_session.py +0 -0
- {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_util.py +0 -0
|
@@ -4,7 +4,21 @@ 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/2.0.
|
|
7
|
+
## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/2.0.2...HEAD)
|
|
8
|
+
|
|
9
|
+
## [2.0.2](https://github.com/alexdlaird/amazon-orders/compare/2.0.1...2.0.2) - 2024-10-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `item_class` to the config file, which allows for overriding the `Item` class.
|
|
14
|
+
- Support for Amazon's new `data-component` tag on order subtotals.
|
|
15
|
+
- Build and stability improvements.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- The return value of `Order._parse_recipient()` is now optional, so parsing doesn't break digital goods without a shipping address.
|
|
20
|
+
- Redundant order ID logic to parse from the URI, simplified to consistently fetch from page.
|
|
21
|
+
- Support for order details selector on Amazon's legacy digital orders page.
|
|
8
22
|
|
|
9
23
|
## [2.0.1](https://github.com/alexdlaird/amazon-orders/compare/2.0.0...2.0.1) - 2024-10-27
|
|
10
24
|
|
|
@@ -278,7 +278,11 @@ Order #{}
|
|
|
278
278
|
order_str += f"\n Order Details Link: {order.order_details_link}"
|
|
279
279
|
order_str += f"\n Grand Total: ${order.grand_total:,.2f}"
|
|
280
280
|
order_str += f"\n Order Placed Date: {order.order_placed_date}"
|
|
281
|
-
|
|
281
|
+
if order.recipient:
|
|
282
|
+
order_str += f"\n {order.recipient}"
|
|
283
|
+
else:
|
|
284
|
+
order_str += "\n Recipient: None"
|
|
285
|
+
|
|
282
286
|
if order.payment_method:
|
|
283
287
|
order_str += f"\n Payment Method: {order.payment_method}"
|
|
284
288
|
if order.payment_method_last_4:
|
|
@@ -35,6 +35,7 @@ class AmazonOrdersConfig:
|
|
|
35
35
|
"selectors_class": "amazonorders.selectors.Selectors",
|
|
36
36
|
"order_class": "amazonorders.entity.order.Order",
|
|
37
37
|
"shipment_class": "amazonorders.entity.shipment.Shipment",
|
|
38
|
+
"item_class": "amazonorders.entity.item.Item",
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
if os.path.exists(self.config_path):
|
|
@@ -62,11 +63,13 @@ class AmazonOrdersConfig:
|
|
|
62
63
|
selectors_class_split = self.selectors_class.split(".")
|
|
63
64
|
order_class_split = self.order_class.split(".")
|
|
64
65
|
shipment_class_split = self.shipment_class.split(".")
|
|
66
|
+
item_class_split = self.item_class.split(".")
|
|
65
67
|
|
|
66
68
|
self.constants = util.load_class(constants_class_split[:-1], constants_class_split[-1])()
|
|
67
69
|
self.selectors = util.load_class(selectors_class_split[:-1], selectors_class_split[-1])()
|
|
68
70
|
self.order_cls = util.load_class(order_class_split[:-1], order_class_split[-1])
|
|
69
71
|
self.shipment_cls = util.load_class(shipment_class_split[:-1], shipment_class_split[-1])
|
|
72
|
+
self.item_cls = util.load_class(item_class_split[:-1], item_class_split[-1])
|
|
70
73
|
|
|
71
74
|
def __getattr__(self,
|
|
72
75
|
key: str) -> Any:
|
|
@@ -4,8 +4,7 @@ __license__ = "MIT"
|
|
|
4
4
|
import json
|
|
5
5
|
import logging
|
|
6
6
|
from datetime import date, datetime
|
|
7
|
-
from typing import List, Optional, TypeVar, Union
|
|
8
|
-
from urllib.parse import parse_qs, urlparse
|
|
7
|
+
from typing import Any, List, Optional, TypeVar, Union
|
|
9
8
|
|
|
10
9
|
from bs4 import BeautifulSoup, Tag
|
|
11
10
|
|
|
@@ -15,7 +14,6 @@ from amazonorders.entity.item import Item
|
|
|
15
14
|
from amazonorders.entity.parsable import Parsable
|
|
16
15
|
from amazonorders.entity.recipient import Recipient
|
|
17
16
|
from amazonorders.entity.shipment import Shipment
|
|
18
|
-
from amazonorders.exception import AmazonOrdersError
|
|
19
17
|
|
|
20
18
|
logger = logging.getLogger(__name__)
|
|
21
19
|
|
|
@@ -42,7 +40,9 @@ class Order(Parsable):
|
|
|
42
40
|
#: The Order Items.
|
|
43
41
|
self.items: List[Item] = clone.items if clone and not full_details else self._parse_items()
|
|
44
42
|
#: The Order number.
|
|
45
|
-
self.order_number: str = clone.order_number if clone else self.
|
|
43
|
+
self.order_number: str = clone.order_number if clone else self.safe_simple_parse(
|
|
44
|
+
selector=self.config.selectors.FIELD_ORDER_NUMBER_SELECTOR,
|
|
45
|
+
required=True)
|
|
46
46
|
#: The Order details link.
|
|
47
47
|
self.order_details_link: Optional[str] = clone.order_details_link if clone else self.safe_parse(
|
|
48
48
|
self._parse_order_details_link)
|
|
@@ -84,6 +84,9 @@ class Order(Parsable):
|
|
|
84
84
|
return f"Order #{self.order_number}: {self.items}"
|
|
85
85
|
|
|
86
86
|
def _parse_shipments(self) -> List[Shipment]:
|
|
87
|
+
if not self.parsed:
|
|
88
|
+
return []
|
|
89
|
+
|
|
87
90
|
shipments: List[Shipment] = [self.config.shipment_cls(x, self.config)
|
|
88
91
|
for x in util.select(self.parsed,
|
|
89
92
|
self.config.selectors.SHIPMENT_ENTITY_SELECTOR)]
|
|
@@ -91,8 +94,12 @@ class Order(Parsable):
|
|
|
91
94
|
return shipments
|
|
92
95
|
|
|
93
96
|
def _parse_items(self) -> List[Item]:
|
|
94
|
-
|
|
95
|
-
|
|
97
|
+
if not self.parsed:
|
|
98
|
+
return []
|
|
99
|
+
|
|
100
|
+
items: List[Item] = [self.config.item_cls(x, self.config)
|
|
101
|
+
for x in util.select(self.parsed,
|
|
102
|
+
self.config.selectors.ITEM_ENTITY_SELECTOR)]
|
|
96
103
|
items.sort()
|
|
97
104
|
return items
|
|
98
105
|
|
|
@@ -104,21 +111,6 @@ class Order(Parsable):
|
|
|
104
111
|
|
|
105
112
|
return value
|
|
106
113
|
|
|
107
|
-
def _parse_order_number(self) -> str:
|
|
108
|
-
try:
|
|
109
|
-
order_details_link = self._parse_order_details_link()
|
|
110
|
-
except Exception:
|
|
111
|
-
# We're not using safe_parse here because it's fine if this fails, no need for noise
|
|
112
|
-
order_details_link = None
|
|
113
|
-
|
|
114
|
-
if order_details_link:
|
|
115
|
-
parsed_url = urlparse(order_details_link)
|
|
116
|
-
value = parse_qs(parsed_url.query)["orderID"][0]
|
|
117
|
-
else:
|
|
118
|
-
value = self.simple_parse(self.config.selectors.FIELD_ORDER_NUMBER_SELECTOR, required=True)
|
|
119
|
-
|
|
120
|
-
return value
|
|
121
|
-
|
|
122
114
|
def _parse_grand_total(self) -> float:
|
|
123
115
|
value = self.simple_parse(self.config.selectors.FIELD_ORDER_GRAND_TOTAL_SELECTOR)
|
|
124
116
|
|
|
@@ -150,14 +142,14 @@ class Order(Parsable):
|
|
|
150
142
|
|
|
151
143
|
return value
|
|
152
144
|
|
|
153
|
-
def _parse_recipient(self) -> Recipient:
|
|
145
|
+
def _parse_recipient(self) -> Optional[Recipient]:
|
|
154
146
|
value = util.select_one(self.parsed, self.config.selectors.FIELD_ORDER_ADDRESS_SELECTOR)
|
|
155
147
|
|
|
156
148
|
if not value:
|
|
157
149
|
value = util.select_one(self.parsed, self.config.selectors.FIELD_ORDER_ADDRESS_FALLBACK_1_SELECTOR)
|
|
158
150
|
|
|
159
151
|
if value:
|
|
160
|
-
data_popover = value.get("data-a-popover", {}) # type: ignore[
|
|
152
|
+
data_popover = value.get("data-a-popover", {}) # type: ignore[arg-type]
|
|
161
153
|
inline_content = data_popover.get("inlineContent") # type: ignore[union-attr]
|
|
162
154
|
if inline_content:
|
|
163
155
|
value = BeautifulSoup(json.loads(inline_content), "html.parser")
|
|
@@ -172,13 +164,12 @@ class Order(Parsable):
|
|
|
172
164
|
parsed_parent,
|
|
173
165
|
self.config.selectors.FIELD_ORDER_ADDRESS_FALLBACK_2_SELECTOR
|
|
174
166
|
)
|
|
175
|
-
if not parent_tag:
|
|
176
|
-
raise AmazonOrdersError(
|
|
177
|
-
"FIELD_ORDER_ADDRESS_FALLBACK_2_SELECTOR resulted in None, but it's required. "
|
|
178
|
-
"Check if Amazon changed the expected HTML."
|
|
179
|
-
) # pragma: no cover
|
|
180
167
|
|
|
181
|
-
|
|
168
|
+
if parent_tag:
|
|
169
|
+
value = BeautifulSoup(str(parent_tag.contents[0]).strip(), "html.parser")
|
|
170
|
+
|
|
171
|
+
if not value:
|
|
172
|
+
return None
|
|
182
173
|
|
|
183
174
|
return Recipient(value, self.config)
|
|
184
175
|
|
|
@@ -50,6 +50,11 @@ class Shipment(Parsable):
|
|
|
50
50
|
return str(self.items) < str(other.items)
|
|
51
51
|
|
|
52
52
|
def _parse_items(self) -> List[Item]:
|
|
53
|
-
|
|
53
|
+
if not self.parsed:
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
items: List[Item] = [self.config.item_cls(x, self.config)
|
|
57
|
+
for x in util.select(self.parsed,
|
|
58
|
+
self.config.selectors.ITEM_ENTITY_SELECTOR)]
|
|
54
59
|
items.sort()
|
|
55
60
|
return items
|
|
@@ -36,7 +36,7 @@ class Selectors:
|
|
|
36
36
|
##########################################################################
|
|
37
37
|
|
|
38
38
|
ORDER_HISTORY_ENTITY_SELECTOR = ["div.order", "div.order-card"]
|
|
39
|
-
ORDER_DETAILS_ENTITY_SELECTOR = ["div#orderDetails", "[data-component='orderCard']"]
|
|
39
|
+
ORDER_DETAILS_ENTITY_SELECTOR = ["div#orderDetails", "div#ordersContainer", "[data-component='orderCard']"]
|
|
40
40
|
ITEM_ENTITY_SELECTOR = ["div:has(> div.yohtmlc-item)", ".item-box", "[data-component='purchasedItems']"]
|
|
41
41
|
SHIPMENT_ENTITY_SELECTOR = ["div.shipment", "div.delivery-box", "[data-component='shipments']"]
|
|
42
42
|
|
|
@@ -61,7 +61,7 @@ class Selectors:
|
|
|
61
61
|
"div.a-span3"]
|
|
62
62
|
FIELD_ORDER_PAYMENT_METHOD_SELECTOR = "img.pmts-payment-credit-card-instrument-logo"
|
|
63
63
|
FIELD_ORDER_PAYMENT_METHOD_LAST_4_SELECTOR = "img.pmts-payment-credit-card-instrument-logo"
|
|
64
|
-
FIELD_ORDER_SUBTOTALS_TAG_ITERATOR_SELECTOR = "div#od-subtotals div.a-row"
|
|
64
|
+
FIELD_ORDER_SUBTOTALS_TAG_ITERATOR_SELECTOR = ["div#od-subtotals div.a-row", "[data-component='orderSubtotals']"]
|
|
65
65
|
FIELD_ORDER_SUBTOTALS_INNER_TAG_SELECTOR = "div.a-span-last"
|
|
66
66
|
FIELD_ORDER_ADDRESS_SELECTOR = "div.displayAddressDiv"
|
|
67
67
|
FIELD_ORDER_ADDRESS_FALLBACK_1_SELECTOR = "div.recipient span.a-declarative"
|
|
@@ -50,6 +50,7 @@ class TestConf(TestCase):
|
|
|
50
50
|
with open(config.config_path, "r") as f:
|
|
51
51
|
self.assertEqual("""constants_class: amazonorders.constants.Constants
|
|
52
52
|
cookie_jar_path: {}
|
|
53
|
+
item_class: amazonorders.entity.item.Item
|
|
53
54
|
max_auth_attempts: 10
|
|
54
55
|
order_class: amazonorders.entity.order.Order
|
|
55
56
|
output_dir: {}
|
|
@@ -204,7 +204,8 @@ class TestOrders(UnitTestCase):
|
|
|
204
204
|
@responses.activate
|
|
205
205
|
def test_temp_order_history_file(self):
|
|
206
206
|
"""
|
|
207
|
-
This test
|
|
207
|
+
This test can be used to drop in an order history page at tests/output/temp-order-history.html to easily
|
|
208
|
+
run a test against it for debugging purposes.
|
|
208
209
|
"""
|
|
209
210
|
# GIVEN
|
|
210
211
|
self.amazon_session.is_authenticated = True
|
|
@@ -239,6 +240,10 @@ class TestOrders(UnitTestCase):
|
|
|
239
240
|
"place it at tests/output/temp-order-details.html")
|
|
240
241
|
@responses.activate
|
|
241
242
|
def test_temp_order_details_file(self):
|
|
243
|
+
"""
|
|
244
|
+
This test can be used to drop in an order details page at tests/output/temp-order-details.html to easily
|
|
245
|
+
run a test against it for debugging purposes.
|
|
246
|
+
"""
|
|
242
247
|
# GIVEN
|
|
243
248
|
self.amazon_session.is_authenticated = True
|
|
244
249
|
order_id = "temp-1234"
|
|
@@ -358,10 +358,11 @@ class TestCase(unittest.TestCase):
|
|
|
358
358
|
self.assertIsNotNone(order.grand_total)
|
|
359
359
|
self.assertIsNotNone(order.order_details_link)
|
|
360
360
|
self.assertIsNotNone(order.order_placed_date)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
361
|
+
if order.recipient:
|
|
362
|
+
self.assertIsNotNone(order.recipient.name)
|
|
363
|
+
self.assertIsNotNone(order.recipient.address)
|
|
364
|
+
self.assertGreaterEqual(len(order.shipments), 1)
|
|
365
|
+
self.assertEqual(str(order.items), str(order.shipments[0].items))
|
|
365
366
|
self.assertGreaterEqual(len(order.items), 1)
|
|
366
367
|
self.assertIsNotNone(order.items[0].title)
|
|
367
368
|
self.assertIsNotNone(order.items[0].link)
|
|
@@ -372,10 +373,12 @@ class TestCase(unittest.TestCase):
|
|
|
372
373
|
self.assertIsNotNone(order.payment_method)
|
|
373
374
|
self.assertEqual(4, len(order.payment_method_last_4))
|
|
374
375
|
self.assertIsNotNone(order.subtotal)
|
|
375
|
-
|
|
376
|
+
if order.recipient:
|
|
377
|
+
self.assertIsNotNone(order.shipping_total)
|
|
376
378
|
self.assertIsNotNone(order.total_before_tax)
|
|
377
379
|
self.assertIsNotNone(order.estimated_tax)
|
|
378
380
|
# As of April 2024, this is no longer shown in Order History
|
|
379
381
|
# self.assertIsNotNone(order.items[0].condition)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
+
if order.recipient:
|
|
383
|
+
self.assertIsNotNone(order.items[0].price)
|
|
384
|
+
self.assertIsNotNone(order.items[0].seller.name)
|
|
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
|