initial commit

This commit is contained in:
2025-11-03 09:27:30 -05:00
commit 307d029648
12 changed files with 1680 additions and 0 deletions

77
tests/test_ai.py Normal file
View File

@@ -0,0 +1,77 @@
import json
import pytest
import ai
@pytest.mark.skip
def test_classify_live():
item_names = [
'Syston Cable Technology Cat 6A+ Ethernet Network Cable700MHz 23AWG Solid Bare Copper Wire Outdoor/Indoor, No Ends 100/250/500/1000 Ft Available, Heat Resistant Plenum Rated (100 FT, CMP, Blue-CMP)',
"Chef'n Salad Dressing Mixer, Baking White/Vintage Indigo",
"Lee Women's Ultra Lux Comfort with Flex Motion Skinny Leg Jean with Ever Fit, Black",
]
categories = [
'Home: Mortgage',
'Home: Property Tax',
'Home: Home Insurance',
'Home: Water',
'Home: Oil',
'Home: Other Utilities',
'Car: Car Insurance - Annual',
'Car: Car Insurance - Monthly',
'Car: Maintenance',
'Internal Master Category: Inflow: Ready to Assign',
'Internal Master Category: Deferred Income SubCategory',
'Internal Master Category: Uncategorized',
'Essentials: Groceries',
"Essentials: I'm Too Lazy To Cook",
'Essentials: Gas',
'Essentials: Clothes',
'Essentials: Baby',
'Essentials: Toiletries and Hygiene',
'Essentials: Medical',
'Essentials: Software Subscriptions',
'Essentials: Stuff I Forgot to Budget For',
'Essentials: Charity',
'Semi-Essentials: Sports and Fitness',
'Semi-Essentials: Gifts',
'Semi-Essentials: Technology',
'Semi-Essentials: Household Items',
'Semi-Essentials: Travel',
'Credit Card Payments: Citi Credit',
'Credit Card Payments: BoA Credit',
'Credit Card Payments: Amazon Credit',
"Credit Card Payments: BJ's Credit",
'Debt Payments: Student Loan',
'Just for Fun: Date Night',
'Just for Fun: Hobbies/Activities',
'Just for Fun: Doing Things With Friends',
'Just for Fun: Gaming',
'Just for Fun: Media',
'Long-term: Home Improvement',
'Long-term: The Future'
]
res = ai.classify(item_names, categories)
assert len(res) == 3
expected_categories = [
'Semi-Essentials: Technology',
'Semi-Essentials: Household Items',
'Essentials: Clothes'
]
for classified, ec in zip(res.values(), expected_categories):
assert classified.category == ec
@pytest.mark.skip
def test_classify_live_trouble():
with open('output/item_names.json') as f:
item_names = json.load(f)
with open('output/text_categories.json') as f:
text_categories = json.load(f)
res = ai.classify(item_names, text_categories)
assert set(item_names) ^ res.keys() == set()

66
tests/test_amazon.py Normal file
View File

@@ -0,0 +1,66 @@
import amazon
def test_get_accounts():
env = {
'AMAZON_EMAIL': 'test@example.com',
'AMAZON_PASSWORD': 'password123',
}
res = amazon.get_accounts(env)
assert len(res) == 1
email, pwd = res[0]
assert email == 'test@example.com'
assert pwd == 'password123'
def test_get_accounts_numbered():
env = {
'AMAZON_EMAIL_0': 'test@example.com',
'AMAZON_PASSWORD_0': 'password123',
}
res = amazon.get_accounts(env)
assert len(res) == 1
email, pwd = res[0]
assert email == 'test@example.com'
assert pwd == 'password123'
def test_get_accounts_numbered_multi():
env = {
'AMAZON_EMAIL_0': 'test@example.com',
'AMAZON_PASSWORD_0': 'password123',
'AMAZON_EMAIL_1': 'test2@example.com',
'AMAZON_PASSWORD_1': 'password456',
}
res = amazon.get_accounts(env)
assert len(res) == 2
email1, pwd1 = res[0]
assert email1 == 'test@example.com'
assert pwd1 == 'password123'
email2, pwd2 = res[1]
assert email2 == 'test2@example.com'
assert pwd2 == 'password456'
def test_get_accounts_both():
env = {
'AMAZON_EMAIL': 'test@example.com',
'AMAZON_PASSWORD': 'password123',
'AMAZON_EMAIL_0': 'test2@example.com',
'AMAZON_PASSWORD_0': 'password456',
}
res = amazon.get_accounts(env)
assert len(res) == 2
email1, pwd1 = res[0]
assert email1 == 'test@example.com'
assert pwd1 == 'password123'
email2, pwd2 = res[1]
assert email2 == 'test2@example.com'
assert pwd2 == 'password456'

206
tests/test_main.py Normal file
View File

