Skip to content

Commit 2e6f40d

Browse files
Merge branch 'DanielKarp-main'
Incorporate changes from upstream to get caught up
2 parents 6505491 + a2ba41e commit 2e6f40d

File tree

9 files changed

+331
-198
lines changed

9 files changed

+331
-198
lines changed

.pre-commit-config.yaml

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
repos:
2-
- repo: https://github.com/pre-commit/pre-commit-hooks
3-
rev: v5.0.0
2+
- repo: https://github.com/pre-commit/pre-commit-hooks
3+
rev: v6.0.0
44
hooks:
5-
- id: trailing-whitespace
6-
- id: end-of-file-fixer
7-
- id: check-yaml
8-
- id: check-added-large-files
9-
- repo: https://github.com/astral-sh/ruff-pre-commit
10-
rev: v0.11.4
5+
- id: trailing-whitespace
6+
- id: end-of-file-fixer
7+
- id: check-yaml
8+
- id: check-added-large-files
9+
- repo: https://github.com/astral-sh/ruff-pre-commit
10+
rev: v0.12.12
1111
hooks:
12-
- id: ruff
12+
- id: ruff-check
1313
args: [--fix]
14-
- id: ruff-format
15-
- repo: https://github.com/astral-sh/uv-pre-commit
16-
rev: 0.6.13
14+
- id: ruff-format
15+
- repo: https://github.com/astral-sh/uv-pre-commit
16+
rev: 0.8.15
1717
hooks:
18-
- id: uv-lock
19-
- id: uv-export
18+
- id: uv-lock
19+
- id: uv-export
2020
args: [--quiet]
21-
- repo: https://github.com/fpgmaas/deptry.git
22-
rev: 0.23.0
21+
- repo: https://github.com/fpgmaas/deptry.git
22+
rev: 0.23.1
2323
hooks:
24-
- id: deptry
24+
- id: deptry
2525
args: [src]

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
:bangbang: : Development has moved to [WoosterTech fork](https://github.com/WoosterTech/YNAmazon/). This repository is no longer maintained.
2+
13
# YNAmazon
24
A program to annotate YNAB transactions with Amazon order info
35

pyproject.toml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,12 @@ dependencies = [
1616
"pydantic-core>=2.33.1",
1717
"furl>=2.1.4",
1818
"requests>=2.30.0",
19+
"cache-decorator>=2.2.0",
1920
]
2021

2122
[project.optional-dependencies]
2223
ai = [
23-
"openai>=1.12.0", # OpenAI Python client for AI summarization
24+
"openai>=1.12.0", # OpenAI Python client for AI summarization
2425
]
2526

2627
[project.urls]
@@ -59,8 +60,8 @@ build-backend = "hatchling.build"
5960

6061

6162
[[tool.mypy.overrides]]
62-
module= ["amazonorders.*"]
63+
module = ["amazonorders.*"]
6364
ignore_missing_imports = true
6465

65-
# [tool.deptry]
66-
# known_first_party = ["ynamazon"]
66+
[tool.deptry]
67+
known_first_party = ["ynamazon"]

src/ynamazon/amazon_transactions.py

Lines changed: 130 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
# pyright: reportDeprecated=false
2+
import os
3+
import tempfile
24
from datetime import date
35
from decimal import Decimal
46
from typing import Annotated, Union # , Self # not available python <3.11
57

68
from amazonorders.entity.order import Order
79
from amazonorders.entity.transaction import Transaction
8-
from amazonorders.exception import AmazonOrdersAuthError
910
from amazonorders.orders import AmazonOrders
1011
from amazonorders.session import AmazonSession
1112
from amazonorders.transactions import AmazonTransactions
13+
from cache_decorator import Cache
1214
from loguru import logger
1315
from pydantic import AnyUrl, BaseModel, EmailStr, Field, SecretStr, field_validator
1416
from rich import print as rprint
@@ -75,93 +77,142 @@ def amazon_session(self) -> AmazonSession:
7577
)
7678

7779

