ynab-cli 0.5.3__tar.gz → 0.5.4__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.3 → ynab_cli-0.5.4}/CHANGELOG.md +6 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/PKG-INFO +1 -1
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/pyproject.toml +1 -1
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/models/rules.py +21 -8
- ynab_cli-0.5.4/src/ynab_cli/domain/use_cases/budgets.py +45 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/categories.py +36 -20
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/payees.py +57 -45
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/transactions.py +16 -10
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/app.py +8 -14
- ynab_cli-0.5.4/src/ynab_cli/host/textual/app.tcss +67 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories/list_all.py +38 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories/list_unused.py +38 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories/tabs.py +27 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/dialogs}/dialog.py +2 -2
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/dialogs}/screen.py +17 -4
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/list_all.py +32 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +38 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/list_unused.py +74 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +36 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/tabs.py +37 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +118 -0
- ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/transactions/tabs.py +22 -0
- ynab_cli-0.5.4/tests/factories/__init__.py +0 -0
- ynab_cli-0.5.4/tests/unit/__init__.py +0 -0
- ynab_cli-0.5.4/tests/unit/domain/__init__.py +0 -0
- ynab_cli-0.5.4/tests/unit/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/uv.lock +1 -1
- ynab_cli-0.5.3/src/ynab_cli/domain/use_cases/budgets.py +0 -37
- ynab_cli-0.5.3/src/ynab_cli/host/textual/app.tcss +0 -96
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/categories.py +0 -91
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -10
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/payees.py +0 -153
- ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/transactions.py +0 -90
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.env-sample +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/FUNDING.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/dependabot.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/workflows/pr.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/workflows/publish-to-pypi.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.gitignore +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.python-version +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/LICENSE.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/Makefile +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/README.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/docs/index.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/docs/transaction_rules.md +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/mkdocs.yml +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/rich/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/rich/io.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/textual/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/textual/io.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/client.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/errors.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/types.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/util.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/constants.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/models/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/ports/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/ports/io.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/settings.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/cli.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/cli.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/budgets.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/categories.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/payees.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/transactions.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/constants.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/cli.py +0 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets}/__init__.py +0 -0
- {ynab_cli-0.5.3/tests → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories}/__init__.py +0 -0
- {ynab_cli-0.5.3/tests/factories → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common}/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/dialogs}/__init__.py +0 -0
- {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/dialogs}/form.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -0
- {ynab_cli-0.5.3/tests/unit → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees}/__init__.py +0 -0
- {ynab_cli-0.5.3/tests/unit/domain → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/transactions}/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/py.typed +0 -0
- {ynab_cli-0.5.3/tests/unit/domain/use_cases → ynab_cli-0.5.4/tests}/__init__.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/factories/ynab.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/unit/conftest.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/unit/domain/use_cases/test_categories.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/unit/domain/use_cases/test_payees.py +0 -0
- {ynab_cli-0.5.3 → ynab_cli-0.5.4}/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.4 (2025-06-10)
|
|
9
|
+
|
|
10
|
+
### Fix
|
|
11
|
+
|
|
12
|
+
- **tui**: added some command parameter forms
|
|
13
|
+
|
|
8
14
|
## v0.5.3 (2025-06-09)
|
|
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.4
|
|
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,4 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import Any, Self
|
|
2
2
|
|
|
3
3
|
from attrs import define as _attrs_define
|
|
4
4
|
|
|
@@ -18,20 +18,33 @@ class TransactionRule:
|
|
|
18
18
|
# See: https://api.ynab.com/v1#/Transactions/updateTransactions
|
|
19
19
|
patch: models.SaveTransactionWithIdOrImportId | None = None
|
|
20
20
|
|
|
21
|
+
def to_dict(self) -> dict[str, Any]:
|
|
22
|
+
return {
|
|
23
|
+
"rules": self.rules,
|
|
24
|
+
"patch": self.patch.to_dict() if self.patch else None,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
29
|
+
rules = [str(r) for r in data.get("rules", [])]
|
|
30
|
+
patch = None
|
|
31
|
+
if "patch" in data:
|
|
32
|
+
patch = models.SaveTransactionWithIdOrImportId.from_dict(data["patch"])
|
|
33
|
+
|
|
34
|
+
return cls(rules=rules, patch=patch)
|
|
35
|
+
|
|
21
36
|
|
|
22
37
|
@_attrs_define
|
|
23
38
|
class TransactionRules:
|
|
24
39
|
transaction_rules: list[TransactionRule]
|
|
25
40
|
|
|
41
|
+
def to_dict(self) -> dict[str, Any]:
|
|
42
|
+
return {"transaction_rules": [rule.to_dict() for rule in self.transaction_rules]}
|
|
43
|
+
|
|
26
44
|
@classmethod
|
|
27
|
-
def from_dict(cls, data: dict[str, Any]) ->
|
|
45
|
+
def from_dict(cls, data: dict[str, Any]) -> Self:
|
|
28
46
|
transaction_rules = []
|
|
29
47
|
for rule in data.get("transaction_rules", []):
|
|
30
|
-
|
|
31
|
-
patch = None
|
|
32
|
-
if "patch" in rule:
|
|
33
|
-
patch = models.SaveTransactionWithIdOrImportId.from_dict(rule["patch"])
|
|
34
|
-
|
|
35
|
-
transaction_rules.append(TransactionRule(rules=rules, patch=patch))
|
|
48
|
+
transaction_rules.append(TransactionRule.from_dict(rule))
|
|
36
49
|
|
|
37
50
|
return cls(transaction_rules=transaction_rules)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from collections.abc import AsyncIterator
|
|
2
|
+
from typing import TypedDict
|
|
3
|
+
|
|
4
|
+
from ynab_cli.adapters import ynab
|
|
5
|
+
from ynab_cli.adapters.ynab import models, util
|
|
6
|
+
from ynab_cli.adapters.ynab.api.budgets import get_budgets
|
|
7
|
+
from ynab_cli.domain import ports
|
|
8
|
+
from ynab_cli.domain.constants import YNAB_API_URL
|
|
9
|
+
from ynab_cli.domain.settings import Settings
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ListAllParams(TypedDict):
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
async def list_all(
|
|
17
|
+
settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
|
|
18
|
+
) -> AsyncIterator[models.BudgetSummary]:
|
|
19
|
+
try:
|
|
20
|
+
progress_total = 0
|
|
21
|
+
|
|
22
|
+
if client is None:
|
|
23
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
24
|
+
|
|
25
|
+
async with client:
|
|
26
|
+
budgets = (
|
|
27
|
+
await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=True, client=client)
|
|
28
|
+
).data.budgets
|
|
29
|
+
budgets.sort(key=lambda b: b.name)
|
|
30
|
+
|
|
31
|
+
progress_total = len(budgets)
|
|
32
|
+
await io.progress.update(total=progress_total)
|
|
33
|
+
for budget in budgets:
|
|
34
|
+
await io.progress.update(advance=1)
|
|
35
|
+
yield budget
|
|
36
|
+
|
|
37
|
+
except Exception as e:
|
|
38
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
39
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
40
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
41
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
42
|
+
else:
|
|
43
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
44
|
+
finally:
|
|
45
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -20,10 +20,16 @@ class ListUnusedParams(TypedDict):
|
|
|
20
20
|
pass
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
async def list_unused(
|
|
24
|
-
|
|
23
|
+
async def list_unused(
|
|
24
|
+
settings: Settings, io: ports.IO, params: ListUnusedParams, *, client: ynab.AuthenticatedClient | None = None
|
|
25
|
+
) -> AsyncIterator[models.Category]:
|
|
26
|
+
try:
|
|
25
27
|
progress_total = 0
|
|
26
|
-
|
|
28
|
+
|
|
29
|
+
if client is None:
|
|
30
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
31
|
+
|
|
32
|
+
async with client:
|
|
27
33
|
category_groups = (
|
|
28
34
|
await util.get_asyncio_detailed(
|
|
29
35
|
io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=client
|
|
@@ -63,23 +69,31 @@ async def list_unused(settings: Settings, io: ports.IO, params: ListUnusedParams
|
|
|
63
69
|
if not num_transactions:
|
|
64
70
|
yield category
|
|
65
71
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
await io.
|
|
72
|
+
except Exception as e:
|
|
73
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
74
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
75
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
76
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
77
|
+
else:
|
|
78
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
79
|
+
finally:
|
|
80
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
73
81
|
|
|
74
82
|
|
|
75
83
|
class ListAllParams(TypedDict):
|
|
76
84
|
pass
|
|
77
85
|
|
|
78
86
|
|
|
79
|
-
async def list_all(
|
|
80
|
-
|
|
87
|
+
async def list_all(
|
|
88
|
+
settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
|
|
89
|
+
) -> AsyncIterator[models.Category]:
|
|
90
|
+
try:
|
|
81
91
|
progress_total = 0
|
|
82
|
-
|
|
92
|
+
|
|
93
|
+
if client is None:
|
|
94
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
95
|
+
|
|
96
|
+
async with client:
|
|
83
97
|
category_groups = (
|
|
84
98
|
await util.get_asyncio_detailed(
|
|
85
99
|
io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=client
|
|
@@ -106,10 +120,12 @@ async def list_all(settings: Settings, io: ports.IO, params: ListAllParams) -> A
|
|
|
106
120
|
|
|
107
121
|
yield category
|
|
108
122
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
await io.
|
|
123
|
+
except Exception as e:
|
|
124
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
125
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
126
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
127
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
128
|
+
else:
|
|
129
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
130
|
+
finally:
|
|
131
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -47,13 +47,13 @@ class NormalizeNamesParams(TypedDict):
|
|
|
47
47
|
async def normalize_names(
|
|
48
48
|
settings: Settings, io: ports.IO, params: NormalizeNamesParams, *, client: ynab.AuthenticatedClient | None = None
|
|
49
49
|
) -> AsyncIterator[tuple[models.Payee, str]]:
|
|
50
|
-
|
|
51
|
-
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
52
|
-
|
|
53
|
-
async with client:
|
|
50
|
+
try:
|
|
54
51
|
progress_total = 0
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
if client is None:
|
|
54
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
55
|
+
|
|
56
|
+
async with client:
|
|
57
57
|
payees = (
|
|
58
58
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
59
59
|
).data.payees
|
|
@@ -81,13 +81,15 @@ async def normalize_names(
|
|
|
81
81
|
body=models.PatchPayeeWrapper(payee=models.SavePayee(name=normalized_name)),
|
|
82
82
|
)
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
await io.
|
|
84
|
+
except Exception as e:
|
|
85
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
86
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
87
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
88
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
89
|
+
else:
|
|
90
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
91
|
+
finally:
|
|
92
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
91
93
|
|
|
92
94
|
|
|
93
95
|
class ListDuplicatesParams(TypedDict):
|
|
@@ -97,13 +99,13 @@ class ListDuplicatesParams(TypedDict):
|
|
|
97
99
|
async def list_duplicates(
|
|
98
100
|
settings: Settings, io: ports.IO, params: ListDuplicatesParams, *, client: ynab.AuthenticatedClient | None = None
|
|
99
101
|
) -> AsyncIterator[tuple[models.Payee, models.Payee]]:
|
|
100
|
-
|
|
101
|
-
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
102
|
-
|
|
103
|
-
async with client:
|
|
102
|
+
try:
|
|
104
103
|
progress_total = 0
|
|
105
104
|
|
|
106
|
-
|
|
105
|
+
if client is None:
|
|
106
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
107
|
+
|
|
108
|
+
async with client:
|
|
107
109
|
possible_duplicates: dict[tuple[UUID, str], list[tuple[UUID, str]]] = {}
|
|
108
110
|
|
|
109
111
|
payees = (
|
|
@@ -142,13 +144,15 @@ async def list_duplicates(
|
|
|
142
144
|
|
|
143
145
|
yield (payee, filtered_payee)
|
|
144
146
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
await io.
|
|
147
|
+
except Exception as e:
|
|
148
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
149
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
150
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
151
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
152
|
+
else:
|
|
153
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
154
|
+
finally:
|
|
155
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
152
156
|
|
|
153
157
|
|
|
154
158
|
class ListUnusedParams(TypedDict):
|
|
@@ -158,13 +162,13 @@ class ListUnusedParams(TypedDict):
|
|
|
158
162
|
async def list_unused(
|
|
159
163
|
settings: Settings, io: ports.IO, params: ListUnusedParams, *, client: ynab.AuthenticatedClient | None = None
|
|
160
164
|
) -> AsyncIterator[models.Payee]:
|
|
161
|
-
|
|
162
|
-
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
163
|
-
|
|
164
|
-
async with client:
|
|
165
|
+
try:
|
|
165
166
|
progress_total = 0
|
|
166
167
|
|
|
167
|
-
|
|
168
|
+
if client is None:
|
|
169
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
170
|
+
|
|
171
|
+
async with client:
|
|
168
172
|
payees = (
|
|
169
173
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
170
174
|
).data.payees
|
|
@@ -205,11 +209,15 @@ async def list_unused(
|
|
|
205
209
|
body=models.PatchPayeeWrapper(payee=models.SavePayee(name=new_name)),
|
|
206
210
|
)
|
|
207
211
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
except Exception as e:
|
|
213
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
214
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
215
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
216
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
217
|
+
else:
|
|
218
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
219
|
+
finally:
|
|
220
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
213
221
|
|
|
214
222
|
|
|
215
223
|
class ListAllParams(TypedDict):
|
|
@@ -219,11 +227,13 @@ class ListAllParams(TypedDict):
|
|
|
219
227
|
async def list_all(
|
|
220
228
|
settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
|
|
221
229
|
) -> AsyncIterator[models.Payee]:
|
|
222
|
-
|
|
223
|
-
|
|
230
|
+
try:
|
|
231
|
+
progress_total = 0
|
|
232
|
+
|
|
233
|
+
if client is None:
|
|
234
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
224
235
|
|
|
225
|
-
|
|
226
|
-
try:
|
|
236
|
+
async with client:
|
|
227
237
|
payees = (
|
|
228
238
|
await util.get_asyncio_detailed(io, get_payees.asyncio_detailed, settings.ynab.budget_id, client=client)
|
|
229
239
|
).data.payees
|
|
@@ -239,10 +249,12 @@ async def list_all(
|
|
|
239
249
|
|
|
240
250
|
yield payee
|
|
241
251
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
await io.
|
|
252
|
+
except Exception as e:
|
|
253
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
254
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
255
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
256
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
257
|
+
else:
|
|
258
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
259
|
+
finally:
|
|
260
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -44,11 +44,15 @@ class ApplyRulesParams(TypedDict):
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
async def apply_rules(
|
|
47
|
-
settings: Settings, io: ports.IO, params: ApplyRulesParams
|
|
47
|
+
settings: Settings, io: ports.IO, params: ApplyRulesParams, *, client: ynab.AuthenticatedClient | None = None
|
|
48
48
|
) -> AsyncIterator[tuple[models.TransactionDetail, models.SaveTransactionWithIdOrImportId]]:
|
|
49
|
-
|
|
49
|
+
try:
|
|
50
50
|
progress_total = 0
|
|
51
|
-
|
|
51
|
+
|
|
52
|
+
if client is None:
|
|
53
|
+
client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
|
|
54
|
+
|
|
55
|
+
async with client:
|
|
52
56
|
transactions = (
|
|
53
57
|
await util.get_asyncio_detailed(
|
|
54
58
|
io,
|
|
@@ -88,10 +92,12 @@ async def apply_rules(
|
|
|
88
92
|
body=models.PatchTransactionsWrapper(transactions=save_transactions),
|
|
89
93
|
)
|
|
90
94
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
await io.
|
|
95
|
+
except Exception as e:
|
|
96
|
+
if isinstance(e, util.ApiError) and e.status_code == 401:
|
|
97
|
+
await io.print("Invalid or expired access token. Please update your settings.")
|
|
98
|
+
elif isinstance(e, util.ApiError) and e.status_code == 429:
|
|
99
|
+
await io.print("API rate limit exceeded. Try again later, or get a new access token.")
|
|
100
|
+
else:
|
|
101
|
+
await io.print(f"Exception when calling YNAB: {e}\n")
|
|
102
|
+
finally:
|
|
103
|
+
await io.progress.update(total=progress_total, completed=progress_total)
|
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
from typing import Any, ClassVar
|
|
2
2
|
|
|
3
|
-
from textual import
|
|
3
|
+
from textual import work
|
|
4
4
|
from textual.app import App, ComposeResult
|
|
5
5
|
from textual.binding import Binding, BindingType
|
|
6
6
|
from textual.containers import Horizontal, Vertical
|
|
7
7
|
from textual.reactive import reactive
|
|
8
|
-
from textual.widgets import
|
|
8
|
+
from textual.widgets import Checkbox, Footer, Header, Input, Label, TabbedContent, TabPane
|
|
9
9
|
from typing_extensions import override
|
|
10
10
|
|
|
11
11
|
from ynab_cli.domain.settings import Settings, YnabSettings
|
|
12
|
-
from ynab_cli.host.textual.widgets.categories import CategoriesTabs
|
|
12
|
+
from ynab_cli.host.textual.widgets.categories.tabs import CategoriesTabs
|
|
13
13
|
from ynab_cli.host.textual.widgets.common.dialogs import CANCELLED, DialogForm, SaveCancelDialogScreen
|
|
14
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
|
|
15
|
+
from ynab_cli.host.textual.widgets.payees.tabs import PayeesTabs
|
|
16
|
+
from ynab_cli.host.textual.widgets.transactions.tabs import TransactionsTabs
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
class CommandTabs(RunnableWidget):
|
|
@@ -81,6 +81,7 @@ class YnabCliApp(App[None]):
|
|
|
81
81
|
|
|
82
82
|
BINDINGS: ClassVar[list[BindingType]] = [
|
|
83
83
|
Binding("ctrl+c", "quit", "Quit", show=True, priority=True),
|
|
84
|
+
Binding("s", "settings", "Settings", show=True, priority=False),
|
|
84
85
|
Binding("r", "run", "Run", show=True, priority=False),
|
|
85
86
|
]
|
|
86
87
|
|
|
@@ -97,13 +98,7 @@ class YnabCliApp(App[None]):
|
|
|
97
98
|
@override
|
|
98
99
|
def compose(self) -> ComposeResult:
|
|
99
100
|
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
|
-
|
|
101
|
+
yield CommandTabs().data_bind(settings=YnabCliApp.settings)
|
|
107
102
|
yield Footer(id="footer")
|
|
108
103
|
|
|
109
104
|
def on_mount(self) -> None:
|
|
@@ -113,8 +108,7 @@ class YnabCliApp(App[None]):
|
|
|
113
108
|
command_tabs = self.query_one(CommandTabs)
|
|
114
109
|
await command_tabs.run_command()
|
|
115
110
|
|
|
116
|
-
|
|
117
|
-
async def _settings_button_pressed(self, event: Button.Pressed) -> None:
|
|
111
|
+
async def action_settings(self) -> None:
|
|
118
112
|
self._get_settings()
|
|
119
113
|
|
|
120
114
|
@work(exclusive=True)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
CommandTabs {
|
|
2
|
+
height: 1fr;
|
|
3
|
+
padding-top: 1;
|
|
4
|
+
|
|
5
|
+
& CategoriesTabs,
|
|
6
|
+
& PayeesTabs,
|
|
7
|
+
& TransactionsTabs {
|
|
8
|
+
border: solid $primary;
|
|
9
|
+
height: auto;
|
|
10
|
+
padding-top: 1;
|
|
11
|
+
|
|
12
|
+
& DataTable {
|
|
13
|
+
border: solid $secondary;
|
|
14
|
+
height: 1fr;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
& Log {
|
|
18
|
+
border: solid $secondary;
|
|
19
|
+
height: 8;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
RunnableWidget {
|
|
25
|
+
height: auto;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
DialogScreen {
|
|
29
|
+
align: center middle;
|
|
30
|
+
|
|
31
|
+
& > Dialog {
|
|
32
|
+
border-title-background: $panel;
|
|
33
|
+
border-title-color: $text;
|
|
34
|
+
border: solid $primary;
|
|
35
|
+
height: auto;
|
|
36
|
+
max-height: 90%;
|
|
37
|
+
max-width: 90%;
|
|
38
|
+
width: auto;
|
|
39
|
+
|
|
40
|
+
& > DialogForm {
|
|
41
|
+
border: solid $secondary;
|
|
42
|
+
height: auto;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
& Horizontal, Vertical {
|
|
46
|
+
height: auto;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
& Button, Input, Select, TextArea, Label {
|
|
50
|
+
margin: 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
& > SaveCancelDialog {
|
|
55
|
+
& > Horizontal {
|
|
56
|
+
align: right middle;
|
|
57
|
+
height: auto;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ApplyRulesParamsDialogForm {
|
|
63
|
+
& > ScrollableContainer {
|
|
64
|
+
border: solid $secondary;
|
|
65
|
+
height: 10;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from textual.widgets import DataTable, Log, ProgressBar
|
|
2
|
+
from typing_extensions import override
|
|
3
|
+
|
|
4
|
+
from ynab_cli.adapters.textual.io import TextualIO
|
|
5
|
+
from ynab_cli.domain.use_cases import categories as use_cases
|
|
6
|
+
from ynab_cli.host.textual.widgets.common.base_command import BaseCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ListAllCommand(BaseCommand[use_cases.ListAllParams]):
|
|
10
|
+
def on_mount(self) -> None:
|
|
11
|
+
table = self.query_one(DataTable)
|
|
12
|
+
table.add_columns(
|
|
13
|
+
"Category Group Id",
|
|
14
|
+
"Category Group Name",
|
|
15
|
+
"Category Id",
|
|
16
|
+
"Category Name",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
async def run_command(self) -> None:
|
|
21
|
+
params: use_cases.ListAllParams = {}
|
|
22
|
+
self._run_command_worker(params)
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
async def _run_command(self, use_case_params: use_cases.ListAllParams) -> None:
|
|
26
|
+
progress_bar = self.query_one(ProgressBar)
|
|
27
|
+
table = self.query_one(DataTable)
|
|
28
|
+
log = self.query_one(Log)
|
|
29
|
+
|
|
30
|
+
async for category in use_cases.list_all(
|
|
31
|
+
self.settings, TextualIO(self.app, log, progress_bar), use_case_params
|
|
32
|
+
):
|
|
33
|
+
table.add_row(
|
|
34
|
+
category.category_group_id,
|
|
35
|
+
category.category_group_name,
|
|
36
|
+
category.id,
|
|
37
|
+
category.name,
|
|
38
|
+
)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from textual.widgets import DataTable, Log, ProgressBar
|
|
2
|
+
from typing_extensions import override
|
|
3
|
+
|
|
4
|
+
from ynab_cli.adapters.textual.io import TextualIO
|
|
5
|
+
from ynab_cli.domain.use_cases import categories as use_cases
|
|
6
|
+
from ynab_cli.host.textual.widgets.common.base_command import BaseCommand
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ListUnusedCommand(BaseCommand[use_cases.ListUnusedParams]):
|
|
10
|
+
def on_mount(self) -> None:
|
|
11
|
+
table = self.query_one(DataTable)
|
|
12
|
+
table.add_columns(
|
|
13
|
+
"Category Group Id",
|
|
14
|
+
"Category Group Name",
|
|
15
|
+
"Category Id",
|
|
16
|
+
"Category Name",
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
@override
|
|
20
|
+
async def run_command(self) -> None:
|
|
21
|
+
params: use_cases.ListUnusedParams = {}
|
|
22
|
+
self._run_command_worker(params)
|
|
23
|
+
|
|
24
|
+
@override
|
|
25
|
+
async def _run_command(self, use_case_params: use_cases.ListUnusedParams) -> None:
|
|
26
|
+
progress_bar = self.query_one(ProgressBar)
|
|
27
|
+
table = self.query_one(DataTable)
|
|
28
|
+
log = self.query_one(Log)
|
|
29
|
+
|
|
30
|
+
async for category in use_cases.list_unused(
|
|
31
|
+
self.settings, TextualIO(self.app, log, progress_bar), use_case_params
|
|
32
|
+
):
|
|
33
|
+
table.add_row(
|
|
34
|
+
category.category_group_id,
|
|
35
|
+
category.category_group_name,
|
|
36
|
+
category.id,
|
|
37
|
+
category.name,
|
|
38
|
+
)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.widgets import TabbedContent, TabPane
|
|
3
|
+
from typing_extensions import override
|
|
4
|
+
|
|
5
|
+
from ynab_cli.host.textual.widgets.common.runnable_widget import RunnableWidget
|
|
6
|
+
|
|
7
|
+
from .list_all import ListAllCommand
|
|
8
|
+
from .list_unused import ListUnusedCommand
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CategoriesTabs(RunnableWidget):
|
|
12
|
+
@override
|
|
13
|
+
def compose(self) -> ComposeResult:
|
|
14
|
+
with TabbedContent():
|
|
15
|
+
with TabPane("Unused", id="unused"):
|
|
16
|
+
yield ListUnusedCommand().data_bind(settings=CategoriesTabs.settings)
|
|
17
|
+
with TabPane("All", id="all"):
|
|
18
|
+
yield ListAllCommand().data_bind(settings=CategoriesTabs.settings)
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
async def run_command(self) -> None:
|
|
22
|
+
tabbed_content = self.query_one(TabbedContent)
|
|
23
|
+
if tabbed_content.active_pane:
|
|
24
|
+
if tabbed_content.active_pane.id == "unused":
|
|
25
|
+
await self.query_one(ListUnusedCommand).run_command()
|
|
26
|
+
elif tabbed_content.active_pane.id == "all":
|
|
27
|
+
await self.query_one(ListAllCommand).run_command()
|
|
@@ -44,8 +44,8 @@ class SaveCancelDialog(Dialog[DialogResultType]):
|
|
|
44
44
|
yield from super().compose()
|
|
45
45
|
|
|
46
46
|
with Horizontal():
|
|
47
|
-
yield Button("Cancel", id="cancel")
|
|
48
|
-
yield Button("Save", id="save")
|
|
47
|
+
yield Button("Cancel", id="cancel", variant="error")
|
|
48
|
+
yield Button("Save", id="save", variant="primary")
|
|
49
49
|
|
|
50
50
|
@on(Button.Pressed, "#cancel")
|
|
51
51
|
async def _cancel_button_pressed(self, _: Button.Pressed) -> None:
|