@@ -0,0 +1,206 @@
import datetime
import json
import pickle
from types import SimpleNamespace
from amazonorders.entity.order import Order
from amazonorders.entity.item import Item
from amazonorders.entity.shipment import Shipment
from amazonorders.entity.transaction import Transaction
from amazonorders.entity.recipient import Recipient
import pytest
import main
KEYS = {
Item: [
'title', 'link', 'price', 'seller', 'condition',
'return_eligible_date', 'image_link', 'quantity'
],
Order: [
'full_details', 'index', 'shipments', 'items', 'order_number', 'order_details_link',
'grand_total', 'order_placed_date', 'recipient', 'payment_method', 'payment_method_last_4',
'subtotal', 'shipping_total', 'free_shipping', 'promotion_applied', 'coupon_savings',
'reward_points', 'subscription_discount', 'total_before_tax', 'estimated_tax',
'refund_total', 'multibuy_discount', 'amazon_discount', 'gift_card', 'gift_wrap',
],
Transaction: [
'completed_date', 'payment_method', 'grand_total', 'is_refund',
'order_number', 'order_details_link', 'seller',
],
Shipment: [
'items', 'delivery_status', 'tracking_link',
],
Recipient: [
'name', 'address',
],
}
def to_dict(entity):
entity_keys = KEYS[type(entity)]
result = {}
for k, v in entity.__dict__.items():
if k in entity_keys:
if type(v) in KEYS:
result[k] = to_dict(v)
elif type(v) == list and len(v) > 0 and type(v[0]) in KEYS:
result[k] = [to_dict(i) for i in v]
elif type(v) in (datetime.date, datetime.datetime):
result[k] = v.isoformat()
else:
result[k] = v
return result
# @pytest.fixture
def mock_amz_hist():
with open('output/transactions_2025.pkl', 'rb') as f:
return pickle.load(f)
# @pytest.fixture
def mock_ynab_hist():
with open('output/ynab_history.json') as f:
return json.load(f)
def test_get_tx_categories(monkeypatch):
order = SimpleNamespace(
items=[
SimpleNamespace(
title='An example item',
price=17.49
)
]
)
item_details={
'An example item': SimpleNamespace(
category='Example category',
description='example',
)
}
def classify(_names, _categories):
return item_details
monkeypatch.setattr('ai.classify', classify)
res = main.get_tx_categories(order, item_details)
assert len(res) == 1
assert res[0].category_name == 'Example category'
assert res[0].description == 'example'
assert res[0].ratio == 1.0
def test_get_categories_multi_items(monkeypatch):
order = SimpleNamespace(
items=[
SimpleNamespace(
title='An example item',
price=17.49
),
SimpleNamespace(
title='Another example',
price=12.34,
)
]
)
item_details={
'An example item': SimpleNamespace(
category='Example category',
description='example',
),
'Another example': SimpleNamespace(
category='Example category',
description='second example',
)
}
def classify(_names, _categories):
return item_details
monkeypatch.setattr('ai.classify', classify)
res = main.get_tx_categories(order, item_details)
assert len(res) == 1
assert res[0].category_name == 'Example category'
assert res[0].description == 'example, second example'
assert res[0].ratio == 1.0
def test_get_categories_multi_categories(monkeypatch):
order = SimpleNamespace(
items=[
SimpleNamespace(
title='An example item',
price=17.49
),
SimpleNamespace(
title='Another example',
price=12.34,
)
]
)
item_details={
'An example item': SimpleNamespace(
category='Example category',
description='example',
),
'Another example': SimpleNamespace(
category='Other category',
description='second example',
)
}
def classify(_names, _categories):
return item_details
monkeypatch.setattr('ai.classify', classify)
res = main.get_tx_categories(order, item_details)
assert len(res) == 2
assert res[0].category_name == 'Example category'
assert res[0].description == 'example'
assert res[0].ratio == pytest.approx(0.59, abs=0.01)
assert res[1].category_name == 'Other category'
assert res[1].description == 'second example'
assert res[1].ratio == pytest.approx(0.41, abs=0.01)
def test_build_tx_update_multi():
tx_categories = [
SimpleNamespace(
category_name='Example category',
description='example',
ratio=0.68,
),
SimpleNamespace(
category_name='Other category',
description='second example',
ratio=0.32,
)
]
category_ids = {
'Example category': '123',
'Other category': '456',
}
# ynab uses tenths of a cent as its unit
tx_total = 23760
res = main.build_tx_update(tx_categories, category_ids, tx_total)
assert len(res.subtransactions) == 2
sub0 = res.subtransactions[0]
assert sub0.category_id == '123'
assert sub0.memo == 'example'
assert sub0.amount == 16157
sub1 = res.subtransactions[1]
assert sub1.category_id == '456'
assert sub1.memo == 'second example'
assert sub1.amount == 7603