ynab-cli 0.5.3__tar.gz → 0.6.0__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_cli-0.6.0/.github/.copilot-instructions.md +191 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/CHANGELOG.md +19 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/PKG-INFO +4 -2
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/pyproject.toml +15 -7
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/rules.py +21 -8
- ynab_cli-0.6.0/src/ynab_cli/domain/use_cases/budgets.py +45 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/categories.py +36 -20
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/payees.py +57 -45
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/transactions.py +16 -10
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/cli.py +1 -1
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/budgets.py +6 -3
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/categories.py +11 -4
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/payees.py +22 -6
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/transactions.py +7 -2
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/app.py +18 -45
- ynab_cli-0.6.0/src/ynab_cli/host/textual/app.tcss +67 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/cli.py +6 -2
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories/list_all.py +33 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories/list_unused.py +33 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories/tabs.py +33 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/command_widget.py +61 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/dialog.py +2 -2
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/screen.py +19 -4
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/list_all.py +29 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +35 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/list_unused.py +61 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +33 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/tabs.py +43 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/tabs.py +37 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +108 -0
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/transactions/tabs.py +28 -0
- ynab_cli-0.6.0/tests/conftest.py +13 -0
- ynab_cli-0.6.0/tests/factories/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/tests/factories/ynab.py +1 -1
- ynab_cli-0.6.0/tests/unit/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/adapters/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/adapters/rich/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/adapters/rich/test_io.py +128 -0
- ynab_cli-0.6.0/tests/unit/adapters/textual/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/adapters/textual/test_io.py +308 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/tests/unit/conftest.py +4 -4
- ynab_cli-0.6.0/tests/unit/domain/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/domain/models/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/domain/models/test_rules.py +306 -0
- ynab_cli-0.6.0/tests/unit/domain/ports/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/domain/ports/test_io.py +502 -0
- ynab_cli-0.6.0/tests/unit/domain/use_cases/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/domain/use_cases/test_budgets.py +62 -0
- ynab_cli-0.6.0/tests/unit/domain/use_cases/test_categories.py +258 -0
- ynab_cli-0.6.0/tests/unit/domain/use_cases/test_payees.py +366 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/tests/unit/domain/use_cases/test_transactions.py +78 -4
- ynab_cli-0.6.0/tests/unit/host/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/host/click/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/host/click/commands/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/host/click/commands/test_budgets.py +28 -0
- ynab_cli-0.6.0/tests/unit/host/click/commands/test_categories.py +66 -0
- ynab_cli-0.6.0/tests/unit/host/click/commands/test_payees.py +108 -0
- ynab_cli-0.6.0/tests/unit/host/click/commands/test_transactions.py +67 -0
- ynab_cli-0.6.0/tests/unit/host/test_cli.py +35 -0
- ynab_cli-0.6.0/tests/unit/host/textual/__init__.py +0 -0
- ynab_cli-0.6.0/tests/unit/host/textual/test_app.py +44 -0
- ynab_cli-0.6.0/tests/unit/host/textual/test_cli.py +21 -0
- ynab_cli-0.6.0/uv.lock +1766 -0
- ynab_cli-0.5.3/src/ynab_cli/domain/use_cases/budgets.py +0 -37
- ynab_cli-0.5.3/src/ynab_cli/host/textual/app.tcss +0 -96
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/categories.py +0 -91
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -47
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -10
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -11
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/payees.py +0 -153
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/transactions.py +0 -90
- ynab_cli-0.5.3/tests/unit/domain/use_cases/test_categories.py +0 -68
- ynab_cli-0.5.3/tests/unit/domain/use_cases/test_payees.py +0 -72
- ynab_cli-0.5.3/uv.lock +0 -1659
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.env-sample +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/FUNDING.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/dependabot.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/workflows/pr.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.gitignore +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.python-version +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/LICENSE.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/Makefile +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/README.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/docs/index.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/docs/transaction_rules.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/mkdocs.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/io.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/io.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/client.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/errors.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/types.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/util.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/constants.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/io.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/settings.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/cli.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/constants.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/__init__.py +0 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets}/__init__.py +0 -0
- {ynab_cli-0.5.3/tests → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories}/__init__.py +0 -0
- {ynab_cli-0.5.3/tests/factories → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common}/__init__.py +0 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/__init__.py +0 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/form.py +0 -0
- {ynab_cli-0.5.3/tests/unit → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees}/__init__.py +0 -0
- {ynab_cli-0.5.3/tests/unit/domain → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/transactions}/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/py.typed +0 -0
- {ynab_cli-0.5.3/tests/unit/domain/use_cases → ynab_cli-0.6.0/tests}/__init__.py +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# YNAB CLI - Copilot Instructions
|
|
2
|
+
|
|
3
|
+
## Project Overview
|
|
4
|
+
|
|
5
|
+
This is a command-line interface for interacting with YNAB (You Need A Budget) written in Python. The project follows clean architecture principles with ports and adapters pattern.
|
|
6
|
+
|
|
7
|
+
## Architecture
|
|
8
|
+
|
|
9
|
+
The project uses a layered architecture:
|
|
10
|
+
|
|
11
|
+
- `src/ynab_cli/domain/`: Core business logic, models, and ports (interfaces)
|
|
12
|
+
- `src/ynab_cli/adapters/`: External adapters (YNAB API, Rich UI, Textual TUI)
|
|
13
|
+
- `src/ynab_cli/host/`: Application entry points and framework integration (Click CLI, Textual app)
|
|
14
|
+
|
|
15
|
+
### Key Patterns
|
|
16
|
+
|
|
17
|
+
1. **Ports and Adapters**: Domain defines protocols/interfaces, adapters implement them
|
|
18
|
+
2. **Dependency Injection**: Dependencies are injected through constructors
|
|
19
|
+
3. **Async/Await**: Most operations are async for better I/O handling
|
|
20
|
+
4. **Type Safety**: Heavy use of type hints and protocols
|
|
21
|
+
|
|
22
|
+
## Coding Standards
|
|
23
|
+
|
|
24
|
+
### Python Version & Dependencies
|
|
25
|
+
- Target Python 3.11+
|
|
26
|
+
- Use `uv` for dependency management
|
|
27
|
+
- All dependencies defined in `pyproject.toml`
|
|
28
|
+
|
|
29
|
+
### Code Style
|
|
30
|
+
- Follow PEP 8
|
|
31
|
+
- Use type hints for all function parameters and return values
|
|
32
|
+
- Prefer `typing_extensions` imports for newer typing features
|
|
33
|
+
- Use `attrs` for data classes with `@define` decorator
|
|
34
|
+
- Use `@override` decorator from `typing_extensions` when overriding methods in protocols or base classes
|
|
35
|
+
|
|
36
|
+
### Async Patterns
|
|
37
|
+
- Most I/O operations should be async
|
|
38
|
+
- Use `anyio` for async utilities when needed
|
|
39
|
+
- Progress tracking should be async-compatible
|
|
40
|
+
|
|
41
|
+
### Error Handling
|
|
42
|
+
- Use proper exception types
|
|
43
|
+
- Handle API errors gracefully
|
|
44
|
+
- Provide meaningful error messages to users
|
|
45
|
+
|
|
46
|
+
## Testing Guidelines
|
|
47
|
+
|
|
48
|
+
### Test Structure
|
|
49
|
+
- Unit tests in `tests/unit/` mirroring `src/` structure
|
|
50
|
+
- Use pytest as the testing framework
|
|
51
|
+
- All test functions should have proper type annotations
|
|
52
|
+
- Test classes should be named `Test<ClassName>`
|
|
53
|
+
- Test methods should be descriptive: `test_<method_name>_<scenario>`
|
|
54
|
+
|
|
55
|
+
### Mocking
|
|
56
|
+
- Use `unittest.mock` for mocking external dependencies
|
|
57
|
+
- Mock at the adapter level, not domain level
|
|
58
|
+
- Use `MagicMock` for complex objects, `Mock` for simple ones
|
|
59
|
+
|
|
60
|
+
### Test Patterns
|
|
61
|
+
```python
|
|
62
|
+
from typing import Any
|
|
63
|
+
from unittest.mock import MagicMock, Mock
|
|
64
|
+
import pytest
|
|
65
|
+
|
|
66
|
+
class TestMyClass:
|
|
67
|
+
def test_method_name_success_scenario(self) -> None:
|
|
68
|
+
"""Test description of what is being tested."""
|
|
69
|
+
# Arrange
|
|
70
|
+
mock_dependency = Mock()
|
|
71
|
+
instance = MyClass(mock_dependency)
|
|
72
|
+
|
|
73
|
+
# Act
|
|
74
|
+
result = instance.method_name()
|
|
75
|
+
|
|
76
|
+
# Assert
|
|
77
|
+
assert result == expected_value
|
|
78
|
+
mock_dependency.some_method.assert_called_once()
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Domain Models
|
|
82
|
+
|
|
83
|
+
### Data Classes
|
|
84
|
+
- Use `@attrs.define` for data classes
|
|
85
|
+
- Implement `to_dict()` and `from_dict()` for serialization when needed
|
|
86
|
+
- Use proper type hints including `Self` for class methods
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from typing import Any, Self
|
|
90
|
+
from attrs import define
|
|
91
|
+
|
|
92
|
+
@define
|
|
93
|
+
class MyModel:
|
|
94
|
+
field: str
|
|
95
|
+
optional_field: int | None = None
|
|
96
|
+
|
|
97
|
+
def to_dict(self) -> dict[str, Any]:
|
|
98
|
+
return {"field": self.field, "optional_field": self.optional_field}
|
|
99
|
+
|
|
100
|
+
@classmethod
|
|
101
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
102
|
+
return cls(field=data["field"], optional_field=data.get("optional_field"))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Protocols
|
|
106
|
+
- Define interfaces as protocols in `domain/ports/`
|
|
107
|
+
- Use `typing.Protocol` for structural typing
|
|
108
|
+
- Mark implementation methods with `@override`
|
|
109
|
+
|
|
110
|
+
## UI Frameworks
|
|
111
|
+
|
|
112
|
+
### Rich (Progress & Prompts)
|
|
113
|
+
- Used for CLI progress bars and user prompts
|
|
114
|
+
- Implement `Progress` and `IO` protocols
|
|
115
|
+
- Handle console stop/start for prompts
|
|
116
|
+
|
|
117
|
+
### Textual (TUI)
|
|
118
|
+
- Used for interactive terminal UI
|
|
119
|
+
- Follow Textual app patterns
|
|
120
|
+
- Use proper CSS for styling
|
|
121
|
+
- Handle async operations properly
|
|
122
|
+
|
|
123
|
+
### Click (CLI Framework)
|
|
124
|
+
- Use Click for command-line interface
|
|
125
|
+
- Use `click-default-group` for default commands
|
|
126
|
+
- Implement proper help text and options
|
|
127
|
+
- Use context objects for passing settings
|
|
128
|
+
|
|
129
|
+
## API Integration
|
|
130
|
+
|
|
131
|
+
### YNAB API
|
|
132
|
+
- All API models should be in `adapters/ynab/models/`
|
|
133
|
+
- Use proper error handling for HTTP requests
|
|
134
|
+
- Implement rate limiting if needed
|
|
135
|
+
- Use `httpx` for HTTP client
|
|
136
|
+
|
|
137
|
+
## File Organization
|
|
138
|
+
|
|
139
|
+
### Import Order
|
|
140
|
+
1. Standard library imports
|
|
141
|
+
2. Third-party imports
|
|
142
|
+
3. Local imports (domain, then adapters, then host)
|
|
143
|
+
|
|
144
|
+
### Module Structure
|
|
145
|
+
- Keep modules focused and cohesive
|
|
146
|
+
- Use `__init__.py` for public APIs
|
|
147
|
+
- Private modules should have underscore prefix
|
|
148
|
+
|
|
149
|
+
## Common Patterns
|
|
150
|
+
|
|
151
|
+
### Progress Tracking
|
|
152
|
+
```python
|
|
153
|
+
async def long_running_operation(io: IO) -> None:
|
|
154
|
+
await io.progress.update(total=100)
|
|
155
|
+
for i in range(100):
|
|
156
|
+
# Do work
|
|
157
|
+
await io.progress.update(advance=1)
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### Settings & Configuration
|
|
161
|
+
- Use environment variables with `python-dotenv`
|
|
162
|
+
- Settings objects should be immutable
|
|
163
|
+
- Pass settings through dependency injection
|
|
164
|
+
|
|
165
|
+
### Error Messages
|
|
166
|
+
- Provide actionable error messages
|
|
167
|
+
- Include context about what was being attempted
|
|
168
|
+
- Use appropriate log levels
|
|
169
|
+
|
|
170
|
+
## Development Workflow
|
|
171
|
+
|
|
172
|
+
1. Create feature branches from main
|
|
173
|
+
2. Write tests first (TDD when possible)
|
|
174
|
+
3. Implement functionality
|
|
175
|
+
4. Ensure all tests pass
|
|
176
|
+
5. Run linting and formatting (`make lint`)
|
|
177
|
+
6. Create pull request with descriptive title and description
|
|
178
|
+
|
|
179
|
+
## Performance Considerations
|
|
180
|
+
|
|
181
|
+
- Use async I/O for network operations
|
|
182
|
+
- Implement progress tracking for long operations
|
|
183
|
+
- Consider memory usage for large datasets
|
|
184
|
+
- Use appropriate data structures for lookups
|
|
185
|
+
|
|
186
|
+
## Security
|
|
187
|
+
|
|
188
|
+
- Never log sensitive data (API keys, personal info)
|
|
189
|
+
- Use environment variables for secrets
|
|
190
|
+
- Validate user inputs
|
|
191
|
+
- Handle API rate limits gracefully
|
|
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## v0.6.0 (2025-06-28)
|
|
9
|
+
|
|
10
|
+
### Feat
|
|
11
|
+
|
|
12
|
+
- use anyio and uvloop
|
|
13
|
+
|
|
14
|
+
### Fix
|
|
15
|
+
|
|
16
|
+
- **tests**: adding moar tests
|
|
17
|
+
- **tests**: added tests for a bunch of things
|
|
18
|
+
- **deps**: bump textual from 3.3.0 to 3.4.0
|
|
19
|
+
- **tui**: fix some ui issues and clean up some code
|
|
20
|
+
|
|
21
|
+
## v0.5.4 (2025-06-10)
|
|
22
|
+
|
|
23
|
+
### Fix
|
|
24
|
+
|
|
25
|
+
- **tui**: added some command parameter forms
|
|
26
|
+
|
|
8
27
|
## v0.5.3 (2025-06-09)
|
|
9
28
|
|
|
10
29
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ynab-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: YNAB Command Line Interface
|
|
5
5
|
Project-URL: Changelog, https://github.com/jeffsawatzky/ynab-cli/blob/main/CHANGELOG.md
|
|
6
6
|
Project-URL: Documentation, https://jeffsawatzky.github.io/ynab-cli/
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
19
19
|
Classifier: Topic :: Office/Business :: Financial
|
|
20
20
|
Classifier: Topic :: Utilities
|
|
21
21
|
Requires-Python: >=3.11
|
|
22
|
+
Requires-Dist: anyio~=4.9
|
|
22
23
|
Requires-Dist: attrs~=25.3
|
|
23
24
|
Requires-Dist: click-default-group~=1.2
|
|
24
25
|
Requires-Dist: click~=8.2
|
|
@@ -28,8 +29,9 @@ Requires-Dist: rapidfuzz~=3.13
|
|
|
28
29
|
Requires-Dist: rich~=14.0
|
|
29
30
|
Requires-Dist: rule-engine~=4.5
|
|
30
31
|
Requires-Dist: textual-fspicker~=0.4
|
|
31
|
-
Requires-Dist: textual~=3.
|
|
32
|
+
Requires-Dist: textual~=3.5
|
|
32
33
|
Requires-Dist: typing-extensions~=4.14
|
|
34
|
+
Requires-Dist: uvloop~=0.21
|
|
33
35
|
Description-Content-Type: text/markdown
|
|
34
36
|
|
|
35
37
|
# YNAB CLI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ynab-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "YNAB Command Line Interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -33,6 +33,7 @@ classifiers = [
|
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
dependencies = [
|
|
36
|
+
"anyio~=4.9",
|
|
36
37
|
"attrs~=25.3",
|
|
37
38
|
"click-default-group~=1.2",
|
|
38
39
|
"click~=8.2",
|
|
@@ -42,8 +43,9 @@ dependencies = [
|
|
|
42
43
|
"rich~=14.0",
|
|
43
44
|
"rule-engine~=4.5",
|
|
44
45
|
"textual-fspicker~=0.4",
|
|
45
|
-
"textual~=3.
|
|
46
|
+
"textual~=3.5",
|
|
46
47
|
"typing-extensions~=4.14",
|
|
48
|
+
"uvloop~=0.21",
|
|
47
49
|
]
|
|
48
50
|
|
|
49
51
|
[project.urls]
|
|
@@ -59,18 +61,18 @@ ynab-cli = "ynab_cli.host.cli:main"
|
|
|
59
61
|
|
|
60
62
|
[dependency-groups]
|
|
61
63
|
dev = [
|
|
62
|
-
"commitizen>=4.8.
|
|
64
|
+
"commitizen>=4.8.3",
|
|
63
65
|
"factory-boy>=3.3.3",
|
|
64
66
|
"mkdocs-click>=0.9.0",
|
|
65
67
|
"mkdocs-material>=9.6.14",
|
|
66
68
|
"mkdocs>=1.6.1",
|
|
67
69
|
"mypy>=1.16.0",
|
|
68
|
-
"pytest-
|
|
69
|
-
"pytest-cov>=6.1.1",
|
|
70
|
+
"pytest-cov>=6.2.1",
|
|
70
71
|
"pytest-mock>=3.14.1",
|
|
71
|
-
"pytest>=8.4.
|
|
72
|
-
"ruff>=0.
|
|
72
|
+
"pytest>=8.4.1",
|
|
73
|
+
"ruff>=0.12.1",
|
|
73
74
|
"textual-dev>=1.7.0",
|
|
75
|
+
"trio>=0.30.0",
|
|
74
76
|
"types-python-dateutil>=2.9.0.20250516",
|
|
75
77
|
]
|
|
76
78
|
|
|
@@ -136,6 +138,12 @@ max-complexity = 70
|
|
|
136
138
|
builtins-ignorelist = ["type", "id"]
|
|
137
139
|
|
|
138
140
|
|
|
141
|
+
[tool.coverage.run]
|
|
142
|
+
omit = [
|
|
143
|
+
"src/ynab_cli/adapters/ynab/**/*",
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
|
|
139
147
|
[tool.commitizen]
|
|
140
148
|
name = "cz_conventional_commits"
|
|
141
149
|
annotated_tag = true
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any, Self
|
|
2
2
|
|
|
3
3
|
from attrs import define as _attrs_define
|
|
4
4
|
|
|
@@ -18,20 +18,33 @@ class TransactionRule:
|
|
|
18
18
|
# See: https://api.ynab.com/v1#/Transactions/updateTransactions
|
|
19
19
|
patch: models.SaveTransactionWithIdOrImportId | None = None
|
|
20
20
|
|
|
21
|
+
def to_dict(self) -> dict[str, Any]:
|
|
22
|
+
return {
|
|
23
|
+
"rules": self.rules,
|
|
24
|
+
"patch": self.patch.to_dict() if self.patch else None,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
29
|
+
rules = [str(r) for r in data.get("rules", [])]
|
|
30
|
+
patch = None
|
|
31
|
+
if "patch" in data:
|
|
32
|
+
patch = models.SaveTransactionWithIdOrImportId.from_dict(data["patch"])
|
|
33
|
+
|
|
34
|
+
return cls(rules=rules, patch=patch)
|
|
35
|
+
|
|
21
36
|
|
|
22
37
|
@_attrs_define
|
|
23
38
|
class TransactionRules:
|
|
24
39
|
transaction_rules: list[TransactionRule]
|
|
25
40
|
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return {"transaction_rules": [rule.to_dict() for rule in self.transaction_rules]}
|
|
43
|
+
|
|
26
44
|
@classmethod
|
|
27
|
-
def from_dict(cls, data: dict[str, Any]) ->
|
|
45
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
28
46
|
transaction_rules = []
|
|
29
47
|
for rule in data.get("transaction_rules", []):
|
|
30
|
-
|
|
31
|
-
patch = None
|
|
32
|
-
if "patch" in rule:
|
|
33
|
-
patch = models.SaveTransactionWithIdOrImportId.from_dict(rule["patch"])
|
|
34
|
-
|
|
35
|
-
transaction_rules.append(TransactionRule(rules=rules, patch=patch))
|
|
48
|
+
transaction_rules.append(TransactionRule.from_dict(rule))
|
|
36
49
|
|
|
37
50
|
return cls(transaction_rules=transaction_rules)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
|
|
4
|
+
from ynab_cli.adapters import ynab
|
|
5
|
+
from ynab_cli.adapters.ynab import models, util
|
|
6
|
+
from ynab_cli.adapters.ynab.api.budgets import get_budgets
|
|
7
|
+
from ynab_cli.domain import ports
|
|
8
|
+
from ynab_cli.domain.constants import YNAB_API_URL
|
|
9
|
+
from ynab_cli.domain.settings import Settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ListAllParams(TypedDict):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def list_all(
|
|
17
|
+
settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
|
|
18
|
+
) -> AsyncIterator[models.BudgetSummary]:
|
|
19
|
+
try:
|
|
20
|
+
progress_total = 0
|
|
21
|
+
|
|
22
|
+
if client is None:
|
|
23
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
24
|
+
|
|
25
|
+
async with client:
|
|
26
|
+
budgets = (
|
|
27
|
+
await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=False, client=client)
|
|
28
|
+
).data.budgets
|
|
29
|
+
budgets.sort(key=lambda b: b.name)
|
|
30
|
+
|
|
31
|
+
progress_total = len(budgets)
|
|
32
|
+
await io.progress.update(total=progress_total)
|
|
33
|
+
for budget in budgets:
|
|
34
|
+
await io.progress.update(advance=1)
|
|
35
|
+
yield budget
|
|
36
|
+
|
|
37
|
+
except Exception as e:
|
|
38
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
39
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
40
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
41
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
42
|
+
else:
|
|
43
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
44
|
+
finally:
|
|
45
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -20,10 +20,16 @@ class ListUnusedParams(TypedDict):
|
|
|
20
20
|
pass
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
async def list_unused(
|
|
24
|
-
|
|
23
|
+
async def list_unused(
|
|
24
|
+
settings: Settings, io: ports.IO, params: ListUnusedParams, *, client: ynab.AuthenticatedClient | None = None
|
|
25
|
+
) -> AsyncIterator[models.Category]:
|
|
26
|
+
try:
|
|
25
27
|
progress_total = 0
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
if client is None:
|
|
30
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
31
|
+
|
|
32
|
+
async with client:
|
|
27
33
|
category_groups = (
|
|
28
34
|
await util.get_asyncio_detailed(
|
|
29
35
|
io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=client
|
|
@@ -63,23 +69,31 @@ async def list_unused(settings: Settings, io: ports.IO, params: ListUnusedParams
|
|
|
63
69
|
if not num_transactions:
|
|
64
70
|
yield category
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await io.
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
74
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
75
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
76
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
77
|
+
else:
|
|
78
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
79
|
+
finally:
|
|
80
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
73
81
|
|
|
74
82
|
|
|
75
83
|
class ListAllParams(TypedDict):
|
|
76
84
|
pass
|
|
77
85
|
|
|
78
86
|
|
|
79
|
-
async def list_all(
|
|
80
|
-
|
|
87
|
+
async def list_all(
|
|
88
|
+
settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
|
|
89
|
+
) -> AsyncIterator[models.Category]:
|
|
90
|
+
try:
|
|
81
91
|
progress_total = 0
|
|
82
|
-
|
|
92
|
+
|
|
93
|
+
if client is None:
|
|
94
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
95
|
+
|
|
96
|
+
async with client:
|
|
83
97
|
category_groups = (
|
|
84
98
|
await util.get_asyncio_detailed(
|
|
85
99
|
io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=client
|
|
@@ -106,10 +120,12 @@ async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> A
|
|
|
106
120
|
|
|
107
121
|
yield category
|
|
108
122
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await io.
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
125
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
126
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
127
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
128
|
+
else:
|
|
129
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
130
|
+
finally:
|
|
131
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -47,13 +47,13 @@ class NormalizeNamesParams(TypedDict):
|
|
|
47
47
|
async def normalize_names(
|
|
48
48
|
settings: Settings, io: ports.IO, params: NormalizeNamesParams, *, client: ynab.AuthenticatedClient | None = None
|
|
49
49
|
) -> AsyncIterator[tuple[models.Payee, str]]:
|
|
50
|
-
|
|
51
|
-
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
52
|
-
|
|
53
|
-
async with client:
|
|
50
|
+
try:
|
|
54
51
|
progress_total = 0
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
if client is None:
|
|
54
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
55
|
+
|
|
56
|
+
async with client:
|
|
57
57
|
payees = (
|
|
58
58
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
59
59
|
).data.payees
|
|
@@ -81,13 +81,15 @@ async def normalize_names(
|
|
|
81
81
|
body=models.PatchPayeeWrapper(payee=models.SavePayee(name=normalized_name)),
|
|
82
82
|
)
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await io.
|
|
84
|
+
except Exception as e:
|
|
85
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
86
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
87
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
88
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
89
|
+
else:
|
|
90
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
91
|
+
finally:
|
|
92
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
class ListDuplicatesParams(TypedDict):
|
|
@@ -97,13 +99,13 @@ class ListDuplicatesParams(TypedDict):
|
|
|
97
99
|
async def list_duplicates(
|
|
98
100
|
settings: Settings, io: ports.IO, params: ListDuplicatesParams, *, client: ynab.AuthenticatedClient | None = None
|
|
99
101
|
) -> AsyncIterator[tuple[models.Payee, models.Payee]]:
|
|
100
|
-
|
|
101
|
-
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
102
|
-
|
|
103
|
-
async with client:
|
|
102
|
+
try:
|
|
104
103
|
progress_total = 0
|
|
105
104
|
|
|
106
|
-
|
|
105
|
+
if client is None:
|
|
106
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
107
|
+
|
|
108
|
+
async with client:
|
|
107
109
|
possible_duplicates: dict[tuple[UUID, str], list[tuple[UUID, str]]] = {}
|
|
108
110
|
|
|
109
111
|
payees = (
|
|
@@ -142,13 +144,15 @@ async def list_duplicates(
|
|
|
142
144
|
|
|
143
145
|
yield (payee, filtered_payee)
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await io.
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
149
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
150
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
151
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
152
|
+
else:
|
|
153
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
154
|
+
finally:
|
|
155
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
152
156
|
|
|
153
157
|
|
|
154
158
|
class ListUnusedParams(TypedDict):
|
|
@@ -158,13 +162,13 @@ class ListUnusedParams(TypedDict):
|
|
|
158
162
|
async def list_unused(
|
|
159
163
|
settings: Settings, io: ports.IO, params: ListUnusedParams, *, client: ynab.AuthenticatedClient | None = None
|
|
160
164
|
) -> AsyncIterator[models.Payee]:
|
|
161
|
-
|
|
162
|
-
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
163
|
-
|
|
164
|
-
async with client:
|
|
165
|
+
try:
|
|
165
166
|
progress_total = 0
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
if client is None:
|
|
169
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
170
|
+
|
|
171
|
+
async with client:
|
|
168
172
|
payees = (
|
|
169
173
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
170
174
|
).data.payees
|
|
@@ -205,11 +209,15 @@ async def list_unused(
|
|
|
205
209
|
body=models.PatchPayeeWrapper(payee=models.SavePayee(name=new_name)),
|
|
206
210
|
)
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
except Exception as e:
|
|
213
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
214
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
215
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
216
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
217
|
+
else:
|
|
218
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
219
|
+
finally:
|
|
220
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
213
221
|
|
|
214
222
|
|
|
215
223
|
class ListAllParams(TypedDict):
|
|
@@ -219,11 +227,13 @@ class ListAllParams(TypedDict):
|
|
|
219
227
|
async def list_all(
|
|
220
228
|
settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
|
|
221
229
|
) -> AsyncIterator[models.Payee]:
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
try:
|
|
231
|
+
progress_total = 0
|
|
232
|
+
|
|
233
|
+
if client is None:
|
|
234
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
224
235
|
|
|
225
|
-
|
|
226
|
-
try:
|
|
236
|
+
async with client:
|
|
227
237
|
payees = (
|
|
228
238
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
229
239
|
).data.payees
|
|
@@ -239,10 +249,12 @@ async def list_all(
|
|
|
239
249
|
|
|
240
250
|
yield payee
|
|
241
251
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await io.
|
|
252
|
+
except Exception as e:
|
|
253
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
254
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
255
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
256
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
257
|
+
else:
|
|
258
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
259
|
+
finally:
|
|
260
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|