Skip to content

Commit 9db1a7e

Browse files
author
Karl Wooster
committed
refactor(tox): add tox action to github workflows and make sure that project passes all tox tests
required eliminating some "newer" features such as `X Y` unions and match-case blocks
1 parent e9eee9f commit 9db1a7e

File tree

12 files changed

+301
-185
lines changed

12 files changed

+301
-185
lines changed

.github/workflows/tox_check.yml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: Tox Check
2+
3+
on: [push, pull_request]
4+
5+
jobs:
6+
tox:
7+
8+
runs-on: ubuntu-latest
9+
10+
steps:
11+
- uses: actions/checkout@v4
12+
13+
- name: Install libjpeg-dev (used for Pillow library)
14+
run: sudo apt-get install libjpeg-dev
15+
16+
- name: Install uv
17+
uses: astral-sh/setup-uv@v5
18+
with:
19+
version: 0.6.12
20+
21+
- name: Install Python using uv
22+
run: uv python install
23+
24+
- name: Install Packages using uv
25+
run: uv sync --all-groups --all-extras
26+
27+
- name: Run Tox
28+
run: uv run tox

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ A program to annotate YNAB transactions with Amazon order info
3434
```bash
3535
# Basic installation (without AI features)
3636
uv sync
37-
37+
3838
# Installation with AI features
3939
uv sync --extra ai
4040
```

src/ynamazon/amazon/models.py

Lines changed: 24 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import datetime as dt
44
import uuid
55
from decimal import Decimal
6-
from typing import Annotated, Any
6+
from typing import Annotated, Any, Union
77

88
from amazonorders.session import AmazonSession
99
from amazonorders.transactions import AmazonTransactions
@@ -27,12 +27,12 @@ class Entity(BaseClone, abc.ABC):
2727
class Item(Entity):
2828
title: str
2929
link: HttpUrl
30-
price: Decimal | None = None
31-
seller: AmazonSellerType | None = None
32-
condition: str | None = None
33-
return_eligible_date: dt.date | None = None
34-
image_link: HttpUrl | None = None
35-
quantity: int | None = None
30+
price: Union[Decimal, None] = None
31+
seller: Union[AmazonSellerType, None] = None
32+
condition: Union[str, None] = None
33+
return_eligible_date: Union[dt.date, None] = None
34+
image_link: Union[HttpUrl, None] = None
35+
quantity: Union[int, None] = None
3636

3737
def __str__(self) -> str:
3838
if settings.ynab_use_markdown:
@@ -59,15 +59,15 @@ def address_from_str(cls, data: Any) -> Any:
5959

6060
class Recipient(Entity):
6161
name: str
62-
address: Address | None = Field(
62+
address: Union[Address, None] = Field(
6363
repr=False, default=None, description="not parsed properly, don't use"
6464
)
6565

6666

6767
class Shipment(Entity):
6868
items: list[Item]
69-
delivery_status: str | None = None
70-
tracking_link: HttpUrl | None = None
69+
delivery_status: Union[str, None] = None
70+
tracking_link: Union[HttpUrl, None] = None
7171

7272

7373
class Order(Entity):
@@ -77,24 +77,24 @@ class Order(Entity):
7777
shipments: list[Shipment]
7878
items: list[Item]
7979
order_number: str
80-
order_details_link: HttpUrl | None = None
80+
order_details_link: Union[HttpUrl, None] = None
8181
grand_total: Decimal
8282
order_placed_date: dt.date
83-
recipient: Recipient | None = None
84-
payment_method: str | None = None
85-
payment_method_last_4: str | None = None
86-
total_before_tax: Decimal | None = None
83+
recipient: Union[Recipient, None] = None
84+
payment_method: Union[str, None] = None
85+
payment_method_last_4: Union[str, None] = None
86+
total_before_tax: Union[Decimal, None] = None
8787

8888

8989
class Orders(SimpleDict[str, Order]):
9090
@classmethod
9191
def get_order_history(
9292
cls,
9393
config: AmazonConfig,
94-
session: AmazonSession | None = None,
95-
years: list[int] | int | None = None,
94+
session: Union[AmazonSession, None] = None,
95+
years: Union[list[int], Union[int, None]] = None,
9696
*,
97-
debug: bool | None = None,
97+
debug: Union[bool, None] = None,
9898
):
9999
from ynamazon.amazon_transactions import (
100100
_fetch_amazon_order_history, # pyright: ignore[reportPrivateUsage]
@@ -121,14 +121,18 @@ class Transaction(Entity):
121121
order_number: str
122122
order_details_link: HttpUrl
123123
seller_name: Annotated[str, Field(alias="seller")]
124-
order: Order | None = None
124+
order: Union[Order, None] = None
125125

126126
def match_order(self, orders: Orders) -> None:
127127
"""Matches the transaction with the order."""
128128
self.order = orders.get(self.order_number)
129129

130130
def getattr_path(
131-
self, attr_path: str, *, separator: str = "__", default: Any | Missing = MISSING
131+
self,
132+
attr_path: str,
133+
*,
134+
separator: str = "__",
135+
default: Union[Any, Missing] = MISSING,
132136
) -> Any:
133137
return getattr_path(self, attr_path, separator=separator, default=default)
134138

src/ynamazon/amazon_transactions.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ class AmazonConfig(BaseModel):
7878
config: AmazonOrdersConfig = Field(default_factory=lambda: AmazonOrdersConfig())
7979
debug: bool = False
8080

81-
def amazon_session(self, *, debug: bool | None = None) -> AmazonSession:
81+
def amazon_session(self, *, debug: Union[bool, None] = None) -> AmazonSession:
8282
"""Creates an Amazon session."""
8383
logger.debug(f"Creating Amazon session for {self.username}")
8484
return AmazonSession(
@@ -137,7 +137,7 @@ def _fetch_amazon_order_history(
137137
session: AmazonSession,
138138
years: Union[Sequence[Union[int, str]], Union[int, str], None] = None,
139139
debug: bool = False,
140-
config: AmazonOrdersConfig | None = None,
140+
config: Union[AmazonOrdersConfig, None] = None,
141141
) -> list[Order]:
142142
"""Returns a list of Amazon orders.
143143
@@ -175,7 +175,7 @@ def _fetch_sorted_amazon_transactions(
175175
amazon_session: AmazonSession,
176176
transaction_days: int = 31,
177177
debug: bool = False,
178-
config: AmazonOrdersConfig | None = None,
178+
config: Union[AmazonOrdersConfig, None] = None,
179179
) -> list[Transaction]:
180180
"""Fetches and sorts Amazon transactions."""
181181
if not amazon_session.is_authenticated:

src/ynamazon/prompts.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@
4747
3. Popcorn-shaped Purse"
4848
4949
Do not include the order URL or any additional information - just the item list.
50-
"""
50+
"""

src/ynamazon/settings.py

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from os import PathLike
22
from pathlib import Path
3+
from typing import Union
34

45
from pydantic import BaseModel, ConfigDict, EmailStr, SecretStr, model_validator
56
from pydantic_settings import BaseSettings, SettingsConfigDict
@@ -45,7 +46,7 @@ class Settings(BaseSettings):
4546
ynab_budget_id: SecretBudgetId
4647
amazon_user: EmailStr
4748
amazon_password: SecretStr
48-
openai_api_key: SecretApiKey | None = None
49+
openai_api_key: Union[SecretApiKey, None] = None
4950

5051
ynab_payee_name_to_be_processed: str = "Amazon - Needs Memo"
5152
ynab_payee_name_processing_completed: str = "Amazon"
@@ -70,32 +71,28 @@ class ConfigFile(BaseModel):
7071
"""Configuration file for CLI."""
7172

7273
model_config = ConfigDict(extra="forbid")
73-
ynab_api_key: SecretApiKey | None = None
74-
ynab_budget_id: SecretBudgetId | None = None
75-
amazon_user: EmailStr | None = None
76-
amazon_password: SecretStr | None = None
74+
ynab_api_key: Union[SecretApiKey, None] = None
75+
ynab_budget_id: Union[SecretBudgetId, None] = None
76+
amazon_user: Union[EmailStr, None] = None
77+
amazon_password: Union[SecretStr, None] = None
7778

78-
ynab_payee_name_to_be_processed: str | None = None
79-
ynab_payee_name_processing_completed: str | None = None
80-
ynab_use_markdown: bool | None = None
79+
ynab_payee_name_to_be_processed: Union[str, None] = None
80+
ynab_payee_name_processing_completed: Union[str, None] = None
81+
ynab_use_markdown: Union[bool, None] = None
8182

8283
@classmethod
83-
def from_config(cls, file: str | PathLike):
84+
def from_config(cls, file: Union[str, PathLike]):
8485
file_path = Path(file)
8586
if not file_path.exists():
8687
raise FileNotFoundError(f"Config file {file_path} does not exist.")
8788
if not file_path.is_file():
8889
raise ValueError(f"Config file {file_path} is not a file.")
8990

90-
match file_path.suffix:
91-
case ".yaml":
92-
return cls.model_validate_yaml(file_path.read_text())
93-
case ".toml":
94-
return cls.model_validate_toml(file_path.read_text())
95-
case _:
96-
raise ValueError(
97-
f"Config file {file_path} must be a .toml or .yaml file."
98-
)
91+
if file_path.suffix == ".yaml":
92+
return cls.model_validate_yaml(file_path.read_text())
93+
if file_path.suffix == ".toml":
94+
return cls.model_validate_toml(file_path.read_text())
95+
raise ValueError(f"Config file {file_path} must be a .toml or .yaml file.")
9996

10097
@classmethod
10198
def model_validate_yaml(cls, yaml_str: str):

src/ynamazon/utilities/attrpath.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# pyright: strict
22
from collections import deque
3-
from typing import Any
3+
from typing import Any, Union
44

55
from .sentinels import MISSING
66

77

88
def getattr_path(
99
obj: object, path: str, *, separator: str = "__", default: Any = MISSING
10-
) -> Any | None:
10+
) -> Union[Any, None]:
1111
"""Get an attribute path, as defined by a string separated by '__'.
1212
1313
Example:

src/ynamazon/utilities/bases.py

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
Generic,
55
SupportsIndex,
66
TypeVar,
7+
Union,
78
overload,
89
)
910

@@ -42,14 +43,14 @@ def items(self) -> "dict_items[_KT, _VT]":
4243
return self.root.items()
4344

4445
@overload # type: ignore[override]
45-
def get(self, key: _KT, /) -> _VT | None: ...
46+
def get(self, key: _KT, /) -> Union[_VT, None]: ...
4647
@overload
4748
def get(self, key: _KT, default: _VT, /) -> _VT: ...
4849
@overload
49-
def get(self, key: _KT, default: _T, /) -> _VT | _T: ...
50+
def get(self, key: _KT, default: _T, /) -> Union[_VT, _T]: ...
5051
def get(
51-
self, key: _KT, default: _T | _VT | Missing = MISSING, /
52-
) -> _VT | _T | None:
52+
self, key: _KT, default: Union[_T, _VT, Missing] = MISSING, /
53+
) -> Union[_VT, _T, None]:
5354
"""Return the value for key if key is in the dictionary, else default.
5455
5556
Args:
@@ -65,8 +66,10 @@ def pop(self, key: _KT, /) -> _VT: ...
6566
@overload
6667
def pop(self, key: _KT, default: _VT, /) -> _VT: ...
6768
@overload
68-
def pop(self, key: _KT, default: _T, /) -> _VT | _T: ...
69-
def pop(self, key: _KT, default: _T | _VT | Missing = MISSING, /) -> _VT | _T:
69+
def pop(self, key: _KT, default: _T, /) -> Union[_VT, _T]: ...
70+
def pop(
71+
self, key: _KT, default: Union[_T, _VT, Missing] = MISSING, /
72+
) -> Union[_VT, _T]:
7073
"""Remove specified key and return the corresponding value.
7174
7275
Args:
@@ -130,13 +133,12 @@ def pop(self, index: int = -1) -> _T:
130133
return self.root.pop(index)
131134

132135
def __add__(self, other: "SimpleListRoot | Iterable[_T]"):
133-
match other:
134-
case SimpleListRoot():
135-
self.root += other.root
136-
case Iterable():
137-
self.root += list(other)
138-
case _:
139-
raise NotImplementedError
136+
if isinstance(other, SimpleListRoot):
137+
self.root += other.root
138+
elif isinstance(other, Iterable):
139+
self.root += list(other)
140+
else:
141+
raise NotImplementedError
140142

141143
return self
142144

0 commit comments

Comments
 (0)