Skip to content

Commit

Permalink
fix: address code review comments
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffsawatzky committed Oct 29, 2024
1 parent aedf363 commit 7dde210
Show file tree
Hide file tree
Showing 11 changed files with 230 additions and 141 deletions.
3 changes: 2 additions & 1 deletion amazonorders/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from amazonorders.exception import AmazonOrdersError
from amazonorders.orders import AmazonOrders
from amazonorders.session import AmazonSession, IODefault
from amazonorders.transactions import AmazonTransactions

logger = logging.getLogger("amazonorders")

Expand Down Expand Up @@ -205,7 +206,7 @@ def transactions(ctx: Context, **kwargs: Any):
)
click.echo("Info: Fetching transaction history, this might take a minute ...")

amazon_transactions = AmazonOrders(amazon_session, config=ctx.obj["conf"])
amazon_transactions = AmazonTransactions(amazon_session, config=ctx.obj["conf"])

transactions = amazon_transactions.get_transactions(days=days)

Expand Down
26 changes: 23 additions & 3 deletions amazonorders/entity/transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ class Transaction(Parsable):
An Amazon Transaction
"""

def __init__(self, parsed: Tag, config: AmazonOrdersConfig, completed_date: date) -> None:
def __init__(
self, parsed: Tag, config: AmazonOrdersConfig, completed_date: date
) -> None:
super().__init__(parsed, config)

#: The Transaction completed date.
Expand All @@ -33,6 +35,8 @@ def __init__(self, parsed: Tag, config: AmazonOrdersConfig, completed_date: date
self.is_refund: bool = self.grand_total > 0
#: The Transaction order number.
self.order_number: str = self.safe_parse(self._parse_order_number)
#: The Transaction order details link.
self.order_details_link: str = self.safe_parse(self._parse_order_details_link)
#: The Transaction seller name.
self.seller: str = self.safe_simple_parse(
selector=self.config.selectors.FIELD_TRANSACTION_SELLER_NAME_SELECTOR
Expand All @@ -45,14 +49,30 @@ def __str__(self) -> str: # pragma: no cover
return f"Transaction {self.completed_date}: Order #{self.order_number}, Grand Total {self.grand_total}"

def _parse_grand_total(self) -> float:
value = self.simple_parse(self.config.selectors.FIELD_TRANSACTION_GRAND_TOTAL_SELECTOR)
value = self.simple_parse(
self.config.selectors.FIELD_TRANSACTION_GRAND_TOTAL_SELECTOR
)
value = self.to_currency(value)

return value

def _parse_order_number(self) -> str:
value = self.simple_parse(self.config.selectors.FIELD_TRANSACTION_ORDER_NUMBER_SELECTOR)
value = self.simple_parse(
self.config.selectors.FIELD_TRANSACTION_ORDER_NUMBER_SELECTOR
)
match = re.match(".*#([0-9-]+)$", value)
value = match.group(1) if match else ""

return value

def _parse_order_details_link(self) -> str:
value = self.simple_parse(
self.config.selectors.FIELD_TRANSACTION_ORDER_LINK_SELECTOR, link=True
)

if not value and self.order_number:
value = (

Check warning on line 74 in amazonorders/entity/transaction.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/entity/transaction.py#L74

Added line #L74 was not covered by tests
f"{self.config.constants.ORDER_DETAILS_URL}?orderID={self.order_number}"
)

return value
Empty file removed amazonorders/lib/__init__.py
Empty file.
62 changes: 0 additions & 62 deletions amazonorders/lib/transactions.py

This file was deleted.

47 changes: 0 additions & 47 deletions amazonorders/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@
from amazonorders import util
from amazonorders.conf import AmazonOrdersConfig
from amazonorders.entity.order import Order
from amazonorders.entity.transaction import Transaction
from amazonorders.exception import AmazonOrdersError, AmazonOrdersNotFoundError
from amazonorders.lib.transactions import parse_transaction_form_tag
from amazonorders.session import AmazonSession

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -125,48 +123,3 @@ def get_order(self,
order: Order = self.config.order_cls(order_details_tag, self.config, full_details=True)

return order

def get_transactions(
self,
days: int = 365,
) -> List[Transaction]:
"""
Get the Amazon transactions for the given number of days.
:param days: The number of days worth of transactions to get.
:return: A list of the requested Transactions.
"""
if not self.amazon_session.is_authenticated:
raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")

min_date = datetime.date.today() - datetime.timedelta(days=days)

self.amazon_session.get(self.config.constants.TRANSACTION_HISTORY_LANDING_URL)
assert self.amazon_session.last_response_parsed

form_tag = self.amazon_session.last_response_parsed.select_one(
self.config.selectors.TRANSACTION_HISTORY_FORM_SELECTOR
)

transactions: List[Transaction] = []
while form_tag:
loaded_transactions, next_page_post_url, next_page_post_data = (
parse_transaction_form_tag(form_tag, self.config)
)
for transaction in loaded_transactions:
if transaction.completed_date >= min_date:
transactions.append(transaction)
else:
return transactions

if next_page_post_url is None:
return transactions

self.amazon_session.post(next_page_post_url, data=next_page_post_data)
assert self.amazon_session.last_response_parsed

form_tag = self.amazon_session.last_response_parsed.select_one(
self.config.selectors.TRANSACTION_HISTORY_FORM_SELECTOR
)

return transactions
7 changes: 4 additions & 3 deletions amazonorders/selectors.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
__copyright__ = "Copyright (c) 2024 Alex Laird"
__license__ = "MIT"

from amazonorders.constants import Constants


class Selectors:
##########################################################################
Expand Down Expand Up @@ -102,7 +100,7 @@ class Selectors:
# CSS selectors for Transaction fields
#####################################

TRANSACTION_HISTORY_FORM_SELECTOR = f"form[method='post'][action$='{Constants.TRANSACTION_HISTORY_LANDING_ROUTE}']"
TRANSACTION_HISTORY_FORM_SELECTOR = "form:has(input[name='ppw-widgetState'])"
TRANSACTION_DATE_CONTAINERS_SELECTOR = "div.apx-transaction-date-container"
TRANSACTIONS_CONTAINER_SELECTOR = "div"
TRANSACTIONS_SELECTOR = "div.apx-transactions-line-item-component-container"
Expand All @@ -123,6 +121,9 @@ class Selectors:
FIELD_TRANSACTION_ORDER_NUMBER_SELECTOR = (
"div.apx-transactions-line-item-component-container > div:nth-child(2) a.a-link-normal"
)
FIELD_TRANSACTION_ORDER_LINK_SELECTOR = (
"div.apx-transactions-line-item-component-container > div:nth-child(2) a.a-link-normal"
)
FIELD_TRANSACTION_SELLER_NAME_SELECTOR = (
"div.apx-transactions-line-item-component-container > div:nth-child(3) span.a-size-base"
)
148 changes: 148 additions & 0 deletions amazonorders/transactions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
__copyright__ = "Copyright (c) 2024 Jeff Sawatzky"
__license__ = "MIT"

import datetime
import logging
from typing import Dict, List, Optional, Tuple

from bs4 import Tag

from amazonorders.conf import AmazonOrdersConfig
from amazonorders.entity.transaction import Transaction
from amazonorders.exception import AmazonOrdersError
from amazonorders.session import AmazonSession

logger = logging.getLogger(__name__)


def _parse_transaction_form_tag(
form_tag: Tag, config: AmazonOrdersConfig
) -> Tuple[List[Transaction], Optional[str], Optional[Dict[str, str]]]:
transactions = []
date_container_tags = form_tag.select(
config.selectors.TRANSACTION_DATE_CONTAINERS_SELECTOR
)
for date_container_tag in date_container_tags:
date_tag = date_container_tag.select_one(
config.selectors.FIELD_TRANSACTION_COMPLETED_DATE_SELECTOR
)
if not date_tag:
logger.warning("Could not find date tag in transaction form.")
continue

Check warning on line 31 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L30-L31

Added lines #L30 - L31 were not covered by tests

date_str = date_tag.text
date = datetime.datetime.strptime(
date_str, config.constants.TRANSACTION_DATE_FORMAT
).date()

transactions_container_tag = date_container_tag.find_next_sibling(
config.selectors.TRANSACTIONS_CONTAINER_SELECTOR
)
if not isinstance(transactions_container_tag, Tag):
logger.warning(

Check warning on line 42 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L42

Added line #L42 was not covered by tests
"Could not find transactions container tag in transaction form."
)
continue

Check warning on line 45 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L45

Added line #L45 was not covered by tests

transaction_tags = transactions_container_tag.select(
config.selectors.TRANSACTIONS_SELECTOR
)
for transaction_tag in transaction_tags:
transaction = Transaction(transaction_tag, config, date)
transactions.append(transaction)

form_state_input = form_tag.select_one(
config.selectors.TRANSACTIONS_NEXT_PAGE_INPUT_STATE_SELECTOR
)
form_ie_input = form_tag.select_one(
config.selectors.TRANSACTIONS_NEXT_PAGE_INPUT_IE_SELECTOR
)
next_page_input = form_tag.select_one(
config.selectors.TRANSACTIONS_NEXT_PAGE_INPUT_SELECTOR
)
if not next_page_input or not form_state_input or not form_ie_input:
return (transactions, None, None)

next_page_post_url = str(form_tag["action"])
next_page_post_data = {
"ppw-widgetState": str(form_state_input["value"]),
"ie": str(form_ie_input["value"]),
str(next_page_input["name"]): "",
}

return (transactions, next_page_post_url, next_page_post_data)


class AmazonTransactions:
"""
Using an authenticated :class:`~amazonorders.session.AmazonSession`, can be used to query Amazon
for Transaction details and history.
"""

def __init__(
self,
amazon_session: AmazonSession,
debug: Optional[bool] = None,
config: Optional[AmazonOrdersConfig] = None,
) -> None:
if not debug:
debug = amazon_session.debug
if not config:
config = amazon_session.config

#: The AmazonSession to use for requests.
self.amazon_session: AmazonSession = amazon_session
#: The AmazonOrdersConfig to use.
self.config: AmazonOrdersConfig = config

#: Set logger ``DEBUG`` and send output to ``stderr``.
self.debug: bool = debug
if self.debug:
logger.setLevel(logging.DEBUG)

def get_transactions(
self,
days: int = 365,
) -> List[Transaction]:
"""
Get the Amazon transactions for the given number of days.
:param days: The number of days worth of transactions to get.
:return: A list of the requested Transactions.
"""
if not self.amazon_session.is_authenticated:
raise AmazonOrdersError("Call AmazonSession.login() to authenticate first.")

Check warning on line 114 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L114

Added line #L114 was not covered by tests

min_date = datetime.date.today() - datetime.timedelta(days=days)

self.amazon_session.get(self.config.constants.TRANSACTION_HISTORY_LANDING_URL)
if not self.amazon_session.last_response_parsed:
raise AmazonOrdersError("Could not get transaction history landing page.")

Check warning on line 120 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L120

Added line #L120 was not covered by tests

form_tag = self.amazon_session.last_response_parsed.select_one(
self.config.selectors.TRANSACTION_HISTORY_FORM_SELECTOR
)

transactions: List[Transaction] = []
while form_tag:
loaded_transactions, next_page_post_url, next_page_post_data = (
_parse_transaction_form_tag(form_tag, self.config)
)
for transaction in loaded_transactions:
if transaction.completed_date >= min_date:
transactions.append(transaction)
else:
return transactions

if next_page_post_url is None:
return transactions

Check warning on line 138 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L137-L138

Added lines #L137 - L138 were not covered by tests

self.amazon_session.post(next_page_post_url, data=next_page_post_data)
if not self.amazon_session.last_response_parsed:
raise AmazonOrdersError("Could not get next transaction history page.")

Check warning on line 142 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L140-L142

Added lines #L140 - L142 were not covered by tests

form_tag = self.amazon_session.last_response_parsed.select_one(

Check warning on line 144 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L144

Added line #L144 was not covered by tests
self.config.selectors.TRANSACTION_HISTORY_FORM_SELECTOR
)

return transactions

Check warning on line 148 in amazonorders/transactions.py

View check run for this annotation

Codecov / codecov/patch

amazonorders/transactions.py#L148

Added line #L148 was not covered by tests
2 changes: 2 additions & 0 deletions tests/entity/test_transaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ def test_parse(self):
self.assertEqual(transaction.completed_date, date(2024, 1, 1))
self.assertEqual(transaction.payment_method, "My Payment Method")
self.assertEqual(transaction.order_number, "123-4567890-1234567")
self.assertEqual(transaction.order_details_link, "https://www.amazon.com/gp/css/summary/edit.html?orderID=123-4567890-1234567") # noqa
self.assertEqual(transaction.seller, "AMZN Mktp COM")
self.assertEqual(transaction.grand_total, -12.34)
self.assertEqual(transaction.is_refund, False)
Expand Down Expand Up @@ -94,6 +95,7 @@ def test_parse_refund(self):
self.assertEqual(transaction.completed_date, date(2024, 1, 1))
self.assertEqual(transaction.payment_method, "My Payment Method")
self.assertEqual(transaction.order_number, "123-4567890-1234567")
self.assertEqual(transaction.order_details_link, "https://www.amazon.com/gp/css/summary/edit.html?orderID=123-4567890-1234567") # noqa
self.assertEqual(transaction.seller, "AMZN Mktp COM")
self.assertEqual(transaction.grand_total, 12.34)
self.assertEqual(transaction.is_refund, True)
Expand Down
Empty file removed tests/lib/__init__.py
Empty file.
Loading

0 comments on commit 7dde210

Please sign in to comment.