|
1 | 1 | # pyright: reportDeprecated=false |
| 2 | +import os |
| 3 | +import tempfile |
2 | 4 | from datetime import date |
3 | 5 | from decimal import Decimal |
4 | 6 | from typing import Annotated, Union # , Self # not available python <3.11 |
5 | 7 |
|
6 | 8 | from amazonorders.entity.order import Order |
7 | 9 | from amazonorders.entity.transaction import Transaction |
8 | | -from amazonorders.exception import AmazonOrdersAuthError |
9 | 10 | from amazonorders.orders import AmazonOrders |
10 | 11 | from amazonorders.session import AmazonSession |
11 | 12 | from amazonorders.transactions import AmazonTransactions |
| 13 | +from cache_decorator import Cache |
12 | 14 | from loguru import logger |
13 | 15 | from pydantic import AnyUrl, BaseModel, EmailStr, Field, SecretStr, field_validator |
14 | 16 | from rich import print as rprint |
@@ -75,93 +77,142 @@ def amazon_session(self) -> AmazonSession: |
75 | 77 | ) |
76 | 78 |
|
77 | 79 |
|
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. |
84 | 89 |
|
85 | | - Args: |
| 90 | + amazon_config (AmazonConfig): Configuration for Amazon, primarily credentials |
86 | 91 | 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 | + ) |
89 | 117 |
|
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 |
103 | 150 |
|
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 |
106 | 152 |
|
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. |
110 | 155 |
|
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. |
123 | 158 |
|
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"] |
125 | 164 |
|
| 165 | + amazon_orders = AmazonOrders(self._session()) |
126 | 166 |
|
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) |
131 | 171 |
|
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 |
135 | 173 |
|
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 |
165 | 216 |
|
166 | 217 |
|
167 | 218 | def print_amazon_transactions( |
@@ -221,5 +272,5 @@ def locate_amazon_transaction_by_amount( |
221 | 272 | return None |
222 | 273 |
|
223 | 274 |
|
224 | | -if __name__ == "__main__": |
225 | | - print_amazon_transactions(get_amazon_transactions()) |
| 275 | +# if __name__ == "__main__": |
| 276 | +# print_amazon_transactions(AmazonTransactionRetriever.new() |
0 commit comments