ynab-amazon-categorizer 2.0.2__tar.gz → 2.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.github/workflows/release.yml +36 -5
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.gitignore +7 -1
- ynab_amazon_categorizer-2.1.2/.pre-commit-config.yaml +10 -0
- {ynab_amazon_categorizer-2.0.2/src/ynab_amazon_categorizer.egg-info → ynab_amazon_categorizer-2.1.2}/PKG-INFO +4 -5
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/README.md +1 -1
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/pyproject.toml +10 -6
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/__init__.py +4 -1
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/_version.py +3 -3
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/amazon_parser.py +16 -9
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/cli.py +111 -64
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/config.py +3 -2
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/exceptions.py +1 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/memo_generator.py +10 -3
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/transaction_matcher.py +13 -3
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/ynab_client.py +8 -4
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2/src/ynab_amazon_categorizer.egg-info}/PKG-INFO +4 -5
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/SOURCES.txt +0 -2
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_amazon_parser.py +3 -3
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_amazon_parser_real.py +5 -5
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_config.py +5 -3
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_memo_generator.py +4 -4
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_transaction_matcher.py +18 -9
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_ynab_client.py +9 -8
- ynab_amazon_categorizer-2.0.2/.pre-commit-config.yaml +0 -28
- ynab_amazon_categorizer-2.0.2/install.bat +0 -81
- ynab_amazon_categorizer-2.0.2/ynab_amazon_categorizer.py.backup +0 -1020
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.env.example +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.git_archival.txt +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.gitattributes +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/ARCHITECTURE.md +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/CLAUDE.md +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/CONTEXT.md +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/DEVELOPMENT.md +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/LICENSE +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/PROGRESS.md +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/TODO.md +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/setup.cfg +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/__main__.py +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/dependency_links.txt +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/entry_points.txt +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/requires.txt +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/top_level.txt +0 -0
- {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/__init__.py +0 -0
{ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.github/workflows/release.yml
RENAMED
|
@@ -22,14 +22,45 @@ jobs:
|
|
|
22
22
|
- name: Install dependencies
|
|
23
23
|
run: |
|
|
24
24
|
python -m pip install --upgrade pip
|
|
25
|
-
pip install -e .
|
|
25
|
+
pip install -e .[dev]
|
|
26
26
|
|
|
27
|
-
- name:
|
|
27
|
+
- name: Run tests
|
|
28
|
+
run: pytest
|
|
29
|
+
|
|
30
|
+
lint:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- name: Checkout code
|
|
34
|
+
uses: actions/checkout@v4
|
|
35
|
+
- name: Set up Python
|
|
36
|
+
uses: actions/setup-python@v4
|
|
37
|
+
with:
|
|
38
|
+
python-version: '3.9'
|
|
39
|
+
- name: Install dependencies
|
|
28
40
|
run: |
|
|
29
|
-
python -
|
|
41
|
+
python -m pip install --upgrade pip
|
|
42
|
+
pip install -e .[dev]
|
|
43
|
+
- name: Run pre-commit hooks
|
|
44
|
+
run: pre-commit run --all-files
|
|
45
|
+
|
|
46
|
+
type-check:
|
|
47
|
+
runs-on: ubuntu-latest
|
|
48
|
+
steps:
|
|
49
|
+
- name: Checkout code
|
|
50
|
+
uses: actions/checkout@v4
|
|
51
|
+
- name: Set up Python
|
|
52
|
+
uses: actions/setup-python@v4
|
|
53
|
+
with:
|
|
54
|
+
python-version: '3.9'
|
|
55
|
+
- name: Install dependencies
|
|
56
|
+
run: |
|
|
57
|
+
python -m pip install --upgrade pip
|
|
58
|
+
pip install -e .[dev]
|
|
59
|
+
- name: Run type checker
|
|
60
|
+
run: mypy .
|
|
30
61
|
|
|
31
62
|
publish:
|
|
32
|
-
needs: test
|
|
63
|
+
needs: [test, lint, type-check]
|
|
33
64
|
runs-on: ubuntu-latest
|
|
34
65
|
permissions:
|
|
35
66
|
id-token: write
|
|
@@ -61,7 +92,7 @@ jobs:
|
|
|
61
92
|
uses: pypa/gh-action-pypi-publish@release/v1
|
|
62
93
|
|
|
63
94
|
create-release:
|
|
64
|
-
needs: [
|
|
95
|
+
needs: [publish]
|
|
65
96
|
runs-on: ubuntu-latest
|
|
66
97
|
permissions:
|
|
67
98
|
contents: write
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ynab-amazon-categorizer
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Automatically categorize Amazon transactions in YNAB with rich item information
|
|
5
5
|
Author: dizzlkheinz
|
|
6
6
|
License: GPL-3.0
|
|
@@ -14,15 +14,14 @@ Classifier: Intended Audience :: End Users/Desktop
|
|
|
14
14
|
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
15
15
|
Classifier: Operating System :: OS Independent
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.7
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.8
|
|
19
17
|
Classifier: Programming Language :: Python :: 3.9
|
|
20
18
|
Classifier: Programming Language :: Python :: 3.10
|
|
21
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
22
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
22
|
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
24
23
|
Classifier: Topic :: Utilities
|
|
25
|
-
Requires-Python: >=3.
|
|
24
|
+
Requires-Python: >=3.9
|
|
26
25
|
Description-Content-Type: text/markdown
|
|
27
26
|
License-File: LICENSE
|
|
28
27
|
Requires-Dist: requests>=2.25.0
|
|
@@ -40,7 +39,7 @@ Dynamic: license-file
|
|
|
40
39
|
|
|
41
40
|
# YNAB Amazon Categorizer
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
A Python package that assists in categorizing Amazon transactions in YNAB (You Need A Budget) with rich item information, automatic memo generation, and tab-completion for categories.
|
|
44
43
|
|
|
45
44
|
## Features
|
|
46
45
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# YNAB Amazon Categorizer
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A Python package that assists in categorizing Amazon transactions in YNAB (You Need A Budget) with rich item information, automatic memo generation, and tab-completion for categories.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
@@ -7,7 +7,7 @@ name = "ynab-amazon-categorizer"
|
|
|
7
7
|
dynamic = ["version"]
|
|
8
8
|
description = "Automatically categorize Amazon transactions in YNAB with rich item information"
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
11
|
license = {text = "GPL-3.0"}
|
|
12
12
|
authors = [
|
|
13
13
|
{name = "dizzlkheinz"},
|
|
@@ -20,12 +20,11 @@ classifiers = [
|
|
|
20
20
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
|
21
21
|
"Operating System :: OS Independent",
|
|
22
22
|
"Programming Language :: Python :: 3",
|
|
23
|
-
"Programming Language :: Python :: 3.7",
|
|
24
|
-
"Programming Language :: Python :: 3.8",
|
|
25
23
|
"Programming Language :: Python :: 3.9",
|
|
26
24
|
"Programming Language :: Python :: 3.10",
|
|
27
25
|
"Programming Language :: Python :: 3.11",
|
|
28
26
|
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
29
28
|
"Topic :: Office/Business :: Financial :: Accounting",
|
|
30
29
|
"Topic :: Utilities",
|
|
31
30
|
]
|
|
@@ -95,12 +94,15 @@ exclude_lines = [
|
|
|
95
94
|
|
|
96
95
|
[tool.black]
|
|
97
96
|
line-length = 88
|
|
98
|
-
target-version = ["
|
|
97
|
+
target-version = ["py39"]
|
|
99
98
|
include = '\\.pyi?$'
|
|
100
99
|
|
|
101
100
|
[tool.ruff]
|
|
102
|
-
target-version = "
|
|
101
|
+
target-version = "py39"
|
|
103
102
|
line-length = 88
|
|
103
|
+
extend-exclude = ["src/ynab_amazon_categorizer/_version.py"]
|
|
104
|
+
|
|
105
|
+
[tool.ruff.lint]
|
|
104
106
|
select = [
|
|
105
107
|
"E", # pycodestyle errors
|
|
106
108
|
"W", # pycodestyle warnings
|
|
@@ -117,7 +119,7 @@ ignore = [
|
|
|
117
119
|
]
|
|
118
120
|
|
|
119
121
|
[tool.mypy]
|
|
120
|
-
python_version = "3.
|
|
122
|
+
python_version = "3.11"
|
|
121
123
|
check_untyped_defs = true
|
|
122
124
|
disallow_any_generics = true
|
|
123
125
|
disallow_incomplete_defs = true
|
|
@@ -125,4 +127,6 @@ disallow_untyped_defs = true
|
|
|
125
127
|
no_implicit_optional = true
|
|
126
128
|
warn_redundant_casts = true
|
|
127
129
|
warn_unused_ignores = true
|
|
130
|
+
ignore_missing_imports = true
|
|
131
|
+
allow_redefinition = true
|
|
128
132
|
|
|
@@ -4,7 +4,10 @@ try:
|
|
|
4
4
|
from importlib.metadata import PackageNotFoundError, version
|
|
5
5
|
except ImportError:
|
|
6
6
|
# Python < 3.8
|
|
7
|
-
from importlib_metadata import
|
|
7
|
+
from importlib_metadata import ( # type: ignore[no-redef]
|
|
8
|
+
PackageNotFoundError,
|
|
9
|
+
version,
|
|
10
|
+
)
|
|
8
11
|
|
|
9
12
|
try:
|
|
10
13
|
__version__ = version("ynab-amazon-categorizer")
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '2.
|
|
32
|
-
__version_tuple__ = version_tuple = (2,
|
|
31
|
+
__version__ = version = '2.1.2'
|
|
32
|
+
__version_tuple__ = version_tuple = (2, 1, 2)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g4639f2a18'
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
"""Amazon order parsing functionality."""
|
|
2
|
+
|
|
2
3
|
import re
|
|
4
|
+
from typing import Optional
|
|
3
5
|
|
|
4
6
|
|
|
5
7
|
class Order:
|
|
6
8
|
"""Represents a parsed Amazon order."""
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
self.
|
|
10
|
-
self.
|
|
11
|
-
self.
|
|
9
|
+
|
|
10
|
+
def __init__(self) -> None:
|
|
11
|
+
self.order_id: Optional[str] = None
|
|
12
|
+
self.total: Optional[float] = None
|
|
13
|
+
self.date_str: Optional[str] = None
|
|
14
|
+
self.items: list[str] = []
|
|
12
15
|
|
|
13
16
|
|
|
14
17
|
class AmazonParser:
|
|
15
18
|
"""Parses Amazon order data from order history pages."""
|
|
16
19
|
|
|
17
|
-
def parse_orders_page(self, orders_text: str) -> list:
|
|
20
|
+
def parse_orders_page(self, orders_text: str) -> list[Order]:
|
|
18
21
|
"""Parse Amazon orders page text to extract order information"""
|
|
19
22
|
if not orders_text.strip():
|
|
20
23
|
return []
|
|
@@ -23,7 +26,9 @@ class AmazonParser:
|
|
|
23
26
|
|
|
24
27
|
# Find all order blocks using regex
|
|
25
28
|
order_pattern = r"Order placed\s*([A-Za-z]+ \d+, \d{4})\s*Total\s*\$(\d+\.?\d*)\s*.*?Order # (\d{3}-\d{7}-\d{7})"
|
|
26
|
-
order_matches = re.finditer(
|
|
29
|
+
order_matches = re.finditer(
|
|
30
|
+
order_pattern, orders_text, re.DOTALL | re.IGNORECASE
|
|
31
|
+
)
|
|
27
32
|
|
|
28
33
|
for match in order_matches:
|
|
29
34
|
order_date = match.group(1).strip()
|
|
@@ -32,7 +37,9 @@ class AmazonParser:
|
|
|
32
37
|
|
|
33
38
|
# Find the content after this order until the next order or end
|
|
34
39
|
start_pos = match.end()
|
|
35
|
-
next_order = re.search(
|
|
40
|
+
next_order = re.search(
|
|
41
|
+
r"Order placed", orders_text[start_pos:], re.IGNORECASE
|
|
42
|
+
)
|
|
36
43
|
if next_order:
|
|
37
44
|
end_pos = start_pos + next_order.start()
|
|
38
45
|
order_content = orders_text[start_pos:end_pos]
|
|
@@ -54,7 +61,7 @@ class AmazonParser:
|
|
|
54
61
|
|
|
55
62
|
return orders
|
|
56
63
|
|
|
57
|
-
def extract_items_from_content(self, order_content):
|
|
64
|
+
def extract_items_from_content(self, order_content: str) -> list[str]:
|
|
58
65
|
"""Extract item names from order content."""
|
|
59
66
|
items = []
|
|
60
67
|
lines = order_content.split("\n")
|
{ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/cli.py
RENAMED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import json
|
|
2
2
|
import os # For history file path
|
|
3
|
-
|
|
4
|
-
from
|
|
3
|
+
from collections.abc import Iterable
|
|
4
|
+
from typing import Optional, Union
|
|
5
5
|
|
|
6
6
|
# --- NEW: Import prompt_toolkit components ---
|
|
7
7
|
from prompt_toolkit import prompt
|
|
8
|
-
from prompt_toolkit.completion import Completer, Completion
|
|
8
|
+
from prompt_toolkit.completion import CompleteEvent, Completer, Completion
|
|
9
|
+
from prompt_toolkit.document import Document
|
|
9
10
|
from prompt_toolkit.history import FileHistory
|
|
10
|
-
from prompt_toolkit.key_binding import KeyBindings
|
|
11
|
+
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent
|
|
11
12
|
|
|
12
|
-
from .amazon_parser import AmazonParser
|
|
13
|
+
from .amazon_parser import AmazonParser, Order
|
|
13
14
|
|
|
14
15
|
# --- END NEW ---
|
|
15
16
|
# --- Import extracted modules ---
|
|
@@ -33,7 +34,7 @@ YNAB_API_URL = "https://api.ynab.com/v1"
|
|
|
33
34
|
# NOTE: Amazon order parsing has been moved to amazon_parser.py
|
|
34
35
|
|
|
35
36
|
|
|
36
|
-
def prompt_for_amazon_orders_data():
|
|
37
|
+
def prompt_for_amazon_orders_data() -> Optional[list[Order]]:
|
|
37
38
|
"""Prompt user to paste Amazon orders page data"""
|
|
38
39
|
print("\n--- Amazon Orders Data Entry ---")
|
|
39
40
|
print("You can copy and paste the content from your Amazon orders page.")
|
|
@@ -71,12 +72,14 @@ def prompt_for_amazon_orders_data():
|
|
|
71
72
|
# NOTE: find_matching_order moved to transaction_matcher.py
|
|
72
73
|
|
|
73
74
|
|
|
74
|
-
def get_multiline_input_with_custom_submit(
|
|
75
|
+
def get_multiline_input_with_custom_submit(
|
|
76
|
+
prompt_message: str = "Enter multiline text: ",
|
|
77
|
+
) -> Optional[str]:
|
|
75
78
|
"""Get multiline input with Ctrl+J to submit"""
|
|
76
79
|
kb = KeyBindings()
|
|
77
80
|
|
|
78
81
|
@kb.add("escape", "enter") # Binds Alt+Enter to submit
|
|
79
|
-
def _(event):
|
|
82
|
+
def _(event: KeyPressEvent) -> None:
|
|
80
83
|
"""When Alt+Enter is pressed, accept the current buffer's text."""
|
|
81
84
|
event.app.exit(result=event.app.current_buffer.text)
|
|
82
85
|
|
|
@@ -95,7 +98,7 @@ def get_multiline_input_with_custom_submit(prompt_message="Enter multiline text:
|
|
|
95
98
|
return None
|
|
96
99
|
|
|
97
100
|
|
|
98
|
-
def generate_split_summary_memo(matching_order):
|
|
101
|
+
def generate_split_summary_memo(matching_order: Order) -> str:
|
|
99
102
|
"""Generate a summary memo for split transactions showing all items"""
|
|
100
103
|
if (
|
|
101
104
|
not matching_order
|
|
@@ -116,11 +119,13 @@ def generate_split_summary_memo(matching_order):
|
|
|
116
119
|
return summary
|
|
117
120
|
|
|
118
121
|
|
|
119
|
-
def prompt_for_item_details()
|
|
122
|
+
def prompt_for_item_details() -> Optional[
|
|
123
|
+
dict[str, Union[str, int, float, list[str], None]]
|
|
124
|
+
]:
|
|
120
125
|
"""Prompt user to enter item details manually"""
|
|
121
126
|
print("\n--- Manual Item Details Entry ---")
|
|
122
127
|
|
|
123
|
-
item_details = {}
|
|
128
|
+
item_details: dict[str, Union[str, int, float, list[str], None]] = {}
|
|
124
129
|
|
|
125
130
|
# Get item title/description
|
|
126
131
|
title = input("Enter item title/description (optional): ").strip()
|
|
@@ -174,11 +179,13 @@ def prompt_for_item_details():
|
|
|
174
179
|
|
|
175
180
|
class CategoryCompleter(Completer):
|
|
176
181
|
# ... (implementation from v3) ...
|
|
177
|
-
def __init__(self, category_list):
|
|
182
|
+
def __init__(self, category_list: list[tuple[str, str]]) -> None:
|
|
178
183
|
self.categories = [name for name, _id in category_list]
|
|
179
184
|
self.category_list = category_list
|
|
180
185
|
|
|
181
|
-
def get_completions(
|
|
186
|
+
def get_completions(
|
|
187
|
+
self, document: Document, complete_event: CompleteEvent
|
|
188
|
+
) -> Iterable[Completion]:
|
|
182
189
|
text_before_cursor = document.text_before_cursor.lower()
|
|
183
190
|
if text_before_cursor:
|
|
184
191
|
for category_name in self.categories:
|
|
@@ -188,7 +195,9 @@ class CategoryCompleter(Completer):
|
|
|
188
195
|
)
|
|
189
196
|
|
|
190
197
|
|
|
191
|
-
def prompt_for_category_selection(
|
|
198
|
+
def prompt_for_category_selection(
|
|
199
|
+
category_completer: CategoryCompleter, name_to_id_map: dict[str, str]
|
|
200
|
+
) -> tuple[Optional[str], Optional[str]]:
|
|
192
201
|
# ... (implementation from v3) ...
|
|
193
202
|
history_file = os.path.join(os.path.expanduser("~"), ".ynab_amazon_cat_history")
|
|
194
203
|
history = FileHistory(history_file)
|
|
@@ -227,7 +236,7 @@ def prompt_for_category_selection(category_completer, name_to_id_map):
|
|
|
227
236
|
# --- Main Script Logic ---
|
|
228
237
|
|
|
229
238
|
|
|
230
|
-
def main():
|
|
239
|
+
def main() -> None:
|
|
231
240
|
"""Main CLI function."""
|
|
232
241
|
# Load configuration using extracted Config class
|
|
233
242
|
try:
|
|
@@ -260,10 +269,7 @@ def main():
|
|
|
260
269
|
print(f"\nFound {len(categories_list)} usable categories. Completion enabled.")
|
|
261
270
|
|
|
262
271
|
# Setup history for memo input (optional, but can be nice)
|
|
263
|
-
|
|
264
|
-
os.path.expanduser("~"), ".ynab_amazon_memo_history"
|
|
265
|
-
)
|
|
266
|
-
memo_history = FileHistory(memo_history_file)
|
|
272
|
+
os.path.join(os.path.expanduser("~"), ".ynab_amazon_memo_history")
|
|
267
273
|
|
|
268
274
|
# Ask user if they want to provide Amazon orders data for automatic item detection
|
|
269
275
|
print("\n--- Optional: Amazon Orders Data ---")
|
|
@@ -363,18 +369,29 @@ def main():
|
|
|
363
369
|
)
|
|
364
370
|
if matching_order:
|
|
365
371
|
print("\n 🎯 MATCHED ORDER FOUND:")
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
372
|
+
if isinstance(matching_order, Order):
|
|
373
|
+
order_id = matching_order.order_id
|
|
374
|
+
total = matching_order.total
|
|
375
|
+
date_str = matching_order.date_str
|
|
376
|
+
items = matching_order.items
|
|
377
|
+
else:
|
|
378
|
+
order_id = matching_order.get("order_id")
|
|
379
|
+
total = matching_order.get("total")
|
|
380
|
+
date_str = matching_order.get("date") or matching_order.get(
|
|
381
|
+
"date_str"
|
|
382
|
+
)
|
|
383
|
+
items = matching_order.get("items", [])
|
|
384
|
+
|
|
385
|
+
print(f" Order ID: {order_id}")
|
|
386
|
+
print(f" Total: ${total if total is not None else 'N/A'}")
|
|
387
|
+
print(f" Date: {date_str if date_str is not None else 'N/A'}")
|
|
369
388
|
# Use extracted memo generator for order link
|
|
370
389
|
memo_generator = MemoGenerator()
|
|
371
|
-
order_link = memo_generator.generate_amazon_order_link(
|
|
372
|
-
matching_order.order_id
|
|
373
|
-
)
|
|
390
|
+
order_link = memo_generator.generate_amazon_order_link(order_id)
|
|
374
391
|
print(f" Order Link: {order_link}")
|
|
375
|
-
if
|
|
392
|
+
if items:
|
|
376
393
|
print(" Items:")
|
|
377
|
-
for item in
|
|
394
|
+
for item in items:
|
|
378
395
|
print(f" - {item}")
|
|
379
396
|
print()
|
|
380
397
|
else:
|
|
@@ -397,12 +414,18 @@ def main():
|
|
|
397
414
|
updated_payload_dict = None
|
|
398
415
|
|
|
399
416
|
# Check if there are multiple items and suggest splitting
|
|
400
|
-
if
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
417
|
+
# Check if we should offer splitting
|
|
418
|
+
should_offer_split = False
|
|
419
|
+
if matching_order:
|
|
420
|
+
if isinstance(matching_order, Order):
|
|
421
|
+
should_offer_split = bool(
|
|
422
|
+
matching_order.items and len(matching_order.items) > 1
|
|
423
|
+
)
|
|
424
|
+
else:
|
|
425
|
+
items = matching_order.get("items", [])
|
|
426
|
+
should_offer_split = bool(items and len(items) > 1)
|
|
427
|
+
|
|
428
|
+
if should_offer_split:
|
|
406
429
|
print("There is more than one item in this transaction.")
|
|
407
430
|
|
|
408
431
|
split_decision = input(
|
|
@@ -419,17 +442,29 @@ def main():
|
|
|
419
442
|
continue # Back to action prompt
|
|
420
443
|
|
|
421
444
|
# --- ENHANCED MEMO INPUT WITH AUTOMATIC ITEM DETECTION ---
|
|
445
|
+
item_details: Optional[
|
|
446
|
+
dict[str, Union[str, int, float, list[str], None]]
|
|
447
|
+
] = None
|
|
422
448
|
enhanced_memo = None
|
|
423
449
|
|
|
424
450
|
# Use matched order data or prompt for manual entry
|
|
425
451
|
if matching_order:
|
|
426
452
|
print("Using matched order data for memo generation...")
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
453
|
+
if isinstance(matching_order, Order):
|
|
454
|
+
item_details = {
|
|
455
|
+
"order_id": matching_order.order_id or "",
|
|
456
|
+
"items": matching_order.items,
|
|
457
|
+
"total": matching_order.total,
|
|
458
|
+
"date": matching_order.date_str,
|
|
459
|
+
}
|
|
460
|
+
else:
|
|
461
|
+
item_details = {
|
|
462
|
+
"order_id": matching_order.get("order_id", ""),
|
|
463
|
+
"items": matching_order.get("items", []),
|
|
464
|
+
"total": matching_order.get("total"),
|
|
465
|
+
"date": matching_order.get("date")
|
|
466
|
+
or matching_order.get("date_str"),
|
|
467
|
+
}
|
|
433
468
|
else:
|
|
434
469
|
# Ask if user wants to enter item details manually
|
|
435
470
|
manual_entry = input(
|
|
@@ -443,15 +478,19 @@ def main():
|
|
|
443
478
|
if item_details:
|
|
444
479
|
if isinstance(item_details, dict) and "items" in item_details:
|
|
445
480
|
# Auto-matched order data - format as: Item Name\n Order Link
|
|
481
|
+
items_list = item_details["items"]
|
|
446
482
|
items_text = (
|
|
447
|
-
|
|
448
|
-
if
|
|
483
|
+
items_list[0]
|
|
484
|
+
if isinstance(items_list, list) and items_list
|
|
449
485
|
else "Amazon Purchase"
|
|
450
486
|
)
|
|
451
487
|
# Use extracted memo generator
|
|
452
488
|
memo_generator = MemoGenerator()
|
|
489
|
+
order_id_value = item_details["order_id"]
|
|
453
490
|
order_link = memo_generator.generate_amazon_order_link(
|
|
454
|
-
|
|
491
|
+
order_id_value
|
|
492
|
+
if isinstance(order_id_value, str)
|
|
493
|
+
else None
|
|
455
494
|
)
|
|
456
495
|
enhanced_memo = (
|
|
457
496
|
f"{items_text}\n {order_link}"
|
|
@@ -514,7 +553,7 @@ def main():
|
|
|
514
553
|
else:
|
|
515
554
|
# --- SPLITTING ---
|
|
516
555
|
print("\n--- Splitting Transaction ---")
|
|
517
|
-
subtransactions = []
|
|
556
|
+
subtransactions: list[dict[str, Union[int, str, None]]] = []
|
|
518
557
|
remaining_milliunits = amount_milliunits
|
|
519
558
|
split_count = 1
|
|
520
559
|
|
|
@@ -524,12 +563,15 @@ def main():
|
|
|
524
563
|
)
|
|
525
564
|
|
|
526
565
|
# Show which item this split is for if we have matched order data
|
|
527
|
-
if
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
566
|
+
# Show which item this split is for if we have matched order data
|
|
567
|
+
items = []
|
|
568
|
+
if matching_order:
|
|
569
|
+
if isinstance(matching_order, Order):
|
|
570
|
+
items = matching_order.items or []
|
|
571
|
+
else:
|
|
572
|
+
items = matching_order.get("items", [])
|
|
573
|
+
|
|
574
|
+
if items:
|
|
533
575
|
if split_count <= len(items):
|
|
534
576
|
print(f"Item {split_count}: {items[split_count - 1]}")
|
|
535
577
|
else:
|
|
@@ -590,19 +632,26 @@ def main():
|
|
|
590
632
|
|
|
591
633
|
# --- ENHANCED SPLIT MEMO INPUT ---
|
|
592
634
|
# Generate memo for each split based on matched order data
|
|
593
|
-
|
|
635
|
+
split_memo: Optional[str] = None
|
|
636
|
+
suggested_split_memo: str = ""
|
|
594
637
|
|
|
595
638
|
# Use the already matched order if available
|
|
596
639
|
if matching_order:
|
|
597
640
|
print("Using matched order data for split memo...")
|
|
598
|
-
|
|
641
|
+
if isinstance(matching_order, Order):
|
|
642
|
+
items = matching_order.items
|
|
643
|
+
order_id = matching_order.order_id
|
|
644
|
+
else:
|
|
645
|
+
items = matching_order.get("items", [])
|
|
646
|
+
order_id = matching_order.get("order_id")
|
|
647
|
+
|
|
599
648
|
if split_count <= len(items):
|
|
600
649
|
# Use the specific item for this split
|
|
601
650
|
items_text = items[split_count - 1]
|
|
602
651
|
# Use extracted memo generator
|
|
603
652
|
memo_generator = MemoGenerator()
|
|
604
653
|
order_link = memo_generator.generate_amazon_order_link(
|
|
605
|
-
|
|
654
|
+
order_id
|
|
606
655
|
)
|
|
607
656
|
suggested_split_memo = (
|
|
608
657
|
f"{items_text}\n {order_link}"
|
|
@@ -652,15 +701,14 @@ def main():
|
|
|
652
701
|
print(
|
|
653
702
|
f"Enter optional memo for '{category_name}' split (multiline):"
|
|
654
703
|
)
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
split_memo = split_memo.strip()
|
|
704
|
+
split_memo_input = get_multiline_input_with_custom_submit(
|
|
705
|
+
"> "
|
|
706
|
+
)
|
|
707
|
+
split_memo = (
|
|
708
|
+
split_memo_input.strip()
|
|
709
|
+
if split_memo_input is not None
|
|
710
|
+
else ""
|
|
711
|
+
)
|
|
664
712
|
# --- END ENHANCED SPLIT MEMO INPUT ---
|
|
665
713
|
|
|
666
714
|
subtransactions.append(
|
|
@@ -695,7 +743,7 @@ def main():
|
|
|
695
743
|
"payee_name": payee,
|
|
696
744
|
"category_id": None, # Null for splits
|
|
697
745
|
"memo": generate_split_summary_memo(matching_order)
|
|
698
|
-
if matching_order
|
|
746
|
+
if matching_order and isinstance(matching_order, Order)
|
|
699
747
|
else original_memo, # Generate summary memo for splits
|
|
700
748
|
"cleared": t.get("cleared"),
|
|
701
749
|
"approved": True,
|
|
@@ -731,8 +779,7 @@ def main():
|
|
|
731
779
|
confirm = "y"
|
|
732
780
|
if confirm == "y":
|
|
733
781
|
if ynab_client.update_transaction(
|
|
734
|
-
transaction_id,
|
|
735
|
-
updated_payload_dict
|
|
782
|
+
transaction_id, updated_payload_dict
|
|
736
783
|
):
|
|
737
784
|
print("Update successful.")
|
|
738
785
|
break # Exit action loop, go to next transaction
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
5
6
|
|
|
6
7
|
from .exceptions import ConfigurationError
|
|
7
8
|
|
|
@@ -16,13 +17,13 @@ except ImportError:
|
|
|
16
17
|
class Config:
|
|
17
18
|
"""Configuration class for YNAB Amazon Categorizer."""
|
|
18
19
|
|
|
19
|
-
def __init__(self, api_key: str, budget_id: str, account_id: str = None):
|
|
20
|
+
def __init__(self, api_key: str, budget_id: str, account_id: Optional[str] = None):
|
|
20
21
|
self.api_key = api_key
|
|
21
22
|
self.budget_id = budget_id
|
|
22
23
|
self.account_id = account_id
|
|
23
24
|
|
|
24
25
|
@classmethod
|
|
25
|
-
def from_env(cls):
|
|
26
|
+
def from_env(cls) -> "Config":
|
|
26
27
|
# Load .env file if available
|
|
27
28
|
if DOTENV_AVAILABLE:
|
|
28
29
|
# Look for .env file in current directory and parent directories
|