78-
def get_amazon_transactions(
79-
order_years: list[int] | None = None,
80-
transaction_days: int = 31,
81-
configuration: AmazonConfig | None = None,
82-
) -> list[AmazonTransactionWithOrderInfo]:
83-
"""Returns a list of transactions with order info.
80+
class AmazonTransactionRetriever:
81+
def __init__(
82+
self,
83+
amazon_config: AmazonConfig,
84+
order_years: list[str] | None = None,
85+
transaction_days: int = 31,
86+
force_refresh_amazon: bool = False,
87+
):
88+
"""Initialize an AmazonTransactionRetriever.
8489
85-
Args:
90+
amazon_config (AmazonConfig): Configuration for Amazon, primarily credentials
8691
order_years (list[int] | None): A list of years to fetch transactions for. `None` for the current year.
87-
transaction_days (int): Number of days to fetch transactions for.
88-
configuration (AmazonConfig | None): Amazon configuration.
92+
transaction_days (int): Number of days to fetch transactions for. Defaults to 31.
93+
force_refresh_amazon (bool): Refresh cache by fetching transactions directly from Amazon.
94+
"""
95+
self.amazon_config = amazon_config
96+
self.order_years = self.__class__._normalized_years(order_years)
97+
self.transaction_days = transaction_days
98+
self.force_refresh_amazon = force_refresh_amazon
99+
100+
# for memoizing the results of method calls
101+
self._memo = {}
102+
103+
def get_amazon_transactions(self) -> list[AmazonTransactionWithOrderInfo]:
104+
"""Get Amazon transactions linked to orders.
105+
106+
This method exists as a layer to force caching to work one level below with all relevant parameters considered
107+
108+
Returns:
109+
list[TransactionWithOrderInfo]: A list of transactions with order info
110+
"""
111+
return self._get_amazon_transactions(
112+
order_years=self.order_years,
113+
transaction_days=self.transaction_days,
114+
amazon_config=self.amazon_config,
115+
use_cache=not self.force_refresh_amazon,
116+
)
89117

90-
Returns:
91-
list[TransactionWithOrderInfo]: A list of transactions with order info
92-
"""
93-
if configuration is None:
94-
configuration = AmazonConfig()
95-
amazon_session = configuration.amazon_session()
96-
try:
97-
logger.debug("Logging in to Amazon session...")
98-
logger.debug(f"Session debug mode: {amazon_session.debug}") # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
99-
amazon_session.login()
100-
except AmazonOrdersAuthError as e:
101-
logger.error(f"Failed to authenticate Amazon session: {e}")
102-
raise
118+
@Cache(
119+
validity_duration="2h",
120+
enable_cache_arg_name="use_cache",
121+
cache_path=os.path.join(
122+
tempfile.gettempdir(),
123+
"ynamazon",
124+
"amazon_transactions_get_amazon_transactions_{_hash}.pkl",
125+
),
126+
)
127+
def _get_amazon_transactions(
128+
self,
129+
order_years: list[str],
130+
transaction_days: int,
131+
amazon_config: AmazonConfig,
132+
) -> list[AmazonTransactionWithOrderInfo]:
133+
orders_dict = {order.order_number: order for order in self._amazon_orders()}
134+
135+
amazon_transactions = self._amazon_transactions()
136+
137+
amazon_transaction_with_order_details: list[AmazonTransactionWithOrderInfo] = []
138+
for transaction in amazon_transactions:
139+
try:
140+
amazon_transaction_with_order_details.append(
141+
AmazonTransactionWithOrderInfo.from_transaction_and_orders(
142+
orders_dict=orders_dict, transaction=transaction
143+
)
144+
)
145+
except ValueError:
146+
logger.debug(
147+
f"Transaction {transaction.order_number} not found in retrieved orders."
148+
)
149+
continue
103150

104-
orders = _fetch_amazon_order_history(session=amazon_session, years=order_years)
105-
orders_dict = {order.order_number: order for order in orders} # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue, reportUnknownVariableType]
151+
return amazon_transaction_with_order_details
106152

107-
amazon_transactions = _fetch_sorted_amazon_transactions(
108-
transaction_days=transaction_days, amazon_session=amazon_session
109-
)
153+
def _amazon_orders(self) -> list[Order]:
154+
"""Returns a list of Amazon orders.
110155
111-
amazon_transaction_with_order_details: list[AmazonTransactionWithOrderInfo] = []
112-
for transaction in amazon_transactions:
113-
try:
114-
amazon_transaction_with_order_details.append(
115-
AmazonTransactionWithOrderInfo.from_transaction_and_orders(
116-
orders_dict=orders_dict, # pyright: ignore[reportUnknownArgumentType]
117-
transaction=transaction,
118-
)
119-
)
120-
except ValueError:
121-
logger.debug(f"Transaction {transaction.order_number} not found in retrieved orders.") # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
122-
continue
156+
Args:
157+
years (Sequence[int] | None): A sequence of years to fetch orders for. `None` for the current year.
123158
124-
return amazon_transaction_with_order_details
159+
Returns:
160+
list[Order]: A list of Amazon orders.
161+
"""
162+
if "amazon_orders" in self._memo:
163+
return self._memo["amazon_orders"]
125164

