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.
Files changed (43) hide show
  1. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.github/workflows/release.yml +36 -5
  2. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.gitignore +7 -1
  3. ynab_amazon_categorizer-2.1.2/.pre-commit-config.yaml +10 -0
  4. {ynab_amazon_categorizer-2.0.2/src/ynab_amazon_categorizer.egg-info → ynab_amazon_categorizer-2.1.2}/PKG-INFO +4 -5
  5. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/README.md +1 -1
  6. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/pyproject.toml +10 -6
  7. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/__init__.py +4 -1
  8. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/_version.py +3 -3
  9. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/amazon_parser.py +16 -9
  10. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/cli.py +111 -64
  11. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/config.py +3 -2
  12. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/exceptions.py +1 -0
  13. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/memo_generator.py +10 -3
  14. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/transaction_matcher.py +13 -3
  15. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/ynab_client.py +8 -4
  16. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2/src/ynab_amazon_categorizer.egg-info}/PKG-INFO +4 -5
  17. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/SOURCES.txt +0 -2
  18. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_amazon_parser.py +3 -3
  19. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_amazon_parser_real.py +5 -5
  20. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_config.py +5 -3
  21. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_memo_generator.py +4 -4
  22. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_transaction_matcher.py +18 -9
  23. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/test_ynab_client.py +9 -8
  24. ynab_amazon_categorizer-2.0.2/.pre-commit-config.yaml +0 -28
  25. ynab_amazon_categorizer-2.0.2/install.bat +0 -81
  26. ynab_amazon_categorizer-2.0.2/ynab_amazon_categorizer.py.backup +0 -1020
  27. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.env.example +0 -0
  28. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.git_archival.txt +0 -0
  29. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/.gitattributes +0 -0
  30. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/ARCHITECTURE.md +0 -0
  31. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/CLAUDE.md +0 -0
  32. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/CONTEXT.md +0 -0
  33. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/DEVELOPMENT.md +0 -0
  34. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/LICENSE +0 -0
  35. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/PROGRESS.md +0 -0
  36. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/TODO.md +0 -0
  37. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/setup.cfg +0 -0
  38. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer/__main__.py +0 -0
  39. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/dependency_links.txt +0 -0
  40. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/entry_points.txt +0 -0
  41. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/requires.txt +0 -0
  42. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/src/ynab_amazon_categorizer.egg-info/top_level.txt +0 -0
  43. {ynab_amazon_categorizer-2.0.2 → ynab_amazon_categorizer-2.1.2}/tests/__init__.py +0 -0
@@ -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: Test package can be imported and run
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 -c "from ynab_amazon_categorizer.cli import main; print('Package imports successfully')"
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: [test, publish]
95
+ needs: [publish]
65
96
  runs-on: ubuntu-latest
66
97
  permissions:
67
98
  contents: write
@@ -48,4 +48,10 @@ Thumbs.db
48
48
  RELEASE_NOTES.md
49
49
  temp_release.json
50
50
  release/
51
- .claude/
51
+ .claude/
52
+
53
+ # Backups
54
+ ynab_amazon_categorizer.py.backup
55
+
56
+ # Auto-generated files
57
+ src/ynab_amazon_categorizer/_version.py
@@ -0,0 +1,10 @@
1
+ repos:
2
+ - repo: https://github.com/astral-sh/ruff-pre-commit
3
+ # Ruff version.
4
+ rev: v0.13.2
5
+ hooks:
6
+ # Run the linter.
7
+ - id: ruff-check
8
+ args: [ --fix ]
9
+ # Run the formatter.
10
+ - id: ruff-format
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ynab-amazon-categorizer
3
- Version: 2.0.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.7
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
- An enhanced Python package that automatically categorizes Amazon transactions in YNAB (You Need A Budget) with rich item information and automatic memo generation.
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
- An enhanced Python package that automatically categorizes Amazon transactions in YNAB (You Need A Budget) with rich item information and automatic memo generation.
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.7"
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 = ["py37"]
97
+ target-version = ["py39"]
99
98
  include = '\\.pyi?$'
100
99
 
101
100
  [tool.ruff]
102
- target-version = "py37"
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.7"
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 PackageNotFoundError, version
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.0.2'
32
- __version_tuple__ = version_tuple = (2, 0, 2)
31
+ __version__ = version = '2.1.2'
32
+ __version_tuple__ = version_tuple = (2, 1, 2)
33
33
 
34
- __commit_id__ = commit_id = 'g24166dc0b'
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
- def __init__(self):
8
- self.order_id = None
9
- self.total = None
10
- self.date_str = None
11
- self.items = []
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(order_pattern, orders_text, re.DOTALL | re.IGNORECASE)
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(r"Order placed", orders_text[start_pos:], re.IGNORECASE)
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")
@@ -1,15 +1,16 @@
1
1
  import json
2
2
  import os # For history file path
3
- import re # For order ID extraction
4
- from datetime import datetime
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(prompt_message="Enter multiline text: "):
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(self, document, complete_event):
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(category_completer, name_to_id_map):
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
- memo_history_file = os.path.join(
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
- print(f" Order ID: {matching_order.order_id}")
367
- print(f" Total: ${getattr(matching_order, 'total', 'N/A')}")
368
- print(f" Date: {getattr(matching_order, 'date_str', 'N/A')}")
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 hasattr(matching_order, "items") and matching_order.items:
392
+ if items:
376
393
  print(" Items:")
377
- for item in matching_order.items:
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
- matching_order
402
- and hasattr(matching_order, "items")
403
- and matching_order.items
404
- and len(matching_order.items) > 1
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
- item_details = {
428
- "order_id": matching_order.order_id,
429
- "items": getattr(matching_order, "items", []),
430
- "total": getattr(matching_order, "total", None),
431
- "date": getattr(matching_order, "date_str", None),
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
- item_details["items"][0]
448
- if item_details["items"]
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
- item_details["order_id"]
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
- matching_order
529
- and hasattr(matching_order, "items")
530
- and matching_order.items
531
- ):
532
- items = matching_order.items
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
- suggested_split_memo = ""
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
- items = getattr(matching_order, "items", [])
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
- matching_order.order_id
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
- split_memo = get_multiline_input_with_custom_submit("> ")
656
- if split_memo is None:
657
- split_memo = ""
658
- else:
659
- split_memo = split_memo.strip()
660
- if split_memo is None:
661
- split_memo = ""
662
- else:
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
@@ -3,4 +3,5 @@
3
3
 
4
4
  class ConfigurationError(Exception):
5
5
  """Raised when configuration is invalid or missing."""
6
+
6
7
  pass