Compare commits

...

2 Commits

Author SHA1 Message Date
2509c4dd70 clean up typing a bit more 2025-12-07 08:32:45 -05:00
dda8c3403d package project and add entrypoint 2025-12-07 08:20:48 -05:00
4 changed files with 36 additions and 27 deletions

View File

@@ -16,6 +16,9 @@ dependencies = [
"ynab>=1.9.0",
]
[project.scripts]
catnab = "main:main"
[dependency-groups]
dev = [
"ipdb>=0.13.13",
@@ -24,3 +27,6 @@ dev = [
[tool.pytest.ini_options]
pythonpath = ["src"]
[tool.uv]
package = true

View File

@@ -18,7 +18,7 @@ class AmazonAccount:
password: str
def get_accounts(env: dict[str, str] = os.environ) -> list[AmazonAccount]:
def get_accounts(env: os._Environ | dict[str, str] = os.environ) -> list[AmazonAccount]:
accts = []
for k, v in env.items():
if k == 'AMAZON_EMAIL':

View File

@@ -16,6 +16,29 @@ import ai
import ynab_client
@dataclass
class Context:
sync_days: int
budget_id: str
ynab_token: str
amz_accts: list[tuple[str, str]]
ynab_client: ynab_client.YnabClient
def __init__(
self,
sync_days: int,
budget_id: str | None = None,
ynab_token: str | None = None,
):
self.sync_days = sync_days
self.amazon_accts = amazon.get_accounts()
self.ynab_client = ynab_client.YnabClient(
budget_id=budget_id if budget_id else os.environ['YNAB_BUDGET_ID'],
ynab_token=ynab_token if ynab_token else os.environ['YNAB_TOKEN'],
sync_days=sync_days,
)
@dataclass
class TxCategory:
category_name: str | None
@@ -31,7 +54,8 @@ def tx_date_diff(amz_tx: Transaction, ynab_tx: TransactionDetail):
def is_matchable_ynab(tx: TransactionDetail) -> bool:
return (
# we only care about amazon transactions
'amazon' in tx.payee_name.lower()
tx.payee_name is not None
and 'amazon' in tx.payee_name.lower()
# don't categorize them if we already have, or if the user has manually approved
and tx.flag_color != 'purple'
and not tx.approved
@@ -85,7 +109,8 @@ def get_tx_categories(
details = item_details[item.title]
by_category[details.category].append(item)
subtotal = sum(i.price for i in order.items)
# we have already ensured that order details are fully populated, so i.price should not be None
subtotal = sum(i.price for i in order.items) # type: ignore
tx_categories = []
for category_name, items in by_category.items():
category_total = 0
@@ -143,29 +168,6 @@ def build_tx_update(
)
@dataclass
class Context:
sync_days: int
budget_id: str
ynab_token: str
amz_accts: list[tuple[str, str]]
ynab_client: ynab_client.YnabClient
def __init__(
self,
sync_days: int,
budget_id: str | None = None,
ynab_token: str | None = None,
):
self.sync_days = sync_days
self.amazon_accts = amazon.get_accounts()
self.ynab_client = ynab_client.YnabClient(
budget_id=budget_id if budget_id else os.environ['YNAB_BUDGET_ID'],
ynab_token=ynab_token if ynab_token else os.environ['YNAB_TOKEN'],
sync_days=sync_days,
)
def sync_account(ctx: Context, acct: AmazonAccount):
print(f'Syncing transactions from Amazon account: {acct.email}')
with amazon.Session(acct) as amz:
@@ -217,6 +219,7 @@ def main():
@main.command
@click.option('--days', type=int, default=30)
def sync(days: int):
"""Auto-categorize Amazon transactions known to YNAB."""
ctx = Context(days)
for acct in amazon.get_accounts():
sync_account(ctx, acct)

2
uv.lock generated
View File

@@ -81,7 +81,7 @@ wheels = [
[[package]]
name = "catnab"
version = "0.1.0"
source = { virtual = "." }
source = { editable = "." }
dependencies = [
{ name = "amazon-orders" },
{ name = "click" },