ynab-cli 0.5.4__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.4 → ynab_cli-0.6.0}/CHANGELOG.md +13 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/PKG-INFO +4 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/pyproject.toml +15 -7
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/budgets.py +2 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/categories.py +2 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/payees.py +4 -4
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/transactions.py +1 -1
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/cli.py +1 -1
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/budgets.py +6 -3
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/categories.py +11 -4
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/payees.py +22 -6
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/transactions.py +7 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/app.py +15 -36
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/app.tcss +11 -11
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/cli.py +6 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/list_all.py +4 -9
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/list_unused.py +4 -9
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/tabs.py +12 -6
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/command_widget.py +61 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/screen.py +4 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/list_all.py +4 -7
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +4 -7
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/list_unused.py +7 -20
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +4 -7
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/tabs.py +14 -8
- ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/tabs.py +37 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +8 -18
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/transactions/tabs.py +11 -5
- ynab_cli-0.6.0/tests/conftest.py +13 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/factories/ynab.py +1 -1
- 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.4 → 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.4 → 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.4/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -47
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -11
- ynab_cli-0.5.4/tests/unit/domain/use_cases/test_categories.py +0 -68
- ynab_cli-0.5.4/tests/unit/domain/use_cases/test_payees.py +0 -72
- ynab_cli-0.5.4/uv.lock +0 -1659
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.env-sample +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/FUNDING.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/dependabot.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/workflows/pr.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/workflows/publish-to-pypi.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.gitignore +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.python-version +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/LICENSE.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/Makefile +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/README.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/docs/index.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/docs/transaction_rules.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/mkdocs.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/io.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/io.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/client.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/errors.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
- {ynab_cli-0.5.4 → 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.4 → 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.4 → 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.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/types.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/util.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/constants.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/rules.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/io.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/settings.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/cli.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/constants.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/dialog.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/form.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/transactions/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/py.typed +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/factories/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/unit/__init__.py +0 -0
- {ynab_cli-0.5.4/tests/unit/domain → ynab_cli-0.6.0/tests/unit/adapters}/__init__.py +0 -0
- {ynab_cli-0.5.4/tests/unit/domain/use_cases → ynab_cli-0.6.0/tests/unit/adapters/rich}/__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,19 @@ 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
|
+
|
|
8
21
|
## v0.5.4 (2025-06-10)
|
|
9
22
|
|
|
10
23
|
### 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
|
|
@@ -24,7 +24,7 @@ async def list_all(
|
|
|
24
24
|
|
|
25
25
|
async with client:
|
|
26
26
|
budgets = (
|
|
27
|
-
await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=
|
|
27
|
+
await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=False, client=client)
|
|
28
28
|
).data.budgets
|
|
29
29
|
budgets.sort(key=lambda b: b.name)
|
|
30
30
|
|
|
@@ -40,6 +40,6 @@ async def list_all(
|
|
|
40
40
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
41
41
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
42
42
|
else:
|
|
43
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
43
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
44
44
|
finally:
|
|
45
45
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -75,7 +75,7 @@ async def list_unused(
|
|
|
75
75
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
76
76
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
77
77
|
else:
|
|
78
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
78
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
79
79
|
finally:
|
|
80
80
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
81
81
|
|
|
@@ -126,6 +126,6 @@ async def list_all(
|
|
|
126
126
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
127
127
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
128
128
|
else:
|
|
129
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
129
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
130
130
|
finally:
|
|
131
131
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -87,7 +87,7 @@ async def normalize_names(
|
|
|
87
87
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
88
88
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
89
89
|
else:
|
|
90
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
90
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
91
91
|
finally:
|
|
92
92
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
93
93
|
|
|
@@ -150,7 +150,7 @@ async def list_duplicates(
|
|
|
150
150
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
151
151
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
152
152
|
else:
|
|
153
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
153
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
154
154
|
finally:
|
|
155
155
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
156
156
|
|
|
@@ -215,7 +215,7 @@ async def list_unused(
|
|
|
215
215
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
216
216
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
217
217
|
else:
|
|
218
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
218
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
219
219
|
finally:
|
|
220
220
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
221
221
|
|
|
@@ -255,6 +255,6 @@ async def list_all(
|
|
|
255
255
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
256
256
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
257
257
|
else:
|
|
258
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
258
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
259
259
|
finally:
|
|
260
260
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -98,6 +98,6 @@ async def apply_rules(
|
|
|
98
98
|
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
99
99
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
100
100
|
else:
|
|
101
|
-
await io.print(f"Exception when calling YNAB: {e}
|
|
101
|
+
await io.print(f"Exception when calling YNAB: {e}")
|
|
102
102
|
finally:
|
|
103
103
|
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import anyio
|
|
3
2
|
import click
|
|
4
3
|
from rich.table import Table
|
|
5
4
|
|
|
@@ -38,7 +37,11 @@ def list_all(ctx: click.Context) -> None:
|
|
|
38
37
|
"""List all budgets in YNAB."""
|
|
39
38
|
|
|
40
39
|
ctx.ensure_object(dict)
|
|
41
|
-
|
|
40
|
+
anyio.run(
|
|
41
|
+
_list_all,
|
|
42
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
43
|
+
backend_options={"use_uvloop": True},
|
|
44
|
+
)
|
|
42
45
|
|
|
43
46
|
|
|
44
47
|
@click.group()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import anyio
|
|
3
2
|
import click
|
|
4
3
|
from rich.table import Table
|
|
5
4
|
|
|
@@ -68,7 +67,11 @@ def list_unused(ctx: click.Context) -> None:
|
|
|
68
67
|
"""List unused categories in the YNAB budget."""
|
|
69
68
|
|
|
70
69
|
ctx.ensure_object(dict)
|
|
71
|
-
|
|
70
|
+
anyio.run(
|
|
71
|
+
_list_unused,
|
|
72
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
73
|
+
backend_options={"use_uvloop": True},
|
|
74
|
+
)
|
|
72
75
|
|
|
73
76
|
|
|
74
77
|
@click.command()
|
|
@@ -77,7 +80,11 @@ def list_all(ctx: click.Context) -> None:
|
|
|
77
80
|
"""List all categories in the YNAB budget."""
|
|
78
81
|
|
|
79
82
|
ctx.ensure_object(dict)
|
|
80
|
-
|
|
83
|
+
anyio.run(
|
|
84
|
+
_list_all,
|
|
85
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
86
|
+
backend_options={"use_uvloop": True},
|
|
87
|
+
)
|
|
81
88
|
|
|
82
89
|
|
|
83
90
|
@click.group()
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import anyio
|
|
3
2
|
import click
|
|
4
3
|
from rich.table import Table
|
|
5
4
|
|
|
@@ -112,7 +111,11 @@ def normalize_names(ctx: click.Context) -> None:
|
|
|
112
111
|
"""Normalize payee names in the YNAB budget."""
|
|
113
112
|
|
|
114
113
|
ctx.ensure_object(dict)
|
|
115
|
-
|
|
114
|
+
anyio.run(
|
|
115
|
+
_normalize_names,
|
|
116
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
117
|
+
backend_options={"use_uvloop": True},
|
|
118
|
+
)
|
|
116
119
|
|
|
117
120
|
|
|
118
121
|
@click.command()
|
|
@@ -121,7 +124,11 @@ def list_duplicates(ctx: click.Context) -> None:
|
|
|
121
124
|
"""List duplicate payees in the YNAB budget."""
|
|
122
125
|
|
|
123
126
|
ctx.ensure_object(dict)
|
|
124
|
-
|
|
127
|
+
anyio.run(
|
|
128
|
+
_list_duplicates,
|
|
129
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
130
|
+
backend_options={"use_uvloop": True},
|
|
131
|
+
)
|
|
125
132
|
|
|
126
133
|
|
|
127
134
|
@click.command()
|
|
@@ -131,7 +138,12 @@ def list_unused(ctx: click.Context, prefix_unused: bool) -> None:
|
|
|
131
138
|
"""List unused payees in the YNAB budget."""
|
|
132
139
|
|
|
133
140
|
ctx.ensure_object(dict)
|
|
134
|
-
|
|
141
|
+
anyio.run(
|
|
142
|
+
_list_unused,
|
|
143
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
144
|
+
prefix_unused,
|
|
145
|
+
backend_options={"use_uvloop": True},
|
|
146
|
+
)
|
|
135
147
|
|
|
136
148
|
|
|
137
149
|
@click.command()
|
|
@@ -140,7 +152,11 @@ def list_all(ctx: click.Context) -> None:
|
|
|
140
152
|
"""List all payees in the YNAB budget."""
|
|
141
153
|
|
|
142
154
|
ctx.ensure_object(dict)
|
|
143
|
-
|
|
155
|
+
anyio.run(
|
|
156
|
+
_list_all,
|
|
157
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
158
|
+
backend_options={"use_uvloop": True},
|
|
159
|
+
)
|
|
144
160
|
|
|
145
161
|
|
|
146
162
|
@click.group()
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import json
|
|
3
2
|
from typing import IO, Any
|
|
4
3
|
|
|
4
|
+
import anyio
|
|
5
5
|
import click
|
|
6
6
|
from rich.table import Table
|
|
7
7
|
|
|
@@ -62,7 +62,12 @@ def apply_rules(ctx: click.Context, rules_file: IO[Any]) -> None:
|
|
|
62
62
|
|
|
63
63
|
transaction_rules = rules.TransactionRules.from_dict(json.load(rules_file))
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
anyio.run(
|
|
66
|
+
_apply_rules,
|
|
67
|
+
ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
|
|
68
|
+
transaction_rules,
|
|
69
|
+
backend_options={"use_uvloop": True},
|
|
70
|
+
)
|
|
66
71
|
|
|
67
72
|
|
|
68
73
|
@click.group()
|
|
@@ -5,38 +5,12 @@ from textual.app import App, ComposeResult
|
|
|
5
5
|
from textual.binding import Binding, BindingType
|
|
6
6
|
from textual.containers import Horizontal, Vertical
|
|
7
7
|
from textual.reactive import reactive
|
|
8
|
-
from textual.widgets import Checkbox, Footer, Header, Input, Label, TabbedContent
|
|
8
|
+
from textual.widgets import Checkbox, Footer, Header, Input, Label, TabbedContent
|
|
9
9
|
from typing_extensions import override
|
|
10
10
|
|
|
11
11
|
from ynab_cli.domain.settings import Settings, YnabSettings
|
|
12
|
-
from ynab_cli.host.textual.widgets.categories.tabs import CategoriesTabs
|
|
13
12
|
from ynab_cli.host.textual.widgets.common.dialogs import CANCELLED, DialogForm, SaveCancelDialogScreen
|
|
14
|
-
from ynab_cli.host.textual.widgets.
|
|
15
|
-
from ynab_cli.host.textual.widgets.payees.tabs import PayeesTabs
|
|
16
|
-
from ynab_cli.host.textual.widgets.transactions.tabs import TransactionsTabs
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class CommandTabs(RunnableWidget):
|
|
20
|
-
@override
|
|
21
|
-
def compose(self) -> ComposeResult:
|
|
22
|
-
with TabbedContent():
|
|
23
|
-
with TabPane("Transactions", id="transactions"):
|
|
24
|
-
yield TransactionsTabs().data_bind(settings=CommandTabs.settings)
|
|
25
|
-
with TabPane("Categories", id="categories"):
|
|
26
|
-
yield CategoriesTabs().data_bind(settings=CommandTabs.settings)
|
|
27
|
-
with TabPane("Payees", id="payees"):
|
|
28
|
-
yield PayeesTabs().data_bind(settings=CommandTabs.settings)
|
|
29
|
-
|
|
30
|
-
@override
|
|
31
|
-
async def run_command(self) -> None:
|
|
32
|
-
tabbed_content = self.query_one(TabbedContent)
|
|
33
|
-
if tabbed_content.active_pane:
|
|
34
|
-
if tabbed_content.active_pane.id == "transactions":
|
|
35
|
-
await self.query_one(TransactionsTabs).run_command()
|
|
36
|
-
elif tabbed_content.active_pane.id == "categories":
|
|
37
|
-
await self.query_one(CategoriesTabs).run_command()
|
|
38
|
-
elif tabbed_content.active_pane.id == "payees":
|
|
39
|
-
await self.query_one(PayeesTabs).run_command()
|
|
13
|
+
from ynab_cli.host.textual.widgets.tabs import CommandTabs
|
|
40
14
|
|
|
41
15
|
|
|
42
16
|
class SettingsDialogForm(DialogForm[Settings]):
|
|
@@ -81,8 +55,9 @@ class YnabCliApp(App[None]):
|
|
|
81
55
|
|
|
82
56
|
BINDINGS: ClassVar[list[BindingType]] = [
|
|
83
57
|
Binding("ctrl+c", "quit", "Quit", show=True, priority=True),
|
|
84
|
-
Binding("s", "settings", "Settings", show=True, priority=False),
|
|
85
|
-
Binding("
|
|
58
|
+
Binding("s", "settings", "Application Settings", show=True, priority=False),
|
|
59
|
+
Binding("p", "parameters", "Command Parameters", show=True, priority=False),
|
|
60
|
+
Binding("r", "run", "Run Command", show=True, priority=False),
|
|
86
61
|
]
|
|
87
62
|
|
|
88
63
|
settings: reactive[Settings] = reactive(Settings())
|
|
@@ -104,17 +79,21 @@ class YnabCliApp(App[None]):
|
|
|
104
79
|
def on_mount(self) -> None:
|
|
105
80
|
self.query_one(CommandTabs).query_one(TabbedContent).focus()
|
|
106
81
|
|
|
107
|
-
async def action_run(self) -> None:
|
|
108
|
-
command_tabs = self.query_one(CommandTabs)
|
|
109
|
-
await command_tabs.run_command()
|
|
110
|
-
|
|
111
82
|
async def action_settings(self) -> None:
|
|
112
|
-
self.
|
|
83
|
+
self._get_settings_worker()
|
|
113
84
|
|
|
114
85
|
@work(exclusive=True)
|
|
115
|
-
async def
|
|
86
|
+
async def _get_settings_worker(self) -> None:
|
|
116
87
|
result = await self.push_screen_wait(
|
|
117
88
|
SaveCancelDialogScreen(SettingsDialogForm(self.settings), title="Settings")
|
|
118
89
|
)
|
|
119
90
|
if result is not CANCELLED:
|
|
120
91
|
self.settings = result
|
|
92
|
+
|
|
93
|
+
async def action_parameters(self) -> None:
|
|
94
|
+
command_tabs = self.query_one(CommandTabs)
|
|
95
|
+
await command_tabs.active_command().get_command_params()
|
|
96
|
+
|
|
97
|
+
async def action_run(self) -> None:
|
|
98
|
+
command_tabs = self.query_one(CommandTabs)
|
|
99
|
+
await command_tabs.active_command().run_command()
|
|
@@ -9,22 +9,22 @@ CommandTabs {
|
|
|
9
9
|
height: auto;
|
|
10
10
|
padding-top: 1;
|
|
11
11
|
|
|
12
|
-
&
|
|
13
|
-
|
|
14
|
-
height: 1fr;
|
|
15
|
-
}
|
|
12
|
+
& CommandWidget {
|
|
13
|
+
height: auto;
|
|
16
14
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
& DataTable {
|
|
16
|
+
border: solid $secondary;
|
|
17
|
+
height: 1fr;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
& Log {
|
|
21
|
+
border: solid $secondary;
|
|
22
|
+
height: 8;
|
|
23
|
+
}
|
|
20
24
|
}
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
RunnableWidget {
|
|
25
|
-
height: auto;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
28
|
DialogScreen {
|
|
29
29
|
align: center middle;
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import logging
|
|
3
2
|
|
|
3
|
+
import anyio
|
|
4
4
|
import click
|
|
5
5
|
from textual.logging import TextualHandler
|
|
6
6
|
|
|
@@ -39,4 +39,8 @@ def tui(
|
|
|
39
39
|
handlers=[TextualHandler()],
|
|
40
40
|
)
|
|
41
41
|
|
|
42
|
-
|
|
42
|
+
anyio.run(
|
|
43
|
+
_tui,
|
|
44
|
+
ctx.obj[CONTEXT_KEY_SETTINGS],
|
|
45
|
+
backend_options={"use_uvloop": True},
|
|
46
|
+
)
|