ynab-cli 0.5.4__tar.gz → 0.6.1__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.1/.github/.copilot-instructions.md +191 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/CHANGELOG.md +23 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/Makefile +1 -1
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/PKG-INFO +5 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/pyproject.toml +16 -7
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/rich/io.py +5 -5
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/util.py +12 -0
- ynab_cli-0.6.1/src/ynab_cli/domain/use_cases/budgets.py +47 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/categories.py +48 -47
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/payees.py +103 -87
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/transactions.py +26 -24
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/cli.py +1 -1
- ynab_cli-0.6.1/src/ynab_cli/host/click/commands/budgets.py +69 -0
- ynab_cli-0.6.1/src/ynab_cli/host/click/commands/categories.py +129 -0
- ynab_cli-0.6.1/src/ynab_cli/host/click/commands/payees.py +225 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/commands/rich/progress_table.py +2 -2
- ynab_cli-0.6.1/src/ynab_cli/host/click/commands/transactions.py +94 -0
- ynab_cli-0.6.1/src/ynab_cli/host/click/container.py +88 -0
- ynab_cli-0.6.1/src/ynab_cli/host/container.py +68 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/app.py +28 -37
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/app.tcss +11 -11
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/cli.py +13 -6
- ynab_cli-0.6.1/src/ynab_cli/host/textual/container.py +40 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/list_all.py +11 -9
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/list_unused.py +11 -9
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/tabs.py +12 -6
- ynab_cli-0.6.1/src/ynab_cli/host/textual/widgets/common/command_widget.py +61 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/screen.py +4 -2
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/list_all.py +11 -7
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +11 -9
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/list_unused.py +12 -20
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +11 -9
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/tabs.py +14 -8
- ynab_cli-0.6.1/src/ynab_cli/host/textual/widgets/tabs.py +37 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +13 -20
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/transactions/tabs.py +11 -5
- ynab_cli-0.6.1/tests/conftest.py +13 -0
- ynab_cli-0.6.1/tests/factories/ynab.py +18 -0
- ynab_cli-0.6.1/tests/unit/adapters/rich/test_io.py +177 -0
- ynab_cli-0.6.1/tests/unit/adapters/textual/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/adapters/textual/test_io.py +308 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/unit/conftest.py +4 -4
- ynab_cli-0.6.1/tests/unit/domain/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/domain/models/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/domain/models/test_rules.py +306 -0
- ynab_cli-0.6.1/tests/unit/domain/ports/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/domain/ports/test_io.py +502 -0
- ynab_cli-0.6.1/tests/unit/domain/use_cases/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/domain/use_cases/test_budgets.py +64 -0
- ynab_cli-0.6.1/tests/unit/domain/use_cases/test_categories.py +258 -0
- ynab_cli-0.6.1/tests/unit/domain/use_cases/test_payees.py +366 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/unit/domain/use_cases/test_transactions.py +78 -4
- ynab_cli-0.6.1/tests/unit/host/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/host/click/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/host/click/commands/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/host/click/commands/test_budgets.py +25 -0
- ynab_cli-0.6.1/tests/unit/host/click/commands/test_categories.py +64 -0
- ynab_cli-0.6.1/tests/unit/host/click/commands/test_payees.py +107 -0
- ynab_cli-0.6.1/tests/unit/host/click/commands/test_transactions.py +65 -0
- ynab_cli-0.6.1/tests/unit/host/click/conftest.py +21 -0
- ynab_cli-0.6.1/tests/unit/host/conftest.py +13 -0
- ynab_cli-0.6.1/tests/unit/host/test_cli.py +35 -0
- ynab_cli-0.6.1/tests/unit/host/textual/__init__.py +0 -0
- ynab_cli-0.6.1/tests/unit/host/textual/conftest.py +21 -0
- ynab_cli-0.6.1/tests/unit/host/textual/test_app.py +49 -0
- ynab_cli-0.6.1/tests/unit/host/textual/test_cli.py +19 -0
- ynab_cli-0.6.1/uv.lock +1792 -0
- ynab_cli-0.5.4/src/ynab_cli/domain/use_cases/budgets.py +0 -45
- ynab_cli-0.5.4/src/ynab_cli/host/click/commands/budgets.py +0 -54
- ynab_cli-0.5.4/src/ynab_cli/host/click/commands/categories.py +0 -96
- ynab_cli-0.5.4/src/ynab_cli/host/click/commands/payees.py +0 -161
- ynab_cli-0.5.4/src/ynab_cli/host/click/commands/transactions.py +0 -80
- 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/factories/ynab.py +0 -18
- 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.1}/.env-sample +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/FUNDING.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/dependabot.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/workflows/pr.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/workflows/publish-to-pypi.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.gitignore +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.python-version +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/LICENSE.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/README.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/docs/index.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/docs/transaction_rules.md +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/mkdocs.yml +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/rich/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/textual/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/textual/io.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/client.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/errors.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/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.1}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/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.1}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/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.1}/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.1}/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.1}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/types.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/constants.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/models/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/models/rules.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/ports/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/ports/io.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/settings.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/cli.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/commands/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/constants.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/dialog.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/form.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/transactions/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/py.typed +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/factories/__init__.py +0 -0
- {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/unit/__init__.py +0 -0
- {ynab_cli-0.5.4/tests/unit/domain → ynab_cli-0.6.1/tests/unit/adapters}/__init__.py +0 -0
- {ynab_cli-0.5.4/tests/unit/domain/use_cases → ynab_cli-0.6.1/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,29 @@ 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.1 (2025-07-09)
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- fix issue with providing new token
|
|
13
|
+
|
|
14
|
+
### Refactor
|
|
15
|
+
|
|
16
|
+
- work towards using dependency injection container
|
|
17
|
+
|
|
18
|
+
## v0.6.0 (2025-06-28)
|
|
19
|
+
|
|
20
|
+
### Feat
|
|
21
|
+
|
|
22
|
+
- use anyio and uvloop
|
|
23
|
+
|
|
24
|
+
### Fix
|
|
25
|
+
|
|
26
|
+
- **tests**: adding moar tests
|
|
27
|
+
- **tests**: added tests for a bunch of things
|
|
28
|
+
- **deps**: bump textual from 3.3.0 to 3.4.0
|
|
29
|
+
- **tui**: fix some ui issues and clean up some code
|
|
30
|
+
|
|
8
31
|
## v0.5.4 (2025-06-10)
|
|
9
32
|
|
|
10
33
|
### Fix
|
|
@@ -10,7 +10,7 @@ lint: .venv # run linters and type checkers
|
|
|
10
10
|
uv run mypy .
|
|
11
11
|
|
|
12
12
|
test: .venv lint # run tests with coverage
|
|
13
|
-
uv run pytest --cov=src tests
|
|
13
|
+
uv run pytest --cov-report term-missing:skip-covered --cov=src tests
|
|
14
14
|
|
|
15
15
|
clean: # clean up build artifacts and caches
|
|
16
16
|
rm -f .coverage
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ynab-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.1
|
|
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,17 +19,20 @@ 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
|
|
25
26
|
Requires-Dist: httpx~=0.28
|
|
27
|
+
Requires-Dist: lagom~=2.7
|
|
26
28
|
Requires-Dist: python-dotenv~=1.1
|
|
27
29
|
Requires-Dist: rapidfuzz~=3.13
|
|
28
30
|
Requires-Dist: rich~=14.0
|
|
29
31
|
Requires-Dist: rule-engine~=4.5
|
|
30
32
|
Requires-Dist: textual-fspicker~=0.4
|
|
31
|
-
Requires-Dist: textual~=3.
|
|
33
|
+
Requires-Dist: textual~=3.6
|
|
32
34
|
Requires-Dist: typing-extensions~=4.14
|
|
35
|
+
Requires-Dist: uvloop~=0.21
|
|
33
36
|
Description-Content-Type: text/markdown
|
|
34
37
|
|
|
35
38
|
# YNAB CLI
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ynab-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.1"
|
|
4
4
|
description = "YNAB Command Line Interface"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.11"
|
|
@@ -33,17 +33,20 @@ 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",
|
|
39
40
|
"httpx~=0.28",
|
|
41
|
+
"lagom~=2.7",
|
|
40
42
|
"python-dotenv~=1.1",
|
|
41
43
|
"rapidfuzz~=3.13",
|
|
42
44
|
"rich~=14.0",
|
|
43
45
|
"rule-engine~=4.5",
|
|
44
46
|
"textual-fspicker~=0.4",
|
|
45
|
-
"textual~=3.
|
|
47
|
+
"textual~=3.6",
|
|
46
48
|
"typing-extensions~=4.14",
|
|
49
|
+
"uvloop~=0.21",
|
|
47
50
|
]
|
|
48
51
|
|
|
49
52
|
[project.urls]
|
|
@@ -59,18 +62,18 @@ ynab-cli = "ynab_cli.host.cli:main"
|
|
|
59
62
|
|
|
60
63
|
[dependency-groups]
|
|
61
64
|
dev = [
|
|
62
|
-
"commitizen>=4.8.
|
|
65
|
+
"commitizen>=4.8.3",
|
|
63
66
|
"factory-boy>=3.3.3",
|
|
64
67
|
"mkdocs-click>=0.9.0",
|
|
65
68
|
"mkdocs-material>=9.6.14",
|
|
66
69
|
"mkdocs>=1.6.1",
|
|
67
70
|
"mypy>=1.16.0",
|
|
68
|
-
"pytest-
|
|
69
|
-
"pytest-cov>=6.1.1",
|
|
71
|
+
"pytest-cov>=6.2.1",
|
|
70
72
|
"pytest-mock>=3.14.1",
|
|
71
|
-
"pytest>=8.4.
|
|
72
|
-
"ruff>=0.
|
|
73
|
+
"pytest>=8.4.1",
|
|
74
|
+
"ruff>=0.12.1",
|
|
73
75
|
"textual-dev>=1.7.0",
|
|
76
|
+
"trio>=0.30.0",
|
|
74
77
|
"types-python-dateutil>=2.9.0.20250516",
|
|
75
78
|
]
|
|
76
79
|
|
|
@@ -136,6 +139,12 @@ max-complexity = 70
|
|
|
136
139
|
builtins-ignorelist = ["type", "id"]
|
|
137
140
|
|
|
138
141
|
|
|
142
|
+
[tool.coverage.run]
|
|
143
|
+
omit = [
|
|
144
|
+
"src/ynab_cli/adapters/ynab/**/*",
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
|
|
139
148
|
[tool.commitizen]
|
|
140
149
|
name = "cz_conventional_commits"
|
|
141
150
|
annotated_tag = true
|
|
@@ -6,8 +6,9 @@ from ynab_cli.domain.ports.io import IO, Progress
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class RichProgress(Progress):
|
|
9
|
-
def __init__(self,
|
|
10
|
-
self._progress
|
|
9
|
+
def __init__(self, progress: rich_progress.Progress) -> None:
|
|
10
|
+
self._progress = progress
|
|
11
|
+
self._task_id = progress.add_task("Loading...")
|
|
11
12
|
|
|
12
13
|
@override
|
|
13
14
|
async def update(
|
|
@@ -20,10 +21,9 @@ class RichProgress(Progress):
|
|
|
20
21
|
|
|
21
22
|
|
|
22
23
|
class RichIO(IO):
|
|
23
|
-
def __init__(self,
|
|
24
|
-
progress, _ = progress_info
|
|
24
|
+
def __init__(self, progress: rich_progress.Progress) -> None:
|
|
25
25
|
self._progress = progress
|
|
26
|
-
self.progress = RichProgress(
|
|
26
|
+
self.progress = RichProgress(self._progress)
|
|
27
27
|
|
|
28
28
|
@override
|
|
29
29
|
async def prompt(self, prompt: str, password: bool = False) -> str:
|
|
@@ -58,7 +58,19 @@ async def run_asyncio_detailed(
|
|
|
58
58
|
if not new_access_token:
|
|
59
59
|
raise e
|
|
60
60
|
|
|
61
|
+
#
|
|
62
|
+
# Update the client with the new access token
|
|
63
|
+
# Also need to update the headers in the underlying
|
|
64
|
+
# HTTPX clients if they are set up.
|
|
65
|
+
#
|
|
61
66
|
client.token = new_access_token
|
|
67
|
+
auth_header = f"{client.prefix} {client.token}" if client.prefix else client.token
|
|
68
|
+
client._headers[client.auth_header_name] = auth_header
|
|
69
|
+
if client._async_client:
|
|
70
|
+
client._async_client.headers[client.auth_header_name] = auth_header
|
|
71
|
+
if client._client:
|
|
72
|
+
client._client.headers[client.auth_header_name] = auth_header
|
|
73
|
+
|
|
62
74
|
response = await asyncio_detailed(*args, **kwargs)
|
|
63
75
|
parsed_data = get_parsed_response_data(response)
|
|
64
76
|
else:
|
|
@@ -0,0 +1,47 @@
|
|
|
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.settings import Settings
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ListAllParams(TypedDict):
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ListAll:
|
|
16
|
+
"""Use case for listing all budgets."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, io: ports.IO, client: ynab.AuthenticatedClient):
|
|
19
|
+
self._io = io
|
|
20
|
+
self._client = client
|
|
21
|
+
|
|
22
|
+
async def __call__(self, settings: Settings, params: ListAllParams) -> AsyncIterator[models.BudgetSummary]:
|
|
23
|
+
try:
|
|
24
|
+
progress_total = 0
|
|
25
|
+
|
|
26
|
+
budgets = (
|
|
27
|
+
await util.get_asyncio_detailed(
|
|
28
|
+
self._io, get_budgets.asyncio_detailed, include_accounts=False, client=self._client
|
|
29
|
+
)
|
|
30
|
+
).data.budgets
|
|
31
|
+
budgets.sort(key=lambda b: b.name)
|
|
32
|
+
|
|
33
|
+
progress_total = len(budgets)
|
|
34
|
+
await self._io.progress.update(total=progress_total)
|
|
35
|
+
for budget in budgets:
|
|
36
|
+
await self._io.progress.update(advance=1)
|
|
37
|
+
yield budget
|
|
38
|
+
|
|
39
|
+
except Exception as e:
|
|
40
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
41
|
+
await self._io.print("Invalid or expired access token. Please update your settings.")
|
|
42
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
43
|
+
await self._io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
44
|
+
else:
|
|
45
|
+
await self._io.print(f"Exception when calling YNAB: {e}")
|
|
46
|
+
finally:
|
|
47
|
+
await self._io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -6,7 +6,6 @@ from ynab_cli.adapters.ynab import models, util
|
|
|
6
6
|
from ynab_cli.adapters.ynab.api.categories import get_categories
|
|
7
7
|
from ynab_cli.adapters.ynab.api.transactions import get_transactions_by_category
|
|
8
8
|
from ynab_cli.domain import ports
|
|
9
|
-
from ynab_cli.domain.constants import YNAB_API_URL
|
|
10
9
|
from ynab_cli.domain.settings import Settings
|
|
11
10
|
|
|
12
11
|
|
|
@@ -20,47 +19,48 @@ class ListUnusedParams(TypedDict):
|
|
|
20
19
|
pass
|
|
21
20
|
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
) -> AsyncIterator[models.Category]:
|
|
26
|
-
try:
|
|
27
|
-
progress_total = 0
|
|
22
|
+
class ListUnused:
|
|
23
|
+
"""Use case for listing unused categories."""
|
|
28
24
|
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
def __init__(self, io: ports.IO, client: ynab.AuthenticatedClient):
|
|
26
|
+
self._io = io
|
|
27
|
+
self._client = client
|
|
28
|
+
|
|
29
|
+
async def __call__(self, settings: Settings, params: ListUnusedParams) -> AsyncIterator[models.Category]:
|
|
30
|
+
try:
|
|
31
|
+
progress_total = 0
|
|
31
32
|
|
|
32
|
-
async with client:
|
|
33
33
|
category_groups = (
|
|
34
34
|
await util.get_asyncio_detailed(
|
|
35
|
-
|
|
35
|
+
self._io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=self._client
|
|
36
36
|
)
|
|
37
37
|
).data.category_groups
|
|
38
38
|
category_groups.sort(key=lambda cg: cg.name)
|
|
39
39
|
|
|
40
40
|
progress_total = len(category_groups)
|
|
41
|
-
await
|
|
41
|
+
await self._io.progress.update(total=progress_total)
|
|
42
42
|
for category_group in category_groups:
|
|
43
|
-
await
|
|
43
|
+
await self._io.progress.update(advance=1)
|
|
44
44
|
|
|
45
45
|
if _should_skip_category_or_group(category_or_group=category_group):
|
|
46
46
|
continue
|
|
47
47
|
|
|
48
48
|
progress_total += len(category_group.categories)
|
|
49
|
-
await
|
|
49
|
+
await self._io.progress.update(total=progress_total)
|
|
50
50
|
category_group.categories.sort(key=lambda c: c.name)
|
|
51
51
|
for category in category_group.categories:
|
|
52
|
-
await
|
|
52
|
+
await self._io.progress.update(advance=1)
|
|
53
53
|
|
|
54
54
|
if _should_skip_category_or_group(category_or_group=category):
|
|
55
55
|
continue
|
|
56
56
|
|
|
57
57
|
transactions = (
|
|
58
58
|
await util.get_asyncio_detailed(
|
|
59
|
-
|
|
59
|
+
self._io,
|
|
60
60
|
get_transactions_by_category.asyncio_detailed,
|
|
61
61
|
settings.ynab.budget_id,
|
|
62
62
|
str(category.id),
|
|
63
|
-
client=
|
|
63
|
+
client=self._client,
|
|
64
64
|
)
|
|
65
65
|
).data.transactions
|
|
66
66
|
num_transactions = len(transactions)
|
|
@@ -69,63 +69,64 @@ async def list_unused(
|
|
|
69
69
|
if not num_transactions:
|
|
70
70
|
yield category
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
74
|
+
await self._io.print("Invalid or expired access token. Please update your settings.")
|
|
75
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
76
|
+
await self._io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
77
|
+
else:
|
|
78
|
+
await self._io.print(f"Exception when calling YNAB: {e}")
|
|
79
|
+
finally:
|
|
80
|
+
await self._io.progress.update(total=progress_total, completed=progress_total)
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
class ListAllParams(TypedDict):
|
|
84
84
|
pass
|
|
85
85
|
|
|
86
86
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
87
|
+
class ListAll:
|
|
88
|
+
"""Use case for listing all categories."""
|
|
89
|
+
|
|
90
|
+
def __init__(self, io: ports.IO, client: ynab.AuthenticatedClient):
|
|
91
|
+
self._io = io
|
|
92
|
+
self._client = client
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
94
|
+
async def __call__(self, settings: Settings, params: ListAllParams) -> AsyncIterator[models.Category]:
|
|
95
|
+
try:
|
|
96
|
+
progress_total = 0
|
|
95
97
|
|
|
96
|
-
async with client:
|
|
97
98
|
category_groups = (
|
|
98
99
|
await util.get_asyncio_detailed(
|
|
99
|
-
|
|
100
|
+
self._io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=self._client
|
|
100
101
|
)
|
|
101
102
|
).data.category_groups
|
|
102
103
|
category_groups.sort(key=lambda cg: cg.name)
|
|
103
104
|
|
|
104
105
|
progress_total = len(category_groups)
|
|
105
|
-
await
|
|
106
|
+
await self._io.progress.update(total=progress_total)
|
|
106
107
|
for category_group in category_groups:
|
|
107
|
-
await
|
|
108
|
+
await self._io.progress.update(advance=1)
|
|
108
109
|
|
|
109
110
|
if _should_skip_category_or_group(category_or_group=category_group):
|
|
110
111
|
continue
|
|
111
112
|
|
|
112
113
|
progress_total += len(category_group.categories)
|
|
113
|
-
await
|
|
114
|
+
await self._io.progress.update(total=progress_total)
|
|
114
115
|
category_group.categories.sort(key=lambda c: c.name)
|
|
115
116
|
for category in category_group.categories:
|
|
116
|
-
await
|
|
117
|
+
await self._io.progress.update(advance=1)
|
|
117
118
|
|
|
118
119
|
if _should_skip_category_or_group(category_or_group=category):
|
|
119
120
|
continue
|
|
120
121
|
|
|
121
122
|
yield category
|
|
122
123
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
124
|
+
except Exception as e:
|
|
125
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
126
|
+
await self._io.print("Invalid or expired access token. Please update your settings.")
|
|
127
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
128
|
+
await self._io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
129
|
+
else:
|
|
130
|
+
await self._io.print(f"Exception when calling YNAB: {e}")
|
|
131
|
+
finally:
|
|
132
|
+
await self._io.progress.update(total=progress_total, completed=progress_total)
|