165+
amazon_orders = AmazonOrders(self._session())
126166

127-
def _fetch_amazon_order_history(
128-
*, session: AmazonSession, years: Union[list[int], None] = None
129-
) -> list[Order]:
130-
"""Returns a list of Amazon orders.
167+
all_orders: list[Order] = []
168+
for year in self.order_years:
169+
all_orders.extend(amazon_orders.get_order_history(year=year))
170+
all_orders.sort(key=lambda order: order.order_placed_date)
131171

132-
Args:
133-
session (AmazonSession): Amazon session (must be logged in).
134-
years (Sequence[int] | None): A sequence of years to fetch orders for. `None` for the current year.
172+
self._memo["amazon_orders"] = all_orders
135173

136-
Returns:
137-
list[Order]: A list of Amazon orders.
138-
"""
139-
if not session.is_authenticated: # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
140-
raise ValueError("Session must be authenticated.")
141-
amazon_orders = AmazonOrders(session)
142-
if years is None:
143-
years = [date.today().year]
144-
all_orders: list[Order] = []
145-
for year in years:
146-
if len(year_str := str(year)) == 2:
147-
year_str = "20" + year_str
148-
all_orders.extend(amazon_orders.get_order_history(year=year_str)) # pyright: ignore[reportArgumentType]
149-
all_orders.sort(key=lambda order: order.order_placed_date) # pyright: ignore[reportUnknownMemberType, reportUnknownLambdaType, reportAttributeAccessIssue]
150-
151-
return all_orders
152-
153-
154-
def _fetch_sorted_amazon_transactions(
155-
*, amazon_session: AmazonSession, transaction_days: int = 31
156-
) -> list[Transaction]:
157-
"""Fetches and sorts Amazon transactions."""
158-
if not amazon_session.is_authenticated: # pyright: ignore[reportUnknownMemberType, reportAttributeAccessIssue]
159-
raise ValueError("Session must be authenticated.")
160-
amazon_transactions = AmazonTransactions(amazon_session=amazon_session).get_transactions(
161-
days=transaction_days
162-
)
163-
amazon_transactions.sort(key=lambda trans: trans.completed_date) # pyright: ignore[reportUnknownMemberType, reportUnknownLambdaType, reportAttributeAccessIssue]
164-
return amazon_transactions
174+
return self._memo["amazon_orders"]
175+
176+
def _amazon_transactions(self) -> list[Transaction]:
177+
"""Fetches and sorts Amazon transactions."""
178+
if "amazon_transactions" in self._memo:
179+
return self._memo["amazon_transactions"]
180+
181+
self._memo["amazon_transactions"] = AmazonTransactions(
182+
amazon_session=self._session()
183+
).get_transactions(days=self.transaction_days)
184+
185+
self._memo["amazon_transactions"].sort(key=lambda trans: trans.completed_date)
186+
187+
return self._memo["amazon_transactions"]
188+
189+
def _session(self) -> AmazonSession:
190+
if "session" in self._memo:
191+
return self._memo["session"]
192+
193+
amazon_session = self.amazon_config.amazon_session()
194+
amazon_session.login()
195+
196+
if amazon_session.is_authenticated:
197+
self._memo["session"] = amazon_session
198+
return self._memo["session"]
199+
200+
@classmethod
201+
def _normalized_years(cls, years: list[str] | None = None) -> list[str]:
202+
if years is None:
203+
return [date.today().year]
204+
205+
result: list[str] = []
206+
207+
for year in years:
208+
if len(year) == 2:
209+
result.append("20" + year)
210+
elif len(year) == 4:
211+
result.append(year)
212+
else:
213+
raise ValueError("Year must be specified as 2 or 4 digits (e.g. 21 or 2021)")
214+
215+
return result
165216

166217

167218
def print_amazon_transactions(
@@ -221,5 +272,5 @@ def locate_amazon_transaction_by_amount(
221272
return None
222273

223274

224-
if __name__ == "__main__":
225-
print_amazon_transactions(get_amazon_transactions())
275+
# if __name__ == "__main__":
276+
# print_amazon_transactions(AmazonTransactionRetriever.new()

0 commit comments

Comments
 (0)