amazon-orders 3.2.5__tar.gz → 3.2.7__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 (41) hide show
  1. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/CHANGELOG.md +18 -1
  2. {amazon_orders-3.2.5/amazon_orders.egg-info → amazon_orders-3.2.7}/PKG-INFO +1 -1
  3. {amazon_orders-3.2.5 → amazon_orders-3.2.7/amazon_orders.egg-info}/PKG-INFO +1 -1
  4. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/__init__.py +1 -1
  5. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/order.py +2 -2
  6. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/transaction.py +1 -1
  7. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/orders.py +0 -5
  8. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/selectors.py +6 -6
  9. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/transactions.py +1 -1
  10. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/tests/test_cli.py +3 -2
  11. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/tests/test_orders.py +27 -15
  12. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/tests/test_session.py +30 -26
  13. amazon_orders-3.2.7/tests/test_transactions.py +131 -0
  14. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/tests/test_util.py +1 -1
  15. amazon_orders-3.2.5/tests/test_transactions.py +0 -166
  16. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/LICENSE +0 -0
  17. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/MANIFEST.in +0 -0
  18. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/README.md +0 -0
  19. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazon_orders.egg-info/SOURCES.txt +0 -0
  20. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazon_orders.egg-info/dependency_links.txt +0 -0
  21. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazon_orders.egg-info/entry_points.txt +0 -0
  22. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazon_orders.egg-info/requires.txt +0 -0
  23. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazon_orders.egg-info/top_level.txt +0 -0
  24. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/banner.txt +0 -0
  25. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/cli.py +0 -0
  26. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/conf.py +0 -0
  27. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/constants.py +0 -0
  28. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/__init__.py +0 -0
  29. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/item.py +0 -0
  30. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/parsable.py +0 -0
  31. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/recipient.py +0 -0
  32. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/seller.py +0 -0
  33. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/entity/shipment.py +0 -0
  34. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/exception.py +0 -0
  35. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/forms.py +0 -0
  36. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/session.py +0 -0
  37. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/amazonorders/util.py +0 -0
  38. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/pyproject.toml +0 -0
  39. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/setup.cfg +0 -0
  40. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/tests/test_conf.py +0 -0
  41. {amazon_orders-3.2.5 → amazon_orders-3.2.7}/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.5...HEAD)
7
+ ## [Unreleased](https://github.com/alexdlaird/amazon-orders/compare/3.2.7...HEAD)
8
+
9
+ ## [3.2.7](https://github.com/alexdlaird/amazon-orders/compare/3.2.6...3.2.7) - 2025-02-17
10
+
11
+ ### Added
12
+
13
+ - Fixes for parsing Amazon Fresh and Whole Foods Market orders, so they no longer need to be skipped (but their Items and Shipments will still be empty).
14
+
15
+ ## [3.2.6](https://github.com/alexdlaird/amazon-orders/compare/3.2.5...3.2.6) - 2025-02-17
16
+
17
+ ### Added
18
+
19
+ - Add generic integration tests for Transactions, now in weekly run.
20
+ - Other test improvements.
21
+
22
+ ### Fixed
23
+
24
+ - Broken parsing when Transaction is pending.
8
25
 
9
26
  ## [3.2.5](https://github.com/alexdlaird/amazon-orders/compare/3.2.4...3.2.5) - 2025-02-12
10
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: amazon-orders
3
- Version: 3.2.5
3
+ Version: 3.2.7
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.2
2
2
  Name: amazon-orders
3
- Version: 3.2.5
3
+ Version: 3.2.7
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__ = "3.2.5"
3
+ __version__ = "3.2.7"
@@ -92,7 +92,7 @@ class Order(Parsable):
92
92
  return f"Order #{self.order_number}: {self.items}"
93
93
 
94
94
  def _parse_shipments(self) -> List[Shipment]:
95
- if not self.parsed:
95
+ if not self.parsed or len(util.select(self.parsed, self.config.selectors.ORDER_SKIP_ITEMS)) > 0:
96
96
  return []
97
97
 
98
98
  shipments: List[Shipment] = [self.config.shipment_cls(x, self.config)
@@ -102,7 +102,7 @@ class Order(Parsable):
102
102
  return shipments
103
103
 
104
104
  def _parse_items(self) -> List[Item]:
105
- if not self.parsed:
105
+ if not self.parsed or len(util.select(self.parsed, self.config.selectors.ORDER_SKIP_ITEMS)) > 0:
106
106
  return []
107
107
 
108
108
  items: List[Item] = [self.config.item_cls(x, self.config)
@@ -1,4 +1,4 @@
1
- __copyright__ = "Copyright (c) 2024 Jeff Sawatzky"
1
+ __copyright__ = "Copyright (c) 2024 Alex Laird"
2
2
  __license__ = "MIT"
3
3
 
4
4
  import logging
@@ -73,11 +73,6 @@ 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
- # 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
78
- if util.select(order_tag, self.config.selectors.ORDER_HISTORY_BRAND_SELECTOR):
79
- continue
80
-
81
76
  order: Order = self.config.order_cls(order_tag, self.config)
82
77
 
83
78
  if full_details:
@@ -55,12 +55,12 @@ 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
- # 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
58
+ # Selectors defined here mean we don't have a reliable way to parse all details in an Order, so Items and
59
+ # Shipments will be skipped
60
+ ORDER_SKIP_ITEMS = [
61
+ # Identifies an Amazon Fresh order
62
62
  ".brand-info-box .brand-logo img",
63
- # Whole Foods Market is not supported
63
+ # Identifies a Whole Foods Market order
64
64
  "a.yohtmlc-order-details-link[href^='/wholefoodsmarket']"
65
65
  ]
66
66
 
@@ -160,7 +160,7 @@ class Selectors:
160
160
  FIELD_TRANSACTION_GRAND_TOTAL_SELECTOR = [
161
161
  "div.apx-transactions-line-item-component-container > div:nth-child(1) span.a-size-base-plus"]
162
162
  FIELD_TRANSACTION_ORDER_NUMBER_SELECTOR = [
163
- "div.apx-transactions-line-item-component-container > div:nth-child(2) a.a-link-normal"]
163
+ "div.apx-transactions-line-item-component-container div .a-span12 a"]
164
164
  FIELD_TRANSACTION_ORDER_LINK_SELECTOR = [
165
165
  "div.apx-transactions-line-item-component-container > div:nth-child(2) a.a-link-normal"]
166
166
  FIELD_TRANSACTION_SELLER_NAME_SELECTOR = [
@@ -1,4 +1,4 @@
1
- __copyright__ = "Copyright (c) 2024 Jeff Sawatzky"
1
+ __copyright__ = "Copyright (c) 2024 Alex Laird"
2
2
  __license__ = "MIT"
3
3
 
4
4
  import datetime
@@ -65,7 +65,7 @@ class TestCli(UnitTestCase):
65
65
  # GIVEN
66
66
  order_id = "112-2961628-4757846"
67
67
  self.given_login_responses_success()
68
- with open(os.path.join(self.RESOURCES_DIR, "order-details-112-2961628-4757846.html"), "r",
68
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-details-112-2961628-4757846.html"), "r",
69
69
  encoding="utf-8") as f:
70
70
  resp1 = responses.add(
71
71
  responses.GET,
@@ -92,7 +92,8 @@ class TestCli(UnitTestCase):
92
92
  mock_get_today.date.today.return_value = datetime.date(2024, 10, 11)
93
93
  days = 1
94
94
  self.given_login_responses_success()
95
- with open(os.path.join(self.RESOURCES_DIR, "get-transactions.html"), "r", encoding="utf-8") as f:
95
+ with open(os.path.join(self.RESOURCES_DIR, "transactions", "get-transactions-snippet.html"),
96
+ "r", encoding="utf-8") as f:
96
97
  resp = responses.add(
97
98
  responses.GET,
98
99
  f"{self.test_config.constants.TRANSACTION_HISTORY_LANDING_URL}",
@@ -85,7 +85,7 @@ class TestOrders(UnitTestCase):
85
85
  year = 2024
86
86
  start_index = 0
87
87
  resp1 = self.given_order_history_landing_exists()
88
- with open(os.path.join(self.RESOURCES_DIR, "order-history-egift.html"), "r",
88
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-egift.html"), "r",
89
89
  encoding="utf-8") as f:
90
90
  resp2 = responses.add(
91
91
  responses.GET,
@@ -98,7 +98,7 @@ class TestOrders(UnitTestCase):
98
98
  orders = self.amazon_orders.get_order_history(year=year, start_index=start_index)
99
99
 
100
100
  # THEN
101
- self.assertEqual(9, len(orders))
101
+ self.assertEqual(10, len(orders))
102
102
  self.assertEqual(1, resp1.call_count)
103
103
  self.assertEqual(1, resp2.call_count)
104
104
  order = orders[5]
@@ -119,7 +119,7 @@ class TestOrders(UnitTestCase):
119
119
  year = 2010
120
120
  resp1 = self.given_order_history_landing_exists()
121
121
  resp2 = self.given_order_history_exists(year, 0)
122
- with open(os.path.join(self.RESOURCES_DIR, f"order-history-{year}-10.html"), "r",
122
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-history-{year}-10.html"), "r",
123
123
  encoding="utf-8") as f:
124
124
  resp3 = responses.add(
125
125
  responses.GET,
@@ -139,13 +139,13 @@ class TestOrders(UnitTestCase):
139
139
  self.assertEqual(1, resp3.call_count)
140
140
 
141
141
  @responses.activate
142
- def test_get_order_history_skip_fresh(self):
142
+ def test_get_order_history_fresh(self):
143
143
  # GIVEN
144
144
  self.amazon_session.is_authenticated = True
145
145
  year = 2024
146
146
  start_index = 0
147
147
  resp1 = self.given_order_history_landing_exists()
148
- with open(os.path.join(self.RESOURCES_DIR, "order-history-fresh.html"), "r",
148
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-fresh.html"), "r",
149
149
  encoding="utf-8") as f:
150
150
  resp2 = responses.add(
151
151
  responses.GET,
@@ -158,18 +158,24 @@ class TestOrders(UnitTestCase):
158
158
  orders = self.amazon_orders.get_order_history(year=year, start_index=start_index)
159
159
 
160
160
  # THEN
161
- self.assertEqual(9, len(orders))
161
+ self.assertEqual(10, len(orders))
162
162
  self.assertEqual(1, resp1.call_count)
163
163
  self.assertEqual(1, resp2.call_count)
164
+ order = orders[4]
165
+ self.assertEqual("111-2072777-8279433", order.order_number)
166
+ self.assertEqual(80.27, order.grand_total)
167
+ self.assertIsNotNone(order.order_details_link)
168
+ self.assertEqual(date(2025, 1, 3), order.order_placed_date)
169
+ self.assertEqual(0, len(order.items))
164
170
 
165
171
  @responses.activate
166
- def test_get_order_history_skip_wholefoods(self):
172
+ def test_get_order_history_wholefoods(self):
167
173
  # GIVEN
168
174
  self.amazon_session.is_authenticated = True
169
175
  year = 2024
170
176
  start_index = 0
171
177
  resp1 = self.given_order_history_landing_exists()
172
- with open(os.path.join(self.RESOURCES_DIR, "order-history-wholefoods.html"), "r",
178
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-wholefoods.html"), "r",
173
179
  encoding="utf-8") as f:
174
180
  resp2 = responses.add(
175
181
  responses.GET,
@@ -182,9 +188,15 @@ class TestOrders(UnitTestCase):
182
188
  orders = self.amazon_orders.get_order_history(year=year, start_index=start_index)
183
189
 
184
190
  # THEN
185
- self.assertEqual(9, len(orders))
191
+ self.assertEqual(10, len(orders))
186
192
  self.assertEqual(1, resp1.call_count)
187
193
  self.assertEqual(1, resp2.call_count)
194
+ order = orders[7]
195
+ self.assertEqual("113-6307059-7336242", order.order_number)
196
+ self.assertEqual(62.92, order.grand_total)
197
+ self.assertIsNotNone(order.order_details_link)
198
+ self.assertEqual(date(2024, 12, 12), order.order_placed_date)
199
+ self.assertEqual(0, len(order.items))
188
200
 
189
201
  @responses.activate
190
202
  def test_get_order_history_full_details(self):
@@ -291,7 +303,7 @@ class TestOrders(UnitTestCase):
291
303
  # GIVEN
292
304
  self.amazon_session.is_authenticated = True
293
305
  order_id = "112-9685975-5907428"
294
- with open(os.path.join(self.RESOURCES_DIR, f"order-details-{order_id}.html"), "r",
306
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-details-{order_id}.html"), "r",
295
307
  encoding="utf-8") as f:
296
308
  resp1 = responses.add(
297
309
  responses.GET,
@@ -312,7 +324,7 @@ class TestOrders(UnitTestCase):
312
324
  # GIVEN
313
325
  self.amazon_session.is_authenticated = True
314
326
  order_id = "112-5939971-8962610"
315
- with open(os.path.join(self.RESOURCES_DIR, f"order-details-{order_id}.html"), "r",
327
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-details-{order_id}.html"), "r",
316
328
  encoding="utf-8") as f:
317
329
  resp1 = responses.add(
318
330
  responses.GET,
@@ -333,7 +345,7 @@ class TestOrders(UnitTestCase):
333
345
  # GIVEN
334
346
  self.amazon_session.is_authenticated = True
335
347
  order_id = "112-4482432-2955442"
336
- with open(os.path.join(self.RESOURCES_DIR, f"order-details-{order_id}.html"), "r",
348
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-details-{order_id}.html"), "r",
337
349
  encoding="utf-8") as f:
338
350
  resp1 = responses.add(
339
351
  responses.GET,
@@ -354,7 +366,7 @@ class TestOrders(UnitTestCase):
354
366
  # GIVEN
355
367
  self.amazon_session.is_authenticated = True
356
368
  order_id = "112-9087159-1657009"
357
- with open(os.path.join(self.RESOURCES_DIR, f"order-details-{order_id}.html"), "r",
369
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-details-{order_id}.html"), "r",
358
370
  encoding="utf-8") as f:
359
371
  resp1 = responses.add(
360
372
  responses.GET,
@@ -375,7 +387,7 @@ class TestOrders(UnitTestCase):
375
387
  # GIVEN
376
388
  self.amazon_session.is_authenticated = True
377
389
  order_id = "114-8722141-6545058"
378
- with open(os.path.join(self.RESOURCES_DIR, f"order-details-{order_id}.html"), "r",
390
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-details-{order_id}.html"), "r",
379
391
  encoding="utf-8") as f:
380
392
  resp1 = responses.add(
381
393
  responses.GET,
@@ -396,7 +408,7 @@ class TestOrders(UnitTestCase):
396
408
  # GIVEN
397
409
  self.amazon_session.is_authenticated = True
398
410
  order_id = "111-6778632-7354601"
399
- with open(os.path.join(self.RESOURCES_DIR, f"order-details-{order_id}.html"), "r",
411
+ with open(os.path.join(self.RESOURCES_DIR, "orders", f"order-details-{order_id}.html"), "r",
400
412
  encoding="utf-8") as f:
401
413
  resp1 = responses.add(
402
414
  responses.GET,
@@ -37,14 +37,15 @@ class TestSession(UnitTestCase):
37
37
  @responses.activate
38
38
  def test_login_invalid_username(self):
39
39
  # GIVEN
40
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
40
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
41
41
  resp1 = responses.add(
42
42
  responses.GET,
43
43
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
44
44
  body=f.read(),
45
45
  status=200,
46
46
  )
47
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-invalid-email.html"), "r", encoding="utf-8") as f:
47
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-invalid-email.html"), "r",
48
+ encoding="utf-8") as f:
48
49
  resp2 = responses.add(
49
50
  responses.POST,
50
51
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
@@ -64,14 +65,15 @@ class TestSession(UnitTestCase):
64
65
  @responses.activate
65
66
  def test_login_invalid_password(self):
66
67
  # GIVEN
67
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
68
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
68
69
  resp1 = responses.add(
69
70
  responses.GET,
70
71
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
71
72
  body=f.read(),
72
73
  status=200,
73
74
  )
74
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-invalid-password.html"), "r", encoding="utf-8") as f:
75
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-invalid-password.html"), "r",
76
+ encoding="utf-8") as f:
75
77
  resp2 = responses.add(
76
78
  responses.POST,
77
79
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
@@ -92,21 +94,21 @@ class TestSession(UnitTestCase):
92
94
  @patch("builtins.input")
93
95
  def test_mfa(self, input_mock):
94
96
  # GIVEN
95
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
97
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
96
98
  resp1 = responses.add(
97
99
  responses.GET,
98
100
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
99
101
  body=f.read(),
100
102
  status=200,
101
103
  )
102
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-mfa.html"), "r", encoding="utf-8") as f:
104
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-mfa.html"), "r", encoding="utf-8") as f:
103
105
  resp2 = responses.add(
104
106
  responses.POST,
105
107
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
106
108
  body=f.read(),
107
109
  status=200,
108
110
  )
109
- with open(os.path.join(self.RESOURCES_DIR, "order-history-2018-0.html"), "r", encoding="utf-8") as f:
111
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-2018-0.html"), "r", encoding="utf-8") as f:
110
112
  resp3 = responses.add(
111
113
  responses.POST,
112
114
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
@@ -128,28 +130,28 @@ class TestSession(UnitTestCase):
128
130
  @patch("builtins.input")
129
131
  def test_new_otp(self, input_mock):
130
132
  # GIVEN
131
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
133
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
132
134
  resp1 = responses.add(
133
135
  responses.GET,
134
136
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
135
137
  body=f.read(),
136
138
  status=200,
137
139
  )
138
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-new-otp.html"), "r", encoding="utf-8") as f:
140
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-new-otp.html"), "r", encoding="utf-8") as f:
139
141
  resp2 = responses.add(
140
142
  responses.POST,
141
143
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
142
144
  body=f.read(),
143
145
  status=200,
144
146
  )
145
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-mfa.html"), "r", encoding="utf-8") as f:
147
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-mfa.html"), "r", encoding="utf-8") as f:
146
148
  resp3 = responses.add(
147
149
  responses.POST,
148
150
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
149
151
  body=f.read(),
150
152
  status=200,
151
153
  )
152
- with open(os.path.join(self.RESOURCES_DIR, "order-history-2018-0.html"), "r", encoding="utf-8") as f:
154
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-2018-0.html"), "r", encoding="utf-8") as f:
153
155
  resp4 = responses.add(
154
156
  responses.POST,
155
157
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
@@ -171,7 +173,7 @@ class TestSession(UnitTestCase):
171
173
  @responses.activate
172
174
  def test_captcha_1(self):
173
175
  # GIVEN
174
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
176
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
175
177
  resp1 = responses.add(
176
178
  responses.GET,
177
179
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
@@ -184,14 +186,14 @@ class TestSession(UnitTestCase):
184
186
  status=302,
185
187
  headers={"Location": f"{self.test_config.constants.BASE_URL}/ap/cvf/request"}
186
188
  )
187
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-captcha-1.html"), "r", encoding="utf-8") as f:
189
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-captcha-1.html"), "r", encoding="utf-8") as f:
188
190
  resp3 = responses.add(
189
191
  responses.GET,
190
192
  f"{self.test_config.constants.BASE_URL}/ap/cvf/request",
191
193
  body=f.read(),
192
194
  status=200
193
195
  )
194
- with open(os.path.join(self.RESOURCES_DIR, "captcha_easy.jpg"), "rb") as f:
196
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "captcha_easy.jpg"), "rb") as f:
195
197
  resp4 = responses.add(
196
198
  responses.GET,
197
199
  "https://opfcaptcha-prod.s3.amazonaws.com/d32ff4fa043d4f969a1693adfb5d663a.jpg",
@@ -199,7 +201,7 @@ class TestSession(UnitTestCase):
199
201
  headers={"Content-Type": "image/jpeg"},
200
202
  status=200,
201
203
  )
202
- with open(os.path.join(self.RESOURCES_DIR, "order-history-2018-0.html"), "r", encoding="utf-8") as f:
204
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-2018-0.html"), "r", encoding="utf-8") as f:
203
205
  request_data = {
204
206
  "clientContext": "132-7968344-2156059",
205
207
  "cvf_captcha_captcha_action": "verifyCaptcha",
@@ -242,21 +244,21 @@ class TestSession(UnitTestCase):
242
244
  @responses.activate
243
245
  def test_captcha_2(self):
244
246
  # GIVEN
245
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
247
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
246
248
  resp1 = responses.add(
247
249
  responses.GET,
248
250
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
249
251
  body=f.read(),
250
252
  status=200,
251
253
  )
252
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-captcha-2.html"), "r", encoding="utf-8") as f:
254
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-captcha-2.html"), "r", encoding="utf-8") as f:
253
255
  resp2 = responses.add(
254
256
  responses.POST,
255
257
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
256
258
  body=f.read(),
257
259
  status=200,
258
260
  )
259
- with open(os.path.join(self.RESOURCES_DIR, "captcha_easy.jpg"), "rb") as f:
261
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "captcha_easy.jpg"), "rb") as f:
260
262
  resp3 = responses.add(
261
263
  responses.GET,
262
264
  "https://images-na.ssl-images-amazon.com/captcha/ddwwidnf/Captcha_gmwackhtzu.jpg",
@@ -264,7 +266,7 @@ class TestSession(UnitTestCase):
264
266
  headers={"Content-Type": "image/jpeg"},
265
267
  status=200,
266
268
  )
267
- with open(os.path.join(self.RESOURCES_DIR, "order-history-2018-0.html"), "r", encoding="utf-8") as f:
269
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-2018-0.html"), "r", encoding="utf-8") as f:
268
270
  resp4 = responses.add(
269
271
  responses.GET,
270
272
  f"{self.test_config.constants.BASE_URL}/errors/validateCaptcha",
@@ -293,7 +295,7 @@ class TestSession(UnitTestCase):
293
295
  @patch("PIL.Image.Image.show")
294
296
  def test_captcha_1_hard(self, show_mock, input_mock):
295
297
  # GIVEN
296
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
298
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
297
299
  resp1 = responses.add(
298
300
  responses.GET,
299
301
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
@@ -306,14 +308,14 @@ class TestSession(UnitTestCase):
306
308
  status=302,
307
309
  headers={"Location": f"{self.test_config.constants.BASE_URL}/ap/cvf/request"}
308
310
  )
309
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-captcha-1.html"), "r", encoding="utf-8") as f:
311
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-captcha-1.html"), "r", encoding="utf-8") as f:
310
312
  resp3 = responses.add(
311
313
  responses.GET,
312
314
  f"{self.test_config.constants.BASE_URL}/ap/cvf/request",
313
315
  body=f.read(),
314
316
  status=200
315
317
  )
316
- with open(os.path.join(self.RESOURCES_DIR, "captcha_hard.jpg"), "rb") as f:
318
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "captcha_hard.jpg"), "rb") as f:
317
319
  resp4 = responses.add(
318
320
  responses.GET,
319
321
  "https://opfcaptcha-prod.s3.amazonaws.com/d32ff4fa043d4f969a1693adfb5d663a.jpg",
@@ -321,7 +323,7 @@ class TestSession(UnitTestCase):
321
323
  headers={"Content-Type": "image/jpeg"},
322
324
  status=200,
323
325
  )
324
- with open(os.path.join(self.RESOURCES_DIR, "order-history-2018-0.html"), "r", encoding="utf-8") as f:
326
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-2018-0.html"), "r", encoding="utf-8") as f:
325
327
  resp5 = responses.add(
326
328
  responses.POST,
327
329
  f"{self.test_config.constants.BASE_URL}/ap/cvf/verify",
@@ -345,21 +347,23 @@ class TestSession(UnitTestCase):
345
347
  @patch("builtins.input")
346
348
  def test_captcha_otp(self, input_mock):
347
349
  # GIVEN
348
- with open(os.path.join(self.RESOURCES_DIR, "signin.html"), "r", encoding="utf-8") as f:
350
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "signin.html"), "r", encoding="utf-8") as f:
349
351
  resp1 = responses.add(
350
352
  responses.GET,
351
353
  f"{self.test_config.constants.BASE_URL}/gp/sign-in.html",
352
354
  body=f.read(),
353
355
  status=200,
354
356
  )
355
- with open(os.path.join(self.RESOURCES_DIR, "post-signin-captcha-otp.html"), "r", encoding="utf-8") as f:
357
+ with open(os.path.join(self.RESOURCES_DIR, "auth", "post-signin-captcha-otp.html"),
358
+ "r", encoding="utf-8") as f:
356
359
  resp2 = responses.add(
357
360
  responses.POST,
358
361
  self.test_config.constants.SIGN_IN_REDIRECT_URL,
359
362
  body=f.read(),
360
363
  status=200,
361
364
  )
362
- with open(os.path.join(self.RESOURCES_DIR, "order-history-2018-0.html"), "r", encoding="utf-8") as f:
365
+ with open(os.path.join(self.RESOURCES_DIR, "orders", "order-history-2018-0.html"),
366
+ "r", encoding="utf-8") as f:
363
367
  resp3 = responses.add(
364
368
  responses.POST,
365
369
  f"{self.test_config.constants.BASE_URL}/ap/cvf/approval/verifyOtp",
@@ -0,0 +1,131 @@
1
+ __copyright__ = "Copyright (c) 2024 Alex Laird"
2
+ __license__ = "MIT"
3
+
4
+ import datetime
5
+ import os
6
+ from unittest.mock import Mock, patch
7
+
8
+ import responses
9
+ from bs4 import BeautifulSoup
10
+
11
+ from amazonorders.session import AmazonSession
12
+ from amazonorders.transactions import AmazonTransactions, _parse_transaction_form_tag
13
+ from tests.unittestcase import UnitTestCase
14
+
15
+
16
+ class TestOrders(UnitTestCase):
17
+ temp_order_history_file_path = os.path.join(
18
+ os.path.abspath(os.path.dirname(__file__)), "output", "temp-order-history.html"
19
+ )
20
+ temp_order_details_file_path = os.path.join(
21
+ os.path.abspath(os.path.dirname(__file__)), "output", "temp-order-details.html"
22
+ )
23
+
24
+ def setUp(self):
25
+ super().setUp()
26
+
27
+ self.amazon_session = AmazonSession(
28
+ "some-username", "some-password", config=self.test_config
29
+ )
30
+
31
+ self.amazon_transactions = AmazonTransactions(self.amazon_session)
32
+
33
+ @responses.activate
34
+ @patch("amazonorders.transactions.datetime", wraps=datetime)
35
+ def test_get_transactions(self, mock_get_today: Mock):
36
+ # GIVEN
37
+ mock_get_today.date.today.return_value = datetime.date(2024, 10, 11)
38
+ days = 1
39
+ self.amazon_session.is_authenticated = True
40
+ with open(os.path.join(self.RESOURCES_DIR, "transactions", "get-transactions-snippet.html"),
41
+ "r",
42
+ encoding="utf-8") as f:
43
+ responses.add(
44
+ responses.GET,
45
+ f"{self.test_config.constants.TRANSACTION_HISTORY_LANDING_URL}",
46
+ body=f.read(),
47
+ status=200,
48
+ )
49
+
50
+ # WHEN
51
+ transactions = self.amazon_transactions.get_transactions(days=days)
52
+
53
+ # THEN
54
+ self.assertEqual(1, len(transactions))
55
+ transaction = transactions[0]
56
+ self.assertEqual(transaction.completed_date, datetime.date(2024, 10, 11))
57
+ self.assertEqual(transaction.payment_method, "Visa ****1234")
58
+ self.assertEqual(transaction.grand_total, -45.19)
59
+ self.assertFalse(transaction.is_refund)
60
+ self.assertEqual(transaction.order_number, "123-4567890-1234567")
61
+ self.assertEqual(transaction.order_details_link,
62
+ "https://www.amazon.ca/gp/css/summary/edit.html?orderID=123-4567890-1234567")
63
+ self.assertEqual(transaction.seller, "AMZN Mktp CA")
64
+
65
+ @responses.activate
66
+ @patch("amazonorders.transactions.datetime", wraps=datetime)
67
+ def test_get_transactions_with_pending(self, mock_get_today: Mock):
68
+ # GIVEN
69
+ mock_get_today.date.today.return_value = datetime.date(2025, 2, 13)
70
+ days = 30
71
+ self.amazon_session.is_authenticated = True
72
+ with open(os.path.join(self.RESOURCES_DIR, "transactions", "transactions-in-progress.html"),
73
+ "r",
74
+ encoding="utf-8") as f:
75
+ responses.add(
76
+ responses.GET,
77
+ f"{self.test_config.constants.TRANSACTION_HISTORY_LANDING_URL}",
78
+ body=f.read(),
79
+ status=200,
80
+ )
81
+
82
+ # WHEN
83
+ transactions = self.amazon_transactions.get_transactions(days=days)
84
+
85
+ # THEN
86
+ self.assertEqual(20, len(transactions))
87
+ transaction = transactions[0]
88
+ self.assertEqual(transaction.completed_date, datetime.date(2025, 2, 12))
89
+ self.assertEqual(transaction.payment_method, "Prime Visa ****1111")
90
+ self.assertEqual(transaction.grand_total, -26.29)
91
+ self.assertFalse(transaction.is_refund)
92
+ self.assertEqual(transaction.order_number, "234-8832881-7100260")
93
+ self.assertEqual(transaction.order_details_link,
94
+ "https://www.amazon.com/gp/your-account/order-details?orderID=234-8832881-7100260")
95
+ self.assertEqual(transaction.seller, None)
96
+ transaction = transactions[1]
97
+ self.assertEqual(transaction.completed_date, datetime.date(2025, 2, 7))
98
+ self.assertEqual(transaction.payment_method, "Prime Visa ****1111")
99
+ self.assertEqual(transaction.grand_total, 43.94)
100
+ self.assertTrue(transaction.is_refund)
101
+ self.assertEqual(transaction.order_number, "234-3017692-4601031")
102
+ self.assertEqual(transaction.order_details_link,
103
+ "https://www.amazon.com/gp/css/summary/edit.html?orderID=234-3017692-4601031")
104
+ self.assertEqual(transaction.seller, "AMZN Mktp US")
105
+
106
+ def test_parse_transaction_form_tag(self):
107
+ # GIVEN
108
+ with open(os.path.join(self.RESOURCES_DIR, "transactions", "transaction-form-tag.html"),
109
+ "r",
110
+ encoding="utf-8") as f:
111
+ parsed = BeautifulSoup(f.read(), self.test_config.bs4_parser)
112
+ form_tag = parsed.select_one("form")
113
+
114
+ # WHEN
115
+ transactions, next_page_url, next_page_data = _parse_transaction_form_tag(
116
+ form_tag, self.test_config
117
+ )
118
+
119
+ # THEN
120
+ self.assertEqual(len(transactions), 2)
121
+ self.assertEqual(
122
+ next_page_url, "https://www.amazon.com:443/cpe/yourpayments/transactions"
123
+ )
124
+ self.assertEqual(
125
+ next_page_data,
126
+ {
127
+ "ppw-widgetState": "the-ppw-widgetState",
128
+ "ie": "UTF-8",
129
+ 'ppw-widgetEvent:DefaultNextPageNavigationEvent:{"nextPageKey":"key"}': "",
130
+ },
131
+ )
@@ -1,4 +1,4 @@
1
- __copyright__ = "Copyright (c) 2024 Jeff Sawatzky"
1
+ __copyright__ = "Copyright (c) 2024 Alex Laird"
2
2
  __license__ = "MIT"
3
3
 
4
4
  from amazonorders.util import to_type
@@ -1,166 +0,0 @@
1
- __copyright__ = "Copyright (c) 2024 Jeff Sawatzky"
2
- __license__ = "MIT"
3
-
4
- import datetime
5
- import os
6
- from unittest.mock import Mock, patch
7
-
8
- import responses
9
- from bs4 import BeautifulSoup
10
-
11
- from amazonorders.session import AmazonSession
12
- from amazonorders.transactions import AmazonTransactions, _parse_transaction_form_tag
13
- from tests.unittestcase import UnitTestCase
14
-
15
-
16
- class TestOrders(UnitTestCase):
17
- temp_order_history_file_path = os.path.join(
18
- os.path.abspath(os.path.dirname(__file__)), "output", "temp-order-history.html"
19
- )
20
- temp_order_details_file_path = os.path.join(
21
- os.path.abspath(os.path.dirname(__file__)), "output", "temp-order-details.html"
22
- )
23
-
24
- def setUp(self):
25
- super().setUp()
26
-
27
- self.amazon_session = AmazonSession(
28
- "some-username", "some-password", config=self.test_config
29
- )
30
-
31
- self.amazon_transactions = AmazonTransactions(self.amazon_session)
32
-
33
- @responses.activate
34
- @patch("amazonorders.transactions.datetime", wraps=datetime)
35
- def test_transactions_command(self, mock_get_today: Mock):
36
- # GIVEN
37
- mock_get_today.date.today.return_value = datetime.date(2024, 10, 11)
38
- days = 1
39
- self.amazon_session.is_authenticated = True
40
- with open(
41
- os.path.join(self.RESOURCES_DIR, "get-transactions.html"),
42
- "r",
43
- encoding="utf-8",
44
- ) as f:
45
- responses.add(
46
- responses.GET,
47
- f"{self.test_config.constants.TRANSACTION_HISTORY_LANDING_URL}",
48
- body=f.read(),
49
- status=200,
50
- )
51
-
52
- # WHEN
53
- transactions = self.amazon_transactions.get_transactions(days=days)
54
-
55
- # THEN
56
- self.assertEqual(1, len(transactions))
57
-
58
- def test_parse_transaction_form_tag(self):
59
- # GIVEN
60
- parsed = BeautifulSoup(TEST_PARSE_TRANSACTION_FORM_TAG_HTML, self.test_config.bs4_parser)
61
- form_tag = parsed.select_one("form")
62
-
63
- # WHEN
64
- transactions, next_page_url, next_page_data = _parse_transaction_form_tag(
65
- form_tag, self.test_config
66
- )
67
-
68
- # THEN
69
- self.assertEqual(len(transactions), 2)
70
- self.assertEqual(
71
- next_page_url, "https://www.amazon.com:443/cpe/yourpayments/transactions"
72
- )
73
- self.assertEqual(
74
- next_page_data,
75
- {
76
- "ppw-widgetState": "the-ppw-widgetState",
77
- "ie": "UTF-8",
78
- 'ppw-widgetEvent:DefaultNextPageNavigationEvent:{"nextPageKey":"key"}': "",
79
- },
80
- )
81
-
82
-
83
- TEST_PARSE_TRANSACTION_FORM_TAG_HTML = """
84
- <form action="https://www.amazon.com:443/cpe/yourpayments/transactions" class="a-spacing-none" method="post"><input
85
- name="ppw-widgetState" type="hidden" value="the-ppw-widgetState" /><input name="ie" type="hidden"
86
- value="UTF-8" />
87
- <div class="a-box-group a-spacing-base">
88
- <div class="a-box a-spacing-none a-box-title apx-transactions-sleeve-header-container">
89
- <div class="a-box-inner a-padding-base"><span class="a-size-base a-text-bold">Completed</span></div>
90
- </div>
91
- <div class="a-box a-spacing-base">
92
- <div class="a-box-inner a-padding-none">
93
- <div class="a-section a-spacing-base a-padding-base apx-transaction-date-container pmts-portal-component pmts-portal-components-pp-kXMaEm-3"
94
- data-pmts-component-id="pp-kXMaEm-3"><span>October 11, 2024</span></div>
95
- <div class="a-section a-spacing-base pmts-portal-component pmts-portal-components-pp-kXMaEm-3"
96
- data-pmts-component-id="pp-kXMaEm-3">
97
- <div class="a-section a-spacing-base apx-transactions-line-item-component-container">
98
- <div class="a-row pmts-portal-component pmts-portal-components-pp-kXMaEm-4"
99
- data-pmts-component-id="pp-kXMaEm-4">
100
- <div class="a-column a-span9"><span class="a-size-base a-text-bold">Visa ****1234</span>
101
- </div>
102
- <div class="a-column a-span3 a-text-right a-span-last"><span
103
- class="a-size-base-plus a-text-bold">-CA$45.19</span></div>
104
- </div>
105
- <div class="a-section a-spacing-none a-spacing-top-mini pmts-portal-component pmts-portal-components-pp-kXMaEm-4"
106
- data-pmts-component-id="pp-kXMaEm-4">
107
- <div class="a-row">
108
- <div class="a-column a-span12"><a class="a-link-normal"
109
- href="https://www.amazon.ca/gp/css/summary/edit.html?orderID=123-4567890-1234567"
110
- id="pp-kXMaEm-50">Order #123-4567890-1234567</a></div>
111
- </div>
112
- </div>
113
- <div class="a-section a-spacing-none a-spacing-top-mini pmts-portal-component pmts-portal-components-pp-kXMaEm-4"
114
- data-pmts-component-id="pp-kXMaEm-4">
115
- <div class="a-row">
116
- <div class="a-column a-span12"><span class="a-size-base">AMZN Mktp CA</span></div>
117
- </div>
118
- </div>
119
- </div>
120
- </div>
121
- <div class="a-section a-spacing-base a-padding-base apx-transaction-date-container pmts-portal-component pmts-portal-components-pp-kXMaEm-8"
122
- data-pmts-component-id="pp-kXMaEm-8"><span>October 9, 2024</span></div>
123
- <div class="a-section a-spacing-base pmts-portal-component pmts-portal-components-pp-kXMaEm-8"
124
- data-pmts-component-id="pp-kXMaEm-8">
125
- <div class="a-section a-spacing-base apx-transactions-line-item-component-container">
126
- <div class="a-row pmts-portal-component pmts-portal-components-pp-kXMaEm-9"
127
- data-pmts-component-id="pp-kXMaEm-9">
128
- <div class="a-column a-span9"><span class="a-size-base a-text-bold">Mastercard
129
- ****1234</span></div>
130
- <div class="a-column a-span3 a-text-right a-span-last"><span
131
- class="a-size-base-plus a-text-bold">-CA$28.79</span></div>
132
- </div>
133
- <div class="a-section a-spacing-none a-spacing-top-mini pmts-portal-component pmts-portal-components-pp-kXMaEm-9"
134
- data-pmts-component-id="pp-kXMaEm-9">
135
- <div class="a-row">
136
- <div class="a-column a-span12"><a class="a-link-normal"
137
- href="https://www.amazon.ca/gp/css/summary/edit.html?orderID=123-4567890-1234567"
138
- id="pp-kXMaEm-52">Order #123-4567890-1234567</a></div>
139
- </div>
140
- </div>
141
- <div class="a-section a-spacing-none a-spacing-top-mini pmts-portal-component pmts-portal-components-pp-kXMaEm-9"
142
- data-pmts-component-id="pp-kXMaEm-9">
143
- <div class="a-row">
144
- <div class="a-column a-span12"><span class="a-size-base">Amazon.ca</span></div>
145
- </div>
146
- </div>
147
- </div>
148
- </div>
149
- </div>
150
- </div>
151
- </div>
152
- <div class="a-row a-spacing-top-extra-large">
153
- <div class="a-column a-span2 a-text-center"><span class="a-button a-button-span12 a-button-base"><span
154
- class="a-button-inner"><input class="a-button-input"
155
- name='ppw-widgetEvent:DefaultPreviousPageNavigationEvent:{"previousPageKey":"key"}'
156
- type="submit" /><span aria-hidden="true" class="a-button-text">Previous
157
- Page</span></span></span></div>
158
- <div class="a-column a-span2 a-text-center a-span-last"><span
159
- class="a-button a-button-span12 a-button-base"><span class="a-button-inner"><input
160
- class="a-button-input"
161
- name='ppw-widgetEvent:DefaultNextPageNavigationEvent:{"nextPageKey":"key"}'
162
- type="submit" /><span aria-hidden="true" class="a-button-text">Next Page</span></span></span>
163
- </div>
164
- </div>
165
- </form>
166
- """ # noqa
File without changes
File without changes
File without changes
File without changes