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.
Files changed (37) hide show
  1. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/CHANGELOG.md +15 -1
  2. {amazon_orders-2.0.1/amazon_orders.egg-info → amazon_orders-2.0.2}/PKG-INFO +1 -1
  3. {amazon_orders-2.0.1 → amazon_orders-2.0.2/amazon_orders.egg-info}/PKG-INFO +1 -1
  4. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/__init__.py +1 -1
  5. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/cli.py +5 -1
  6. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/conf.py +3 -0
  7. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/order.py +20 -29
  8. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/shipment.py +6 -1
  9. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/selectors.py +2 -2
  10. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_conf.py +1 -0
  11. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_orders.py +6 -1
  12. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/testcase.py +10 -7
  13. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/LICENSE +0 -0
  14. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/MANIFEST.in +0 -0
  15. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/README.md +0 -0
  16. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/SOURCES.txt +0 -0
  17. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/dependency_links.txt +0 -0
  18. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/entry_points.txt +0 -0
  19. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/requires.txt +0 -0
  20. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazon_orders.egg-info/top_level.txt +0 -0
  21. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/banner.txt +0 -0
  22. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/constants.py +0 -0
  23. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/__init__.py +0 -0
  24. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/item.py +0 -0
  25. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/parsable.py +0 -0
  26. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/recipient.py +0 -0
  27. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/entity/seller.py +0 -0
  28. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/exception.py +0 -0
  29. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/forms.py +0 -0
  30. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/orders.py +0 -0
  31. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/session.py +0 -0
  32. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/amazonorders/util.py +0 -0
  33. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/pyproject.toml +0 -0
  34. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/setup.cfg +0 -0
  35. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_cli.py +0 -0
  36. {amazon_orders-2.0.1 → amazon_orders-2.0.2}/tests/test_session.py +0 -0
  37. {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.1...HEAD)
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
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: amazon-orders
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: A CLI and library for interacting with Amazon order history.
5
5
  Maintainer-email: Alex Laird <contact@alexlaird.com>
6
6
  License: MIT License
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: amazon-orders
3
- Version: 2.0.1
3
+ Version: 2.0.2
4
4
  Summary: A CLI and library for interacting with Amazon order history.
5
5
  Maintainer-email: Alex Laird <contact@alexlaird.com>
6
6
  License: MIT License
@@ -1,3 +1,3 @@
1
1
  __copyright__ = "Copyright (c) 2024 Alex Laird"
2
2
  __license__ = "MIT"
3
- __version__ = "2.0.1"
3
+ __version__ = "2.0.2"
@@ -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
- order_str += f"\n {order.recipient}"
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, Any
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.safe_parse(self._parse_order_number)
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
- items = [Item(x, self.config)
95
- for x in util.select(self.parsed, self.config.selectors.ITEM_ENTITY_SELECTOR)]
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[var-annotated]
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
- value = BeautifulSoup(str(parent_tag.contents[0]).strip(), "html.parser")
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
- items = [Item(x, self.config) for x in util.select(self.parsed, self.config.selectors.ITEM_ENTITY_SELECTOR)]
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
- self.assertIsNotNone(order.recipient.name)
362
- self.assertIsNotNone(order.recipient.address)
363
- self.assertGreaterEqual(len(order.shipments), 1)
364
- self.assertEqual(str(order.items), str(order.shipments[0].items))
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
- self.assertIsNotNone(order.shipping_total)
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
- self.assertIsNotNone(order.items[0].price)
381
- self.assertIsNotNone(order.items[0].seller.name)
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