ynab-cli 0.5.2__tar.gz → 0.5.3__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.5.2 → ynab_cli-0.5.3}/CHANGELOG.md +6 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/PKG-INFO +1 -1
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/pyproject.toml +1 -1
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/textual/io.py +12 -14
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/util.py +3 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/budgets.py +5 -1
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/categories.py +14 -4
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/payees.py +20 -4
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/transactions.py +5 -1
- ynab_cli-0.5.3/src/ynab_cli/host/textual/app.py +126 -0
- ynab_cli-0.5.3/src/ynab_cli/host/textual/app.tcss +96 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/cli.py +2 -2
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/widgets/categories.py +5 -4
- ynab_cli-0.5.2/src/ynab_cli/host/textual/widgets/__init__.py → ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/base_command.py +5 -16
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +10 -0
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/__init__.py +12 -0
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/dialog.py +63 -0
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/form.py +11 -0
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/screen.py +40 -0
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +11 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/widgets/payees.py +7 -8
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/widgets/transactions.py +4 -3
- ynab_cli-0.5.3/tests/unit/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/uv.lock +1 -1
- ynab_cli-0.5.2/src/ynab_cli/host/textual/app.py +0 -98
- ynab_cli-0.5.2/src/ynab_cli/host/textual/app.tcss +0 -47
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.env-sample +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/FUNDING.yml +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/dependabot.yml +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/workflows/pr.yml +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/workflows/publish-to-pypi.yml +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.gitignore +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.python-version +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/LICENSE.md +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/Makefile +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/README.md +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/docs/index.md +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/docs/transaction_rules.md +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/mkdocs.yml +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/rich/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/rich/io.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/textual/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/client.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/errors.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/types.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/constants.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/models/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/models/rules.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/ports/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/ports/io.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/settings.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/cli.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/cli.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/budgets.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/categories.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/payees.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/transactions.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/constants.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/__init__.py +0 -0
- {ynab_cli-0.5.2/tests → ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common}/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/py.typed +0 -0
- {ynab_cli-0.5.2/tests/factories → ynab_cli-0.5.3/tests}/__init__.py +0 -0
- {ynab_cli-0.5.2/tests/unit → ynab_cli-0.5.3/tests/factories}/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/factories/ynab.py +0 -0
- {ynab_cli-0.5.2/tests/unit/domain → ynab_cli-0.5.3/tests/unit}/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/conftest.py +0 -0
- {ynab_cli-0.5.2/tests/unit/domain/use_cases → ynab_cli-0.5.3/tests/unit/domain}/__init__.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/domain/use_cases/test_categories.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/domain/use_cases/test_payees.py +0 -0
- {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/domain/use_cases/test_transactions.py +0 -0
|
@@ -5,6 +5,12 @@ 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.5.3 (2025-06-09)
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- **tui**: moves the app settings into a separate modal dialog
|
|
13
|
+
|
|
8
14
|
## v0.5.2 (2025-06-07)
|
|
9
15
|
|
|
10
16
|
### Fix
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ynab-cli
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.3
|
|
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/
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
from typing import Any
|
|
2
2
|
|
|
3
|
-
from textual import on
|
|
4
3
|
from textual.app import App, ComposeResult
|
|
5
|
-
from textual.containers import Vertical
|
|
6
|
-
from textual.screen import ModalScreen
|
|
7
4
|
from textual.widgets import Input, Label, Log, ProgressBar
|
|
8
5
|
from typing_extensions import override
|
|
9
6
|
|
|
10
7
|
from ynab_cli.domain.ports.io import IO, Progress
|
|
8
|
+
from ynab_cli.host.textual.widgets.common.dialogs import CANCELLED, DialogForm, SaveCancelDialogScreen
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
class TextualProgress(Progress):
|
|
@@ -28,9 +26,7 @@ class TextualProgress(Progress):
|
|
|
28
26
|
self._progress_bar.update(**kwargs)
|
|
29
27
|
|
|
30
28
|
|
|
31
|
-
class
|
|
32
|
-
"""A simple modal screen which asks for user input."""
|
|
33
|
-
|
|
29
|
+
class IODialogForm(DialogForm[str]):
|
|
34
30
|
def __init__(self, prompt: str, password: bool, *args: Any, **kwargs: Any) -> None:
|
|
35
31
|
super().__init__(*args, **kwargs)
|
|
36
32
|
self._prompt = prompt
|
|
@@ -38,16 +34,15 @@ class IOScreen(ModalScreen[str]):
|
|
|
38
34
|
|
|
39
35
|
@override
|
|
40
36
|
def compose(self) -> ComposeResult:
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
yield Input(password=self._password)
|
|
37
|
+
yield Label(self._prompt)
|
|
38
|
+
yield Input(password=self._password)
|
|
44
39
|
|
|
45
|
-
@
|
|
46
|
-
def
|
|
47
|
-
self.
|
|
40
|
+
@override
|
|
41
|
+
async def get_result(self) -> str:
|
|
42
|
+
return self.query_one(Input).value.strip()
|
|
48
43
|
|
|
49
44
|
|
|
50
|
-
class
|
|
45
|
+
class TextualIO(IO):
|
|
51
46
|
def __init__(self, app: App[Any], log: Log, progress_bar: ProgressBar) -> None:
|
|
52
47
|
self._app = app
|
|
53
48
|
self._log = log
|
|
@@ -55,7 +50,10 @@ class TextualWorkerIO(IO):
|
|
|
55
50
|
|
|
56
51
|
@override
|
|
57
52
|
async def prompt(self, prompt: str, password: bool = False) -> str:
|
|
58
|
-
|
|
53
|
+
result = await self._app.push_screen_wait(SaveCancelDialogScreen(IODialogForm(prompt, password), title="Input"))
|
|
54
|
+
if result is CANCELLED:
|
|
55
|
+
return ""
|
|
56
|
+
return result
|
|
59
57
|
|
|
60
58
|
@override
|
|
61
59
|
async def print(self, message: str) -> None:
|
|
@@ -55,6 +55,9 @@ async def run_asyncio_detailed(
|
|
|
55
55
|
new_access_token = await io.prompt(
|
|
56
56
|
prompt="API rate limit exceeded. Enter a new access token", password=True
|
|
57
57
|
)
|
|
58
|
+
if not new_access_token:
|
|
59
|
+
raise e
|
|
60
|
+
|
|
58
61
|
client.token = new_access_token
|
|
59
62
|
response = await asyncio_detailed(*args, **kwargs)
|
|
60
63
|
parsed_data = get_parsed_response_data(response)
|
|
@@ -15,6 +15,7 @@ class ListAllParams(TypedDict):
|
|
|
15
15
|
|
|
16
16
|
async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> AsyncIterator[models.BudgetSummary]:
|
|
17
17
|
async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
|
|
18
|
+
progress_total = 0
|
|
18
19
|
try:
|
|
19
20
|
budgets = (
|
|
20
21
|
await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=True, client=client)
|
|
@@ -22,8 +23,9 @@ async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> A
|
|
|
22
23
|
budgets.sort(key=lambda b: b.name)
|
|
23
24
|
|
|
24
25
|
progress_total = len(budgets)
|
|
26
|
+
await io.progress.update(total=progress_total)
|
|
25
27
|
for budget in budgets:
|
|
26
|
-
await io.progress.update(
|
|
28
|
+
await io.progress.update(advance=1)
|
|
27
29
|
yield budget
|
|
28
30
|
|
|
29
31
|
except util.ApiError as e:
|
|
@@ -31,3 +33,5 @@ async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> A
|
|
|
31
33
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
32
34
|
else:
|
|
33
35
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
36
|
+
finally:
|
|
37
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -22,6 +22,7 @@ class ListUnusedParams(TypedDict):
|
|
|
22
22
|
|
|
23
23
|
async def list_unused(settings: Settings, io: ports.IO, params: ListUnusedParams) -> AsyncIterator[models.Category]:
|
|
24
24
|
async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
|
|
25
|
+
progress_total = 0
|
|
25
26
|
try:
|
|
26
27
|
category_groups = (
|
|
27
28
|
await util.get_asyncio_detailed(
|
|
@@ -31,16 +32,18 @@ async def list_unused(settings: Settings, io: ports.IO, params: ListUnusedParams
|
|
|
31
32
|
category_groups.sort(key=lambda cg: cg.name)
|
|
32
33
|
|
|
33
34
|
progress_total = len(category_groups)
|
|
35
|
+
await io.progress.update(total=progress_total)
|
|
34
36
|
for category_group in category_groups:
|
|
35
|
-
await io.progress.update(
|
|
37
|
+
await io.progress.update(advance=1)
|
|
36
38
|
|
|
37
39
|
if _should_skip_category_or_group(category_or_group=category_group):
|
|
38
40
|
continue
|
|
39
41
|
|
|
40
42
|
progress_total += len(category_group.categories)
|
|
43
|
+
await io.progress.update(total=progress_total)
|
|
41
44
|
category_group.categories.sort(key=lambda c: c.name)
|
|
42
45
|
for category in category_group.categories:
|
|
43
|
-
await io.progress.update(
|
|
46
|
+
await io.progress.update(advance=1)
|
|
44
47
|
|
|
45
48
|
if _should_skip_category_or_group(category_or_group=category):
|
|
46
49
|
continue
|
|
@@ -65,6 +68,8 @@ async def list_unused(settings: Settings, io: ports.IO, params: ListUnusedParams
|
|
|
65
68
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
66
69
|
else:
|
|
67
70
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
71
|
+
finally:
|
|
72
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
68
73
|
|
|
69
74
|
|
|
70
75
|
class ListAllParams(TypedDict):
|
|
@@ -73,6 +78,7 @@ class ListAllParams(TypedDict):
|
|
|
73
78
|
|
|
74
79
|
async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> AsyncIterator[models.Category]:
|
|
75
80
|
async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
|
|
81
|
+
progress_total = 0
|
|
76
82
|
try:
|
|
77
83
|
category_groups = (
|
|
78
84
|
await util.get_asyncio_detailed(
|
|
@@ -82,16 +88,18 @@ async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> A
|
|
|
82
88
|
category_groups.sort(key=lambda cg: cg.name)
|
|
83
89
|
|
|
84
90
|
progress_total = len(category_groups)
|
|
91
|
+
await io.progress.update(total=progress_total)
|
|
85
92
|
for category_group in category_groups:
|
|
86
|
-
await io.progress.update(
|
|
93
|
+
await io.progress.update(advance=1)
|
|
87
94
|
|
|
88
95
|
if _should_skip_category_or_group(category_or_group=category_group):
|
|
89
96
|
continue
|
|
90
97
|
|
|
91
98
|
progress_total += len(category_group.categories)
|
|
99
|
+
await io.progress.update(total=progress_total)
|
|
92
100
|
category_group.categories.sort(key=lambda c: c.name)
|
|
93
101
|
for category in category_group.categories:
|
|
94
|
-
await io.progress.update(
|
|
102
|
+
await io.progress.update(advance=1)
|
|
95
103
|
|
|
96
104
|
if _should_skip_category_or_group(category_or_group=category):
|
|
97
105
|
continue
|
|
@@ -103,3 +111,5 @@ async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> A
|
|
|
103
111
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
104
112
|
else:
|
|
105
113
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
114
|
+
finally:
|
|
115
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -51,6 +51,8 @@ async def normalize_names(
|
|
|
51
51
|
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
52
52
|
|
|
53
53
|
async with client:
|
|
54
|
+
progress_total = 0
|
|
55
|
+
|
|
54
56
|
try:
|
|
55
57
|
payees = (
|
|
56
58
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
@@ -58,8 +60,9 @@ async def normalize_names(
|
|
|
58
60
|
payees.sort(key=lambda p: p.name)
|
|
59
61
|
|
|
60
62
|
progress_total = len(payees)
|
|
63
|
+
await io.progress.update(total=progress_total)
|
|
61
64
|
for payee in payees:
|
|
62
|
-
await io.progress.update(
|
|
65
|
+
await io.progress.update(advance=1)
|
|
63
66
|
|
|
64
67
|
if _should_skip_payee(payee=payee):
|
|
65
68
|
continue
|
|
@@ -83,6 +86,8 @@ async def normalize_names(
|
|
|
83
86
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
84
87
|
else:
|
|
85
88
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
89
|
+
finally:
|
|
90
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
86
91
|
|
|
87
92
|
|
|
88
93
|
class ListDuplicatesParams(TypedDict):
|
|
@@ -96,6 +101,8 @@ async def list_duplicates(
|
|
|
96
101
|
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
97
102
|
|
|
98
103
|
async with client:
|
|
104
|
+
progress_total = 0
|
|
105
|
+
|
|
99
106
|
try:
|
|
100
107
|
possible_duplicates: dict[tuple[UUID, str], list[tuple[UUID, str]]] = {}
|
|
101
108
|
|
|
@@ -105,8 +112,9 @@ async def list_duplicates(
|
|
|
105
112
|
payees.sort(key=lambda p: p.name)
|
|
106
113
|
|
|
107
114
|
progress_total = len(payees)
|
|
115
|
+
await io.progress.update(total=progress_total)
|
|
108
116
|
for idx, payee in enumerate(payees):
|
|
109
|
-
await io.progress.update(
|
|
117
|
+
await io.progress.update(advance=1)
|
|
110
118
|
|
|
111
119
|
if _should_skip_payee(payee=payee):
|
|
112
120
|
continue
|
|
@@ -139,6 +147,8 @@ async def list_duplicates(
|
|
|
139
147
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
140
148
|
else:
|
|
141
149
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
150
|
+
finally:
|
|
151
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
142
152
|
|
|
143
153
|
|
|
144
154
|
class ListUnusedParams(TypedDict):
|
|
@@ -152,6 +162,8 @@ async def list_unused(
|
|
|
152
162
|
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
153
163
|
|
|
154
164
|
async with client:
|
|
165
|
+
progress_total = 0
|
|
166
|
+
|
|
155
167
|
try:
|
|
156
168
|
payees = (
|
|
157
169
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
@@ -159,8 +171,9 @@ async def list_unused(
|
|
|
159
171
|
payees.sort(key=lambda p: p.name)
|
|
160
172
|
|
|
161
173
|
progress_total = len(payees)
|
|
174
|
+
await io.progress.update(total=progress_total)
|
|
162
175
|
for payee in payees:
|
|
163
|
-
await io.progress.update(
|
|
176
|
+
await io.progress.update(advance=1)
|
|
164
177
|
|
|
165
178
|
if _should_skip_payee(payee=payee):
|
|
166
179
|
continue
|
|
@@ -217,8 +230,9 @@ async def list_all(
|
|
|
217
230
|
payees.sort(key=lambda p: p.name)
|
|
218
231
|
|
|
219
232
|
progress_total = len(payees)
|
|
233
|
+
await io.progress.update(total=progress_total)
|
|
220
234
|
for payee in payees:
|
|
221
|
-
await io.progress.update(
|
|
235
|
+
await io.progress.update(advance=1)
|
|
222
236
|
|
|
223
237
|
if _should_skip_payee(payee=payee):
|
|
224
238
|
continue
|
|
@@ -230,3 +244,5 @@ async def list_all(
|
|
|
230
244
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
231
245
|
else:
|
|
232
246
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
247
|
+
finally:
|
|
248
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -47,6 +47,7 @@ async def apply_rules(
|
|
|
47
47
|
settings: Settings, io: ports.IO, params: ApplyRulesParams
|
|
48
48
|
) -> AsyncIterator[tuple[models.TransactionDetail, models.SaveTransactionWithIdOrImportId]]:
|
|
49
49
|
async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
|
|
50
|
+
progress_total = 0
|
|
50
51
|
try:
|
|
51
52
|
transactions = (
|
|
52
53
|
await util.get_asyncio_detailed(
|
|
@@ -62,8 +63,9 @@ async def apply_rules(
|
|
|
62
63
|
save_transactions = []
|
|
63
64
|
|
|
64
65
|
progress_total = len(transactions)
|
|
66
|
+
await io.progress.update(total=progress_total)
|
|
65
67
|
for transaction in transactions:
|
|
66
|
-
await io.progress.update(
|
|
68
|
+
await io.progress.update(advance=1)
|
|
67
69
|
|
|
68
70
|
if _should_skip_transaction(transaction=transaction):
|
|
69
71
|
continue
|
|
@@ -91,3 +93,5 @@ async def apply_rules(
|
|
|
91
93
|
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
92
94
|
else:
|
|
93
95
|
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
96
|
+
finally:
|
|
97
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from typing import Any, ClassVar
|
|
2
|
+
|
|
3
|
+
from textual import on, work
|
|
4
|
+
from textual.app import App, ComposeResult
|
|
5
|
+
from textual.binding import Binding, BindingType
|
|
6
|
+
from textual.containers import Horizontal, Vertical
|
|
7
|
+
from textual.reactive import reactive
|
|
8
|
+
from textual.widgets import Button, Checkbox, Footer, Header, Input, Label, TabbedContent, TabPane
|
|
9
|
+
from typing_extensions import override
|
|
10
|
+
|
|
11
|
+
from ynab_cli.domain.settings import Settings, YnabSettings
|
|
12
|
+
from ynab_cli.host.textual.widgets.categories import CategoriesTabs
|
|
13
|
+
from ynab_cli.host.textual.widgets.common.dialogs import CANCELLED, DialogForm, SaveCancelDialogScreen
|
|
14
|
+
from ynab_cli.host.textual.widgets.common.runnable_widget import RunnableWidget
|
|
15
|
+
from ynab_cli.host.textual.widgets.payees import PayeesTabs
|
|
16
|
+
from ynab_cli.host.textual.widgets.transactions 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()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class SettingsDialogForm(DialogForm[Settings]):
|
|
43
|
+
def __init__(self, settings: Settings, *args: Any, **kwargs: Any) -> None:
|
|
44
|
+
super().__init__(*args, **kwargs)
|
|
45
|
+
self._settings = settings
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
with Horizontal():
|
|
50
|
+
with Vertical():
|
|
51
|
+
yield Label("Access Token")
|
|
52
|
+
yield Input(self._settings.ynab.access_token, placeholder="Access Token", id="access_token")
|
|
53
|
+
with Vertical():
|
|
54
|
+
yield Label("Budget Id")
|
|
55
|
+
yield Input(self._settings.ynab.budget_id, placeholder="Budget Id", id="budget_id")
|
|
56
|
+
with Horizontal():
|
|
57
|
+
yield Checkbox("Debug", self._settings.debug, id="debug")
|
|
58
|
+
yield Checkbox("Dry Run", self._settings.dry_run, id="dry_run")
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
async def get_result(self) -> Settings:
|
|
62
|
+
"""Get the result from the dialog form."""
|
|
63
|
+
access_token = self.query_one("#access_token", Input).value.strip()
|
|
64
|
+
budget_id = self.query_one("#budget_id", Input).value.strip()
|
|
65
|
+
debug = self.query_one("#debug", Checkbox).value
|
|
66
|
+
dry_run = self.query_one("#dry_run", Checkbox).value
|
|
67
|
+
|
|
68
|
+
return Settings(
|
|
69
|
+
ynab=YnabSettings(
|
|
70
|
+
access_token=access_token,
|
|
71
|
+
budget_id=budget_id,
|
|
72
|
+
),
|
|
73
|
+
debug=debug,
|
|
74
|
+
dry_run=dry_run,
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class YnabCliApp(App[None]):
|
|
79
|
+
TITLE = "YNAB CLI"
|
|
80
|
+
CSS_PATH: ClassVar[str] = "app.tcss"
|
|
81
|
+
|
|
82
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
83
|
+
Binding("ctrl+c", "quit", "Quit", show=True, priority=True),
|
|
84
|
+
Binding("r", "run", "Run", show=True, priority=False),
|
|
85
|
+
]
|
|
86
|
+
|
|
87
|
+
settings: reactive[Settings] = reactive(Settings())
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
settings: Settings,
|
|
92
|
+
**kwargs: Any,
|
|
93
|
+
) -> None:
|
|
94
|
+
super().__init__(**kwargs)
|
|
95
|
+
self.settings = settings
|
|
96
|
+
|
|
97
|
+
@override
|
|
98
|
+
def compose(self) -> ComposeResult:
|
|
99
|
+
yield Header(id="header")
|
|
100
|
+
|
|
101
|
+
with Vertical(id="toolbar"):
|
|
102
|
+
yield Button("Settings", id="settings_button")
|
|
103
|
+
|
|
104
|
+
with Vertical(id="main"):
|
|
105
|
+
yield CommandTabs().data_bind(settings=YnabCliApp.settings)
|
|
106
|
+
|
|
107
|
+
yield Footer(id="footer")
|
|
108
|
+
|
|
109
|
+
def on_mount(self) -> None:
|
|
110
|
+
self.query_one(CommandTabs).query_one(TabbedContent).focus()
|
|
111
|
+
|
|
112
|
+
async def action_run(self) -> None:
|
|
113
|
+
command_tabs = self.query_one(CommandTabs)
|
|
114
|
+
await command_tabs.run_command()
|
|
115
|
+
|
|
116
|
+
@on(Button.Pressed, "#settings_button")
|
|
117
|
+
async def _settings_button_pressed(self, event: Button.Pressed) -> None:
|
|
118
|
+
self._get_settings()
|
|
119
|
+
|
|
120
|
+
@work(exclusive=True)
|
|
121
|
+
async def _get_settings(self) -> None:
|
|
122
|
+
result = await self.push_screen_wait(
|
|
123
|
+
SaveCancelDialogScreen(SettingsDialogForm(self.settings), title="Settings")
|
|
124
|
+
)
|
|
125
|
+
if result is not CANCELLED:
|
|
126
|
+
self.settings = result
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Screen {
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
& > Vertical#toolbar {
|
|
5
|
+
border: solid $primary;
|
|
6
|
+
height: auto;
|
|
7
|
+
margin-top: 1;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
& > Vertical#main {
|
|
11
|
+
height: 1fr;
|
|
12
|
+
margin-bottom: 1;
|
|
13
|
+
margin-top: 1;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
CommandTabs {
|
|
18
|
+
height: auto;
|
|
19
|
+
|
|
20
|
+
& CategoriesTabs,
|
|
21
|
+
& PayeesTabs,
|
|
22
|
+
& TransactionsTabs {
|
|
23
|
+
border: solid $primary;
|
|
24
|
+
height: auto;
|
|
25
|
+
padding-top: 1;
|
|
26
|
+
|
|
27
|
+
& DataTable {
|
|
28
|
+
border: solid $secondary;
|
|
29
|
+
height: 1fr;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
& Log {
|
|
33
|
+
border: solid $secondary;
|
|
34
|
+
height: 5;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
RunnableWidget {
|
|
40
|
+
height: auto;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
DialogScreen {
|
|
44
|
+
align: center middle;
|
|
45
|
+
|
|
46
|
+
& > Dialog {
|
|
47
|
+
background: $panel;
|
|
48
|
+
border-title-background: $panel;
|
|
49
|
+
border-title-color: $text;
|
|
50
|
+
border: solid $primary;
|
|
51
|
+
height: auto;
|
|
52
|
+
max-width: 90%;
|
|
53
|
+
width: auto;
|
|
54
|
+
|
|
55
|
+
& > Static {
|
|
56
|
+
margin:1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Dialog {
|
|
62
|
+
border: $border;
|
|
63
|
+
height: auto;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
SaveCancelDialog {
|
|
67
|
+
& > Horizontal {
|
|
68
|
+
align: right middle;
|
|
69
|
+
height: auto;
|
|
70
|
+
|
|
71
|
+
& Widget {
|
|
72
|
+
margin: 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
DialogForm {
|
|
78
|
+
border: solid $secondary;
|
|
79
|
+
height: auto;
|
|
80
|
+
|
|
81
|
+
& Static {
|
|
82
|
+
margin: 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
SettingsDialogForm {
|
|
87
|
+
height: auto;
|
|
88
|
+
|
|
89
|
+
& > Horizontal {
|
|
90
|
+
height: auto;
|
|
91
|
+
|
|
92
|
+
& > Vertical {
|
|
93
|
+
height: auto;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -6,11 +6,11 @@ from textual.logging import TextualHandler
|
|
|
6
6
|
|
|
7
7
|
from ynab_cli.domain.settings import Settings, YnabSettings
|
|
8
8
|
from ynab_cli.host.constants import CONTEXT_KEY_SETTINGS, ENV_PREFIX
|
|
9
|
-
from ynab_cli.host.textual.app import
|
|
9
|
+
from ynab_cli.host.textual.app import YnabCliApp
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
async def _tui(settings: Settings) -> None:
|
|
13
|
-
app =
|
|
13
|
+
app = YnabCliApp(settings)
|
|
14
14
|
await app.run_async()
|
|
15
15
|
|
|
16
16
|
|
|
@@ -2,9 +2,10 @@ from textual.app import ComposeResult
|
|
|
2
2
|
from textual.widgets import DataTable, Log, ProgressBar, TabbedContent, TabPane
|
|
3
3
|
from typing_extensions import override
|
|
4
4
|
|
|
5
|
-
from ynab_cli.adapters.textual.io import
|
|
5
|
+
from ynab_cli.adapters.textual.io import TextualIO
|
|
6
6
|
from ynab_cli.domain.use_cases import categories as use_cases
|
|
7
|
-
from ynab_cli.host.textual.widgets import BaseCommand
|
|
7
|
+
from ynab_cli.host.textual.widgets.common.base_command import BaseCommand
|
|
8
|
+
from ynab_cli.host.textual.widgets.common.runnable_widget import RunnableWidget
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class CategoriesTabs(RunnableWidget):
|
|
@@ -48,7 +49,7 @@ class CategoriesListUnusedCommand(BaseCommand[use_cases.ListUnusedParams]):
|
|
|
48
49
|
log = self.query_one(Log)
|
|
49
50
|
|
|
50
51
|
async for category in use_cases.list_unused(
|
|
51
|
-
self.settings,
|
|
52
|
+
self.settings, TextualIO(self.app, log, progress_bar), use_case_params
|
|
52
53
|
):
|
|
53
54
|
table.add_row(
|
|
54
55
|
category.category_group_id,
|
|
@@ -80,7 +81,7 @@ class CategoriesListAllCommand(BaseCommand[use_cases.ListAllParams]):
|
|
|
80
81
|
log = self.query_one(Log)
|
|
81
82
|
|
|
82
83
|
async for category in use_cases.list_all(
|
|
83
|
-
self.settings,
|
|
84
|
+
self.settings, TextualIO(self.app, log, progress_bar), use_case_params
|
|
84
85
|
):
|
|
85
86
|
table.add_row(
|
|
86
87
|
category.category_group_id,
|
|
@@ -1,30 +1,18 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from typing import Generic, TypeVar
|
|
3
2
|
|
|
4
|
-
from textual import work
|
|
3
|
+
from textual import on, work
|
|
5
4
|
from textual.app import ComposeResult
|
|
6
5
|
from textual.containers import Vertical
|
|
7
6
|
from textual.message import Message
|
|
8
|
-
from textual.
|
|
9
|
-
from textual.widgets import DataTable, Log, ProgressBar, Static
|
|
7
|
+
from textual.widgets import DataTable, Log, ProgressBar
|
|
10
8
|
from textual.worker import Worker, WorkerState
|
|
11
9
|
from typing_extensions import override
|
|
12
10
|
|
|
13
|
-
from ynab_cli.
|
|
14
|
-
|
|
15
|
-
log = logging.getLogger(__name__)
|
|
16
|
-
|
|
11
|
+
from ynab_cli.host.textual.widgets.common.runnable_widget import RunnableWidget
|
|
17
12
|
|
|
18
13
|
PARAMS = TypeVar("PARAMS")
|
|
19
14
|
|
|
20
15
|
|
|
21
|
-
class RunnableWidget(Static):
|
|
22
|
-
settings: var[Settings] = var(Settings())
|
|
23
|
-
|
|
24
|
-
async def run_command(self) -> None:
|
|
25
|
-
raise NotImplementedError
|
|
26
|
-
|
|
27
|
-
|
|
28
16
|
class BaseCommand(RunnableWidget, Generic[PARAMS]):
|
|
29
17
|
class CommandCompleted(Message):
|
|
30
18
|
def __init__(self, worker_state: WorkerState) -> None:
|
|
@@ -50,7 +38,8 @@ class BaseCommand(RunnableWidget, Generic[PARAMS]):
|
|
|
50
38
|
async def _run_command(self, use_case_params: PARAMS) -> None:
|
|
51
39
|
raise NotImplementedError
|
|
52
40
|
|
|
53
|
-
|
|
41
|
+
@on(Worker.StateChanged)
|
|
42
|
+
def _worker_state_changed(self, event: Worker.StateChanged) -> None:
|
|
54
43
|
log = self.query_one(Log)
|
|
55
44
|
log.write_line(f"Worker state changed: {event.state}")
|
|
56
45
|
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from .dialog import Dialog, SaveCancelDialog
|
|
2
|
+
from .form import DialogForm
|
|
3
|
+
from .screen import CANCELLED, DialogScreen, SaveCancelDialogScreen
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"CANCELLED",
|
|
7
|
+
"Dialog",
|
|
8
|
+
"DialogForm",
|
|
9
|
+
"DialogScreen",
|
|
10
|
+
"SaveCancelDialog",
|
|
11
|
+
"SaveCancelDialogScreen",
|
|
12
|
+
]
|