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.
Files changed (239) hide show
  1. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/CHANGELOG.md +6 -0
  2. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/PKG-INFO +1 -1
  3. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/pyproject.toml +1 -1
  4. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/textual/io.py +12 -14
  5. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/util.py +3 -0
  6. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/budgets.py +5 -1
  7. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/categories.py +14 -4
  8. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/payees.py +20 -4
  9. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/transactions.py +5 -1
  10. ynab_cli-0.5.3/src/ynab_cli/host/textual/app.py +126 -0
  11. ynab_cli-0.5.3/src/ynab_cli/host/textual/app.tcss +96 -0
  12. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/cli.py +2 -2
  13. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/widgets/categories.py +5 -4
  14. 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
  15. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +10 -0
  16. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/__init__.py +12 -0
  17. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/dialog.py +63 -0
  18. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/form.py +11 -0
  19. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base/screen.py +40 -0
  20. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +11 -0
  21. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/widgets/payees.py +7 -8
  22. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/widgets/transactions.py +4 -3
  23. ynab_cli-0.5.3/tests/unit/domain/use_cases/__init__.py +0 -0
  24. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/uv.lock +1 -1
  25. ynab_cli-0.5.2/src/ynab_cli/host/textual/app.py +0 -98
  26. ynab_cli-0.5.2/src/ynab_cli/host/textual/app.tcss +0 -47
  27. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.env-sample +0 -0
  28. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/FUNDING.yml +0 -0
  29. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/dependabot.yml +0 -0
  30. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/workflows/pr.yml +0 -0
  31. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.github/workflows/publish-to-pypi.yml +0 -0
  32. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.gitignore +0 -0
  33. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/.python-version +0 -0
  34. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/LICENSE.md +0 -0
  35. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/Makefile +0 -0
  36. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/README.md +0 -0
  37. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/docs/index.md +0 -0
  38. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/docs/transaction_rules.md +0 -0
  39. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/mkdocs.yml +0 -0
  40. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/__init__.py +0 -0
  41. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/__init__.py +0 -0
  42. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/rich/__init__.py +0 -0
  43. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/rich/io.py +0 -0
  44. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/textual/__init__.py +0 -0
  45. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
  46. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
  47. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
  48. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
  49. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
  50. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
  51. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
  52. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
  53. {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
  54. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
  55. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
  56. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
  57. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
  58. {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
  59. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
  60. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
  61. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
  62. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
  63. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
  64. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
  65. {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
  66. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
  67. {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
  68. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
  69. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
  70. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
  71. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
  72. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
  73. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
  74. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
  75. {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
  76. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
  77. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
  78. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
  79. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
  80. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
  81. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
  82. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
  83. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
  84. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
  85. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
  86. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
  87. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
  88. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
  89. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
  90. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
  91. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
  92. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/client.py +0 -0
  93. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/errors.py +0 -0
  94. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
  95. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
  96. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
  97. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
  98. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
  99. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
  100. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
  101. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
  102. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
  103. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
  104. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
  105. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
  106. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
  107. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
  108. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
  109. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
  110. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
  111. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
  112. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
  113. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
  114. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
  115. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
  116. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
  117. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
  118. {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
  119. {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
  120. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
  121. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
  122. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
  123. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
  124. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
  125. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
  126. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
  127. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
  128. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
  129. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
  130. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
  131. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
  132. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
  133. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
  134. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
  135. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
  136. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
  137. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
  138. {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
  139. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
  140. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
  141. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
  142. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
  143. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
  144. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
  145. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
  146. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
  147. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
  148. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
  149. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
  150. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
  151. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
  152. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
  153. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
  154. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
  155. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
  156. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
  157. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
  158. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
  159. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
  160. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
  161. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
  162. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
  163. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
  164. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
  165. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
  166. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
  167. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
  168. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
  169. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
  170. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
  171. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
  172. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
  173. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
  174. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
  175. {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
  176. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
  177. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
  178. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
  179. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
  180. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
  181. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
  182. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
  183. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
  184. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
  185. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
  186. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
  187. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
  188. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
  189. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
  190. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
  191. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
  192. {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
  193. {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
  194. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
  195. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
  196. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
  197. {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
  198. {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
  199. {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
  200. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
  201. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
  202. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
  203. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
  204. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
  205. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
  206. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
  207. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/adapters/ynab/types.py +0 -0
  208. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/__init__.py +0 -0
  209. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/constants.py +0 -0
  210. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/models/__init__.py +0 -0
  211. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/models/rules.py +0 -0
  212. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/ports/__init__.py +0 -0
  213. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/ports/io.py +0 -0
  214. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/settings.py +0 -0
  215. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
  216. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/__init__.py +0 -0
  217. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/cli.py +0 -0
  218. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/__init__.py +0 -0
  219. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/cli.py +0 -0
  220. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/__init__.py +0 -0
  221. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/budgets.py +0 -0
  222. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/categories.py +0 -0
  223. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/payees.py +0 -0
  224. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
  225. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
  226. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/click/commands/transactions.py +0 -0
  227. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/constants.py +0 -0
  228. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/host/textual/__init__.py +0 -0
  229. {ynab_cli-0.5.2/tests → ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common}/__init__.py +0 -0
  230. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/src/ynab_cli/py.typed +0 -0
  231. {ynab_cli-0.5.2/tests/factories → ynab_cli-0.5.3/tests}/__init__.py +0 -0
  232. {ynab_cli-0.5.2/tests/unit → ynab_cli-0.5.3/tests/factories}/__init__.py +0 -0
  233. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/factories/ynab.py +0 -0
  234. {ynab_cli-0.5.2/tests/unit/domain → ynab_cli-0.5.3/tests/unit}/__init__.py +0 -0
  235. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/conftest.py +0 -0
  236. {ynab_cli-0.5.2/tests/unit/domain/use_cases → ynab_cli-0.5.3/tests/unit/domain}/__init__.py +0 -0
  237. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/domain/use_cases/test_categories.py +0 -0
  238. {ynab_cli-0.5.2 → ynab_cli-0.5.3}/tests/unit/domain/use_cases/test_payees.py +0 -0
  239. {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.2
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "ynab-cli"
3
- version = "0.5.2"
3
+ version = "0.5.3"
4
4
  description = "YNAB Command Line Interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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 IOScreen(ModalScreen[str]):
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
- with Vertical():
42
- yield Label(self._prompt)
43
- yield Input(password=self._password)
37
+ yield Label(self._prompt)
38
+ yield Input(password=self._password)
44
39
 
45
- @on(Input.Submitted)
46
- def return_response(self, event: Input.Submitted) -> None:
47
- self.dismiss(event.value)
40
+ @override
41
+ async def get_result(self) -> str:
42
+ return self.query_one(Input).value.strip()
48
43
 
49
44
 
50
- class TextualWorkerIO(IO):
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
- return await self._app.push_screen_wait(IOScreen(prompt, password))
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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(total=progress_total, advance=1)
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 App
9
+ from ynab_cli.host.textual.app import YnabCliApp
10
10
 
11
11
 
12
12
  async def _tui(settings: Settings) -> None:
13
- app = App(settings)
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 TextualWorkerIO
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, RunnableWidget
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, TextualWorkerIO(self.app, log, progress_bar), use_case_params
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, TextualWorkerIO(self.app, log, progress_bar), use_case_params
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.reactive import var
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.domain.settings import Settings
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
- def on_worker_state_changed(self, event: Worker.StateChanged) -> None:
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,10 @@
1
+ from .base import CANCELLED, Dialog, DialogForm, DialogScreen, SaveCancelDialog, SaveCancelDialogScreen
2
+
3
+ __all__ = [
4
+ "CANCELLED",
5
+ "Dialog",
6
+ "DialogForm",
7
+ "DialogScreen",
8
+ "SaveCancelDialog",
9
+ "SaveCancelDialogScreen",
10
+ ]
@@ -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
+ ]