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.
Files changed (253) hide show
  1. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/CHANGELOG.md +6 -0
  2. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/PKG-INFO +1 -1
  3. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/pyproject.toml +1 -1
  4. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/models/rules.py +21 -8
  5. ynab_cli-0.5.4/src/ynab_cli/domain/use_cases/budgets.py +45 -0
  6. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/categories.py +36 -20
  7. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/payees.py +57 -45
  8. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/transactions.py +16 -10
  9. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/app.py +8 -14
  10. ynab_cli-0.5.4/src/ynab_cli/host/textual/app.tcss +67 -0
  11. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories/list_all.py +38 -0
  12. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories/list_unused.py +38 -0
  13. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories/tabs.py +27 -0
  14. {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
  15. {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
  16. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/list_all.py +32 -0
  17. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +38 -0
  18. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/list_unused.py +74 -0
  19. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +36 -0
  20. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees/tabs.py +37 -0
  21. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +118 -0
  22. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/transactions/tabs.py +22 -0
  23. ynab_cli-0.5.4/tests/factories/__init__.py +0 -0
  24. ynab_cli-0.5.4/tests/unit/__init__.py +0 -0
  25. ynab_cli-0.5.4/tests/unit/domain/__init__.py +0 -0
  26. ynab_cli-0.5.4/tests/unit/domain/use_cases/__init__.py +0 -0
  27. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/uv.lock +1 -1
  28. ynab_cli-0.5.3/src/ynab_cli/domain/use_cases/budgets.py +0 -37
  29. ynab_cli-0.5.3/src/ynab_cli/host/textual/app.tcss +0 -96
  30. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/categories.py +0 -91
  31. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -10
  32. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/payees.py +0 -153
  33. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/transactions.py +0 -90
  34. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.env-sample +0 -0
  35. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/FUNDING.yml +0 -0
  36. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/dependabot.yml +0 -0
  37. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/workflows/pr.yml +0 -0
  38. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.github/workflows/publish-to-pypi.yml +0 -0
  39. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.gitignore +0 -0
  40. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/.python-version +0 -0
  41. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/LICENSE.md +0 -0
  42. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/Makefile +0 -0
  43. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/README.md +0 -0
  44. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/docs/index.md +0 -0
  45. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/docs/transaction_rules.md +0 -0
  46. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/mkdocs.yml +0 -0
  47. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/__init__.py +0 -0
  48. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/__init__.py +0 -0
  49. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/rich/__init__.py +0 -0
  50. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/rich/io.py +0 -0
  51. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/textual/__init__.py +0 -0
  52. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/textual/io.py +0 -0
  53. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
  54. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
  55. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
  56. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
  57. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
  58. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
  59. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
  60. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
  61. {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
  62. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
  63. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
  64. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
  65. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
  66. {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
  67. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
  68. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
  69. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
  70. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
  71. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
  72. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
  73. {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
  74. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
  75. {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
  76. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
  77. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
  78. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
  79. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
  80. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
  81. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
  82. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
  83. {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
  84. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
  85. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
  86. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
  87. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
  88. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
  89. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
  90. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
  91. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
  92. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
  93. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
  94. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
  95. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
  96. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
  97. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
  98. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
  99. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
  100. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/client.py +0 -0
  101. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/errors.py +0 -0
  102. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
  103. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
  104. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
  105. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
  106. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
  107. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
  108. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
  109. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
  110. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
  111. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
  112. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
  113. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
  114. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
  115. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
  116. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
  117. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
  118. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
  119. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
  120. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
  121. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
  122. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
  123. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
  124. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
  125. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
  126. {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
  127. {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
  128. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
  129. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
  130. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
  131. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
  132. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
  133. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
  134. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
  135. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
  136. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
  137. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
  138. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
  139. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
  140. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
  141. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
  142. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
  143. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
  144. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
  145. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
  146. {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
  147. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
  148. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
  149. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
  150. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
  151. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
  152. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
  153. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
  154. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
  155. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
  156. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
  157. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
  158. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
  159. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
  160. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
  161. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
  162. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
  163. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
  164. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
  165. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
  166. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
  167. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
  168. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
  169. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
  170. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
  171. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
  172. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
  173. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
  174. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
  175. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
  176. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
  177. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
  178. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
  179. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
  180. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
  181. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
  182. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
  183. {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
  184. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
  185. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
  186. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
  187. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
  188. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
  189. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
  190. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
  191. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
  192. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
  193. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
  194. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
  195. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
  196. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
  197. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
  198. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
  199. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
  200. {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
  201. {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
  202. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
  203. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
  204. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
  205. {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
  206. {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
  207. {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
  208. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
  209. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
  210. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
  211. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
  212. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
  213. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
  214. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
  215. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/types.py +0 -0
  216. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/adapters/ynab/util.py +0 -0
  217. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/__init__.py +0 -0
  218. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/constants.py +0 -0
  219. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/models/__init__.py +0 -0
  220. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/ports/__init__.py +0 -0
  221. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/ports/io.py +0 -0
  222. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/settings.py +0 -0
  223. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
  224. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/__init__.py +0 -0
  225. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/cli.py +0 -0
  226. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/__init__.py +0 -0
  227. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/cli.py +0 -0
  228. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/__init__.py +0 -0
  229. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/budgets.py +0 -0
  230. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/categories.py +0 -0
  231. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/payees.py +0 -0
  232. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
  233. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
  234. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/click/commands/transactions.py +0 -0
  235. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/constants.py +0 -0
  236. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/__init__.py +0 -0
  237. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/cli.py +0 -0
  238. {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
  239. {ynab_cli-0.5.3/tests → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/categories}/__init__.py +0 -0
  240. {ynab_cli-0.5.3/tests/factories → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common}/__init__.py +0 -0
  241. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -0
  242. {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
  243. {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
  244. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -0
  245. {ynab_cli-0.5.3/tests/unit → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/payees}/__init__.py +0 -0
  246. {ynab_cli-0.5.3/tests/unit/domain → ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/transactions}/__init__.py +0 -0
  247. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/src/ynab_cli/py.typed +0 -0
  248. {ynab_cli-0.5.3/tests/unit/domain/use_cases → ynab_cli-0.5.4/tests}/__init__.py +0 -0
  249. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/factories/ynab.py +0 -0
  250. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/unit/conftest.py +0 -0
  251. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/unit/domain/use_cases/test_categories.py +0 -0
  252. {ynab_cli-0.5.3 → ynab_cli-0.5.4}/tests/unit/domain/use_cases/test_payees.py +0 -0
  253. {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
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,6 +1,6 @@
1
1
  [project]
2
2
  name = "ynab-cli"
3
- version = "0.5.3"
3
+ version = "0.5.4"
4
4
  description = "YNAB Command Line Interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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]) -> "TransactionRules":
45
+ def from_dict(cls, data: dict[str, Any]) -> Self:
28
46
  transaction_rules = []
29
47
  for rule in data.get("transaction_rules", []):
30
- rules = [str(r) for r in rule.get("rules", [])]
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(settings: Settings, io: ports.IO, params: ListUnusedParams) -> AsyncIterator[models.Category]:
24
- async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
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
- try:
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
- except util.ApiError as e:
67
- if e.status_code == 429:
68
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
69
- else:
70
- await io.print(f"Exception when calling YNAB: {e}\n")
71
- finally:
72
- await io.progress.update(total=progress_total, completed=progress_total)
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(settings: Settings, io: ports.IO, params: ListAllParams) -> AsyncIterator[models.Category]:
80
- async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
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
- try:
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
- except util.ApiError as e:
110
- if e.status_code == 429:
111
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
112
- else:
113
- await io.print(f"Exception when calling YNAB: {e}\n")
114
- finally:
115
- await io.progress.update(total=progress_total, completed=progress_total)
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
- if client is None:
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
- try:
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
- except util.ApiError as e:
85
- if e.status_code == 429:
86
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
87
- else:
88
- await io.print(f"Exception when calling YNAB: {e}\n")
89
- finally:
90
- await io.progress.update(total=progress_total, completed=progress_total)
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
- if client is None:
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
- try:
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
- except util.ApiError as e:
146
- if e.status_code == 429:
147
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
148
- else:
149
- await io.print(f"Exception when calling YNAB: {e}\n")
150
- finally:
151
- await io.progress.update(total=progress_total, completed=progress_total)
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
- if client is None:
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
- try:
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
- except util.ApiError as e:
209
- if e.status_code == 429:
210
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
211
- else:
212
- await io.print(f"Exception when calling YNAB: {e}\n")
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
- if client is None:
223
- client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
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
- async with client:
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
- except util.ApiError as e:
243
- if e.status_code == 429:
244
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
245
- else:
246
- await io.print(f"Exception when calling YNAB: {e}\n")
247
- finally:
248
- await io.progress.update(total=progress_total, completed=progress_total)
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
- async with ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token) as client:
49
+ try:
50
50
  progress_total = 0
51
- try:
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
- except util.ApiError as e:
92
- if e.status_code == 429:
93
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
94
- else:
95
- await io.print(f"Exception when calling YNAB: {e}\n")
96
- finally:
97
- await io.progress.update(total=progress_total, completed=progress_total)
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 on, work
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 Button, Checkbox, Footer, Header, Input, Label, TabbedContent, TabPane
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
- @on(Button.Pressed, "#settings_button")
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: