amazon-orders 3.2.2__tar.gz → 3.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/CHANGELOG.md +19 -2
  2. {amazon_orders-3.2.2/amazon_orders.egg-info → amazon_orders-3.2.4}/PKG-INFO +3 -2
  3. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/README.md +1 -1
  4. {amazon_orders-3.2.2 → amazon_orders-3.2.4/amazon_orders.egg-info}/PKG-INFO +3 -2
  5. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazon_orders.egg-info/requires.txt +1 -0
  6. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/__init__.py +1 -1
  7. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/conf.py +1 -0
  8. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/order.py +11 -4
  9. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/parsable.py +21 -3
  10. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/orders.py +2 -3
  11. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/selectors.py +12 -3
  12. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/session.py +1 -1
  13. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/pyproject.toml +2 -1
  14. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/tests/test_conf.py +3 -1
  15. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/tests/test_orders.py +24 -0
  16. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/tests/test_transactions.py +1 -1
  17. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/LICENSE +0 -0
  18. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/MANIFEST.in +0 -0
  19. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazon_orders.egg-info/SOURCES.txt +0 -0
  20. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazon_orders.egg-info/dependency_links.txt +0 -0
  21. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazon_orders.egg-info/entry_points.txt +0 -0
  22. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazon_orders.egg-info/top_level.txt +0 -0
  23. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/banner.txt +0 -0
  24. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/cli.py +0 -0
  25. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/constants.py +0 -0
  26. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/__init__.py +0 -0
  27. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/item.py +0 -0
  28. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/recipient.py +0 -0
  29. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/seller.py +0 -0
  30. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/shipment.py +0 -0
  31. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/entity/transaction.py +0 -0
  32. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/exception.py +0 -0
  33. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/forms.py +0 -0
  34. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/transactions.py +0 -0
  35. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/amazonorders/util.py +0 -0
  36. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/setup.cfg +0 -0
  37. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/tests/test_cli.py +0 -0
  38. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/tests/test_session.py +0 -0
  39. {amazon_orders-3.2.2 → amazon_orders-3.2.4}/tests/test_util.py +0 -0
  40. {amazon_orders-3.2.2 → amazon_orders-3.2.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/3.2.2...HEAD)
7
+ ## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/3.2.4...HEAD)
8
+
9
+ ## [3.2.4](https://github.com/alexdlaird/amazon-orders/compare/3.2.3...3.2.4) - 2025-02-11
10
+
11
+ ### Added
12
+
13
+ - `Order.promotion_applied` field, which is now parsed alongside other subtotals.
14
+
15
+ ### Fixed
16
+
17
+ - Broken parsing of Whole Foods Market orders, these are now skipped.
18
+ - Parsing issue on `Order.order_number` and `Order.order_placed` due to changes in Amazon.com DOM.
19
+
20
+ ## [3.2.3](https://github.com/alexdlaird/amazon-orders/compare/3.2.2...3.2.3) - 2025-02-06
21
+
22
+ ### Added
23
+
24
+ - `bs4_parser` to the config file, which allows for overriding the parser used by BeautifulSoup, defaulting to the built-in `html.parser`.
8
25
 
9
26
  ## [3.2.2](https://github.com/alexdlaird/amazon-orders/compare/3.2.1...3.2.2) - 2025-01-28
10
27
 
@@ -257,7 +274,7 @@ Build and stability improvements.
257
274
  - [AuthForm](https://amazon-orders.readthedocs.io/api.html#amazonorders.forms.AuthForm) abstract class, and
258
275
  migrated all auth flow items to subclasses of this class.
259
276
  - [Parsable.simple_parse()](https://amazon-orders.readthedocs.io/api.html#amazonorders.entities.parsable.Parsable.simple_parse),
260
- which can handle most basic fields when parised with CSS selectors.
277
+ which can handle most basic fields when parsed with CSS selectors.
261
278
  - Stability improvements.
262
279
  - Test improvements.
263
280
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: amazon-orders
3
- Version: 3.2.2
3
+ Version: 3.2.4
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
@@ -62,6 +62,7 @@ Requires-Dist: responses; extra == "dev"
62
62
  Requires-Dist: flask; extra == "dev"
63
63
  Requires-Dist: twilio; extra == "dev"
64
64
  Requires-Dist: pyngrok; extra == "dev"
65
+ Requires-Dist: lxml; extra == "dev"
65
66
  Provides-Extra: docs
66
67
  Requires-Dist: Sphinx; extra == "docs"
67
68
  Requires-Dist: sphinx-notfound-page; extra == "docs"
@@ -87,7 +88,7 @@ be used to interact with Amazon.com's consumer-facing website.
87
88
 
88
89
  This works by parsing website data from Amazon.com. A periodic build validates functionality to ensure its
89
90
  stability, but as Amazon provides no official API to use, this package may break at any time. This
90
- package only officially supports the English version of the website.
91
+ package only officially supports the English, `.com` version of Amazon.
91
92
 
92
93
  ## Installation
93
94
 
@@ -12,7 +12,7 @@ be used to interact with Amazon.com's consumer-facing website.
12
12
 
13
13
  This works by parsing website data from Amazon.com. A periodic build validates functionality to ensure its
14
14
  stability, but as Amazon provides no official API to use, this package may break at any time. This
15
- package only officially supports the English version of the website.
15
+ package only officially supports the English, `.com` version of Amazon.
16
16
 
17
17
  ## Installation
18
18
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: amazon-orders
3
- Version: 3.2.2
3
+ Version: 3.2.4
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
@@ -62,6 +62,7 @@ Requires-Dist: responses; extra == "dev"
62
62
  Requires-Dist: flask; extra == "dev"
63
63
  Requires-Dist: twilio; extra == "dev"
64
64
  Requires-Dist: pyngrok; extra == "dev"
65
+ Requires-Dist: lxml; extra == "dev"
65
66
  Provides-Extra: docs
66
67
  Requires-Dist: Sphinx; extra == "docs"
67
68
  Requires-Dist: sphinx-notfound-page; extra == "docs"
@@ -87,7 +88,7 @@ be used to interact with Amazon.com's consumer-facing website.
87
88
 
88
89
  This works by parsing website data from Amazon.com. A periodic build validates functionality to ensure its
89
90
  stability, but as Amazon provides no official API to use, this package may break at any time. This
90
- package only officially supports the English version of the website.
91
+ package only officially supports the English, `.com` version of Amazon.
91
92
 
92
93
  ## Installation
93
94
 
@@ -16,6 +16,7 @@ responses
16
16
  flask
17
17
  twilio
18
18
  pyngrok
19
+ lxml
19
20
 
20
21
  [docs]
21
22
  Sphinx
@@ -1,3 +1,3 @@
1
1
  __copyright__ = "Copyright (c) 2024 Alex Laird"
2
2
  __license__ = "MIT"
3
- __version__ = "3.2.2"
3
+ __version__ = "3.2.4"
@@ -36,6 +36,7 @@ class AmazonOrdersConfig:
36
36
  "order_class": "amazonorders.entity.order.Order",
37
37
  "shipment_class": "amazonorders.entity.shipment.Shipment",
38
38
  "item_class": "amazonorders.entity.item.Item",
39
+ "bs4_parser": "html.parser",
39
40
  }
40
41
 
41
42
  if os.path.exists(self.config_path):
@@ -43,7 +43,9 @@ class Order(Parsable):
43
43
  #: The Order number.
44
44
  self.order_number: str = clone.order_number if clone else self.safe_simple_parse(
45
45
  selector=self.config.selectors.FIELD_ORDER_NUMBER_SELECTOR,
46
- required=True)
46
+ required=True,
47
+ prefix_split="#",
48
+ prefix_split_fuzzy=True)
47
49
  #: The Order details link.
48
50
  self.order_details_link: Optional[str] = clone.order_details_link if clone else self.safe_parse(
49
51
  self._parse_order_details_link)
@@ -51,7 +53,10 @@ class Order(Parsable):
51
53
  self.grand_total: float = clone.grand_total if clone else self.safe_parse(self._parse_grand_total)
52
54
  #: The Order placed date.
53
55
  self.order_placed_date: date = clone.order_placed_date if clone else self.safe_simple_parse(
54
- selector=self.config.selectors.FIELD_ORDER_PLACED_DATE_SELECTOR, parse_date=True)
56
+ selector=self.config.selectors.FIELD_ORDER_PLACED_DATE_SELECTOR,
57
+ suffix_split="Order #",
58
+ suffix_split_fuzzy=True,
59
+ parse_date=True)
55
60
  #: The Order Recipients.
56
61
  self.recipient: Recipient = clone.recipient if clone else self.safe_parse(self._parse_recipient)
57
62
 
@@ -69,6 +74,8 @@ class Order(Parsable):
69
74
  self.subtotal: Optional[float] = self._if_full_details(self._parse_currency("subtotal"))
70
75
  #: The Order shipping total. Only populated when ``full_details`` is ``True``.
71
76
  self.shipping_total: Optional[float] = self._if_full_details(self._parse_currency("shipping"))
77
+ #: The Order promotion applied. Only populated when ``full_details`` is ``True``.
78
+ self.promotion_applied: Optional[float] = self._if_full_details(self._parse_currency("promotion"))
72
79
  #: The Order Subscribe & Save discount. Only populated when ``full_details`` is ``True``.
73
80
  self.subscription_discount: Optional[float] = self._if_full_details(self._parse_currency("subscribe"))
74
81
  #: The Order total before tax. Only populated when ``full_details`` is ``True``.
@@ -146,7 +153,7 @@ class Order(Parsable):
146
153
  data_popover = value.get("data-a-popover", {}) # type: ignore[arg-type, var-annotated]
147
154
  inline_content = data_popover.get("inlineContent") # type: ignore[union-attr]
148
155
  if inline_content:
149
- value = BeautifulSoup(json.loads(inline_content), "html.parser")
156
+ value = BeautifulSoup(json.loads(inline_content), self.config.bs4_parser)
150
157
 
151
158
  if not value:
152
159
  # TODO: there are multiple shipToData tags, we should double check we're picking the right one
@@ -166,7 +173,7 @@ class Order(Parsable):
166
173
  )
167
174
 
168
175
  if parent_tag:
169
- value = BeautifulSoup(str(parent_tag.contents[0]).strip(), "html.parser")
176
+ value = BeautifulSoup(str(parent_tag.contents[0]).strip(), self.config.bs4_parser)
170
177
 
171
178
  if not value:
172
179
  return None
@@ -67,7 +67,10 @@ class Parsable:
67
67
  required: bool = False,
68
68
  prefix_split: Optional[str] = None,
69
69
  wrap_tag: Optional[Type] = None,
70
- parse_date: bool = False) -> Any:
70
+ parse_date: bool = False,
71
+ prefix_split_fuzzy: bool = False,
72
+ suffix_split: Optional[str] = None,
73
+ suffix_split_fuzzy: bool = False) -> Any:
71
74
  """
72
75
  Will attempt to extract the text value of the given CSS selector(s) for a field, and
73
76
  is suitable for most basic functionality on a well-formed page.
@@ -86,6 +89,9 @@ class Parsable:
86
89
  :param wrap_tag: Wrap the selected tag in this class before returning.
87
90
  :param parse_date: ``True`` if the resulting value should be fuzzy parsed in to a date (returning ``None`` if
88
91
  parsing fails).
92
+ :param prefix_split_fuzzy: ``True`` if the value should still be used even if ``prefix_split`` is not found.
93
+ :param suffix_split: Only select the field with the given suffix, returning the left side of the split if so.
94
+ :param suffix_split_fuzzy: ``True`` if the value should still be used even if ``suffix_split`` is not found.
89
95
  :return: The cleaned up return value from the parsed ``selector``.
90
96
  """
91
97
  if isinstance(selector, str):
@@ -109,12 +115,24 @@ class Parsable:
109
115
 
110
116
  if prefix_split:
111
117
  if prefix_split not in tag.text:
112
- continue
118
+ if prefix_split_fuzzy:
119
+ value = tag.text.strip()
120
+ else:
121
+ continue
113
122
  else:
114
123
  value = tag.text.strip().split(prefix_split)[1]
115
124
  else:
116
125
  value = tag.text
117
126
 
127
+ if suffix_split:
128
+ if suffix_split not in value:
129
+ if suffix_split_fuzzy:
130
+ value = value.strip()
131
+ else:
132
+ continue
133
+ else:
134
+ value = value.strip().split(suffix_split)[0]
135
+
118
136
  if wrap_tag:
119
137
  value = wrap_tag(tag, self.config)
120
138
  else:
@@ -142,7 +160,7 @@ class Parsable:
142
160
  """
143
161
  A helper function that uses :func:`simple_parse` as the ``parse_function()`` passed to :func:`safe_parse`.
144
162
 
145
- :param selector: The selector to pass to :func:`simple_parse`.
163
+ :param selector: The CSS selector to pass to :func:`simple_parse`.
146
164
  :param kwargs: The ``kwargs`` will be passed to ``parse_function``.
147
165
  :return: The return value from :func:`simple_parse`.
148
166
  """
@@ -73,9 +73,8 @@ class AmazonOrders:
73
73
  response_parsed = self.amazon_session.last_response_parsed
74
74
 
75
75
  for order_tag in util.select(response_parsed, self.config.selectors.ORDER_HISTORY_ENTITY_SELECTOR):
76
- # If we find a brand logo (for instance, Amazon Fresh), we don't know how to parse this. If we know how
77
- # to do this in the future, we can implement it, but right now we have no reliable way, so skipping
78
- # these orders.
76
+ # First check if this Order is known to be of a type that we do not currently have a way to support
77
+ # parsing, meaning it should be skipped
79
78
  if util.select(order_tag, self.config.selectors.ORDER_HISTORY_BRAND_SELECTOR):
80
79
  continue
81
80
 
@@ -55,7 +55,14 @@ class Selectors:
55
55
  SHIPMENT_ENTITY_SELECTOR = ["[data-component='orderCard'] [data-component='shipments'] .a-box",
56
56
  "div.shipment",
57
57
  "div.delivery-box"]
58
- ORDER_HISTORY_BRAND_SELECTOR = ".brand-info-box .brand-logo img"
58
+ # Selectors defined here, if found in an Order, will cause the Order to be skipped, since it means we currently
59
+ # do not have a way to support fully parsing its details
60
+ ORDER_HISTORY_BRAND_SELECTOR = [
61
+ # Amazon Fresh is not supported
62
+ ".brand-info-box .brand-logo img",
63
+ # Whole Foods Market is not supported
64
+ "a.yohtmlc-order-details-link[href^='/wholefoodsmarket']"
65
+ ]
59
66
 
60
67
  #####################################
61
68
  # CSS selectors for Item fields
@@ -81,12 +88,14 @@ class Selectors:
81
88
  #####################################
82
89
 
83
90
  FIELD_ORDER_DETAILS_LINK_SELECTOR = "a.yohtmlc-order-details-link"
84
- FIELD_ORDER_NUMBER_SELECTOR = [".order-date-invoice-item bdi[dir='ltr']",
91
+ FIELD_ORDER_NUMBER_SELECTOR = ["[data-component='briefOrderInfo'] div.a-column",
92
+ ".order-date-invoice-item bdi[dir='ltr']",
85
93
  "bdi[dir='ltr']",
86
94
  "span[dir='ltr']"]
87
95
  FIELD_ORDER_GRAND_TOTAL_SELECTOR = ["div.yohtmlc-order-total span.value",
88
96
  "div.order-header div.a-column.a-span2"]
89
- FIELD_ORDER_PLACED_DATE_SELECTOR = ["span.order-date-invoice-item",
97
+ FIELD_ORDER_PLACED_DATE_SELECTOR = ["[data-component='briefOrderInfo'] div.a-column",
98
+ "span.order-date-invoice-item",
90
99
  "div.a-span3"]
91
100
  FIELD_ORDER_PAYMENT_METHOD_SELECTOR = "img.pmts-payment-credit-card-instrument-logo"
92
101
  FIELD_ORDER_PAYMENT_METHOD_LAST_4_SELECTOR = "span:has(img.pmts-payment-credit-card-instrument-logo):last-child"
@@ -141,7 +141,7 @@ class AmazonSession:
141
141
 
142
142
  self.last_response = self.session.request(method, url, **kwargs)
143
143
  self.last_response_parsed = BeautifulSoup(self.last_response.text,
144
- "html.parser")
144
+ self.config.bs4_parser)
145
145
 
146
146
  cookies = dict_from_cookiejar(self.session.cookies)
147
147
  if os.path.exists(self.config.cookie_jar_path):
@@ -41,7 +41,8 @@ dev = [
41
41
  "responses",
42
42
  "flask",
43
43
  "twilio",
44
- "pyngrok"
44
+ "pyngrok",
45
+ "lxml"
45
46
  ]
46
47
  docs = [
47
48
  "Sphinx",
@@ -41,6 +41,7 @@ class TestConf(TestCase):
41
41
  self.assertEqual(10, config.max_auth_attempts)
42
42
  self.assertEqual(self.test_output_dir, config.output_dir)
43
43
  self.assertEqual(self.test_cookie_jar_path, config.cookie_jar_path)
44
+ self.assertEqual("html.parser", config.bs4_parser)
44
45
 
45
46
  # GIVEN
46
47
  config.save()
@@ -48,7 +49,8 @@ class TestConf(TestCase):
48
49
  # THEN
49
50
  self.assertTrue(os.path.exists(config_path))
50
51
  with open(config.config_path, "r") as f:
51
- self.assertEqual("""constants_class: amazonorders.constants.Constants
52
+ self.assertEqual("""bs4_parser: html.parser
53
+ constants_class: amazonorders.constants.Constants
52
54
  cookie_jar_path: {}
53
55
  item_class: amazonorders.entity.item.Item
54
56
  max_auth_attempts: 10
@@ -127,6 +127,30 @@ class TestOrders(UnitTestCase):
127
127
  self.assertEqual(1, resp1.call_count)
128
128
  self.assertEqual(1, resp2.call_count)
129
129
 
130
+ @responses.activate
131
+ def test_get_order_history_skip_wholefoods(self):
132
+ # GIVEN
133
+ self.amazon_session.is_authenticated = True
134
+ year = 2024
135
+ start_index = 0
136
+ resp1 = self.given_order_history_landing_exists()
137
+ with open(os.path.join(self.RESOURCES_DIR, "order-history-wholefoods.html"), "r",
138
+ encoding="utf-8") as f:
139
+ resp2 = responses.add(
140
+ responses.GET,
141
+ self.test_config.constants.ORDER_HISTORY_URL,
142
+ body=f.read(),
143
+ status=200,
144
+ )
145
+
146
+ # WHEN
147
+ orders = self.amazon_orders.get_order_history(year=year, start_index=start_index)
148
+
149
+ # THEN
150
+ self.assertEqual(9, len(orders))
151
+ self.assertEqual(1, resp1.call_count)
152
+ self.assertEqual(1, resp2.call_count)
153
+
130
154
  @responses.activate
131
155
  def test_get_order_history_full_details(self):
132
156
  # GIVEN
@@ -57,7 +57,7 @@ class TestOrders(UnitTestCase):
57
57
 
58
58
  def test_parse_transaction_form_tag(self):
59
59
  # GIVEN
60
- parsed = BeautifulSoup(TEST_PARSE_TRANSACTION_FORM_TAG_HTML, "html.parser")
60
+ parsed = BeautifulSoup(TEST_PARSE_TRANSACTION_FORM_TAG_HTML, self.test_config.bs4_parser)
61
61
  form_tag = parsed.select_one("form")
62
62
 
63
63
  # WHEN
File without changes
File without changes
File without changes