ynab-cli 0.5.4__tar.gz → 0.6.0__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 (275) hide show
  1. ynab_cli-0.6.0/.github/.copilot-instructions.md +191 -0
  2. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/CHANGELOG.md +13 -0
  3. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/PKG-INFO +4 -2
  4. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/pyproject.toml +15 -7
  5. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/budgets.py +2 -2
  6. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/categories.py +2 -2
  7. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/payees.py +4 -4
  8. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/transactions.py +1 -1
  9. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/cli.py +1 -1
  10. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/budgets.py +6 -3
  11. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/categories.py +11 -4
  12. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/payees.py +22 -6
  13. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/transactions.py +7 -2
  14. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/app.py +15 -36
  15. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/app.tcss +11 -11
  16. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/cli.py +6 -2
  17. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/list_all.py +4 -9
  18. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/list_unused.py +4 -9
  19. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/tabs.py +12 -6
  20. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/command_widget.py +61 -0
  21. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/screen.py +4 -2
  22. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/list_all.py +4 -7
  23. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +4 -7
  24. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/list_unused.py +7 -20
  25. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +4 -7
  26. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/tabs.py +14 -8
  27. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/tabs.py +37 -0
  28. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +8 -18
  29. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/transactions/tabs.py +11 -5
  30. ynab_cli-0.6.0/tests/conftest.py +13 -0
  31. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/factories/ynab.py +1 -1
  32. ynab_cli-0.6.0/tests/unit/adapters/rich/test_io.py +128 -0
  33. ynab_cli-0.6.0/tests/unit/adapters/textual/__init__.py +0 -0
  34. ynab_cli-0.6.0/tests/unit/adapters/textual/test_io.py +308 -0
  35. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/unit/conftest.py +4 -4
  36. ynab_cli-0.6.0/tests/unit/domain/__init__.py +0 -0
  37. ynab_cli-0.6.0/tests/unit/domain/models/__init__.py +0 -0
  38. ynab_cli-0.6.0/tests/unit/domain/models/test_rules.py +306 -0
  39. ynab_cli-0.6.0/tests/unit/domain/ports/__init__.py +0 -0
  40. ynab_cli-0.6.0/tests/unit/domain/ports/test_io.py +502 -0
  41. ynab_cli-0.6.0/tests/unit/domain/use_cases/__init__.py +0 -0
  42. ynab_cli-0.6.0/tests/unit/domain/use_cases/test_budgets.py +62 -0
  43. ynab_cli-0.6.0/tests/unit/domain/use_cases/test_categories.py +258 -0
  44. ynab_cli-0.6.0/tests/unit/domain/use_cases/test_payees.py +366 -0
  45. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/unit/domain/use_cases/test_transactions.py +78 -4
  46. ynab_cli-0.6.0/tests/unit/host/__init__.py +0 -0
  47. ynab_cli-0.6.0/tests/unit/host/click/__init__.py +0 -0
  48. ynab_cli-0.6.0/tests/unit/host/click/commands/__init__.py +0 -0
  49. ynab_cli-0.6.0/tests/unit/host/click/commands/test_budgets.py +28 -0
  50. ynab_cli-0.6.0/tests/unit/host/click/commands/test_categories.py +66 -0
  51. ynab_cli-0.6.0/tests/unit/host/click/commands/test_payees.py +108 -0
  52. ynab_cli-0.6.0/tests/unit/host/click/commands/test_transactions.py +67 -0
  53. ynab_cli-0.6.0/tests/unit/host/test_cli.py +35 -0
  54. ynab_cli-0.6.0/tests/unit/host/textual/__init__.py +0 -0
  55. ynab_cli-0.6.0/tests/unit/host/textual/test_app.py +44 -0
  56. ynab_cli-0.6.0/tests/unit/host/textual/test_cli.py +21 -0
  57. ynab_cli-0.6.0/uv.lock +1766 -0
  58. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -47
  59. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -11
  60. ynab_cli-0.5.4/tests/unit/domain/use_cases/test_categories.py +0 -68
  61. ynab_cli-0.5.4/tests/unit/domain/use_cases/test_payees.py +0 -72
  62. ynab_cli-0.5.4/uv.lock +0 -1659
  63. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.env-sample +0 -0
  64. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/FUNDING.yml +0 -0
  65. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/dependabot.yml +0 -0
  66. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/workflows/pr.yml +0 -0
  67. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.github/workflows/publish-to-pypi.yml +0 -0
  68. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.gitignore +0 -0
  69. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/.python-version +0 -0
  70. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/LICENSE.md +0 -0
  71. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/Makefile +0 -0
  72. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/README.md +0 -0
  73. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/docs/index.md +0 -0
  74. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/docs/transaction_rules.md +0 -0
  75. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/mkdocs.yml +0 -0
  76. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/__init__.py +0 -0
  77. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/__init__.py +0 -0
  78. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/__init__.py +0 -0
  79. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/io.py +0 -0
  80. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/__init__.py +0 -0
  81. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/io.py +0 -0
  82. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
  83. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
  84. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
  85. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
  86. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
  87. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
  88. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
  89. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
  90. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
  91. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
  92. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
  93. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
  94. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
  95. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
  96. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
  97. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
  98. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
  99. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
  100. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
  101. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
  102. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +0 -0
  103. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
  104. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +0 -0
  105. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
  106. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
  107. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
  108. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
  109. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
  110. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
  111. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
  112. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
  113. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
  114. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
  115. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
  116. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
  117. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
  118. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
  119. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
  120. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
  121. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
  122. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
  123. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
  124. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
  125. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
  126. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
  127. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
  128. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
  129. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/client.py +0 -0
  130. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/errors.py +0 -0
  131. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
  132. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
  133. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
  134. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
  135. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
  136. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
  137. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
  138. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
  139. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
  140. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
  141. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
  142. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
  143. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
  144. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
  145. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
  146. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
  147. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
  148. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
  149. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
  150. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
  151. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
  152. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
  153. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
  154. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
  155. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +0 -0
  156. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +0 -0
  157. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
  158. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
  159. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
  160. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
  161. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
  162. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
  163. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
  164. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
  165. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
  166. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
  167. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
  168. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
  169. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
  170. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
  171. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
  172. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
  173. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
  174. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
  175. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
  176. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
  177. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
  178. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
  179. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
  180. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
  181. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
  182. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
  183. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
  184. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
  185. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
  186. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
  187. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
  188. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
  189. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
  190. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
  191. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
  192. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
  193. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
  194. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
  195. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
  196. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
  197. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
  198. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
  199. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
  200. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
  201. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
  202. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
  203. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
  204. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
  205. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
  206. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
  207. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
  208. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
  209. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
  210. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
  211. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
  212. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +0 -0
  213. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
  214. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
  215. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
  216. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
  217. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
  218. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
  219. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
  220. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
  221. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
  222. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
  223. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
  224. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
  225. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
  226. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
  227. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
  228. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
  229. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +0 -0
  230. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +0 -0
  231. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
  232. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
  233. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
  234. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +0 -0
  235. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +0 -0
  236. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +0 -0
  237. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
  238. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
  239. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
  240. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
  241. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
  242. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
  243. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
  244. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/types.py +0 -0
  245. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/util.py +0 -0
  246. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/__init__.py +0 -0
  247. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/constants.py +0 -0
  248. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/__init__.py +0 -0
  249. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/rules.py +0 -0
  250. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/__init__.py +0 -0
  251. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/io.py +0 -0
  252. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/settings.py +0 -0
  253. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
  254. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/__init__.py +0 -0
  255. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/__init__.py +0 -0
  256. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/cli.py +0 -0
  257. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/__init__.py +0 -0
  258. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
  259. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
  260. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/constants.py +0 -0
  261. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/__init__.py +0 -0
  262. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/__init__.py +0 -0
  263. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/categories/__init__.py +0 -0
  264. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/__init__.py +0 -0
  265. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -0
  266. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/dialog.py +0 -0
  267. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/common/dialogs/form.py +0 -0
  268. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/payees/__init__.py +0 -0
  269. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/widgets/transactions/__init__.py +0 -0
  270. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/src/ynab_cli/py.typed +0 -0
  271. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/__init__.py +0 -0
  272. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/factories/__init__.py +0 -0
  273. {ynab_cli-0.5.4 → ynab_cli-0.6.0}/tests/unit/__init__.py +0 -0
  274. {ynab_cli-0.5.4/tests/unit/domain → ynab_cli-0.6.0/tests/unit/adapters}/__init__.py +0 -0
  275. {ynab_cli-0.5.4/tests/unit/domain/use_cases → ynab_cli-0.6.0/tests/unit/adapters/rich}/__init__.py +0 -0
@@ -0,0 +1,191 @@
1
+ # YNAB CLI - Copilot Instructions
2
+
3
+ ## Project Overview
4
+
5
+ This is a command-line interface for interacting with YNAB (You Need A Budget) written in Python. The project follows clean architecture principles with ports and adapters pattern.
6
+
7
+ ## Architecture
8
+
9
+ The project uses a layered architecture:
10
+
11
+ - `src/ynab_cli/domain/`: Core business logic, models, and ports (interfaces)
12
+ - `src/ynab_cli/adapters/`: External adapters (YNAB API, Rich UI, Textual TUI)
13
+ - `src/ynab_cli/host/`: Application entry points and framework integration (Click CLI, Textual app)
14
+
15
+ ### Key Patterns
16
+
17
+ 1. **Ports and Adapters**: Domain defines protocols/interfaces, adapters implement them
18
+ 2. **Dependency Injection**: Dependencies are injected through constructors
19
+ 3. **Async/Await**: Most operations are async for better I/O handling
20
+ 4. **Type Safety**: Heavy use of type hints and protocols
21
+
22
+ ## Coding Standards
23
+
24
+ ### Python Version & Dependencies
25
+ - Target Python 3.11+
26
+ - Use `uv` for dependency management
27
+ - All dependencies defined in `pyproject.toml`
28
+
29
+ ### Code Style
30
+ - Follow PEP 8
31
+ - Use type hints for all function parameters and return values
32
+ - Prefer `typing_extensions` imports for newer typing features
33
+ - Use `attrs` for data classes with `@define` decorator
34
+ - Use `@override` decorator from `typing_extensions` when overriding methods in protocols or base classes
35
+
36
+ ### Async Patterns
37
+ - Most I/O operations should be async
38
+ - Use `anyio` for async utilities when needed
39
+ - Progress tracking should be async-compatible
40
+
41
+ ### Error Handling
42
+ - Use proper exception types
43
+ - Handle API errors gracefully
44
+ - Provide meaningful error messages to users
45
+
46
+ ## Testing Guidelines
47
+
48
+ ### Test Structure
49
+ - Unit tests in `tests/unit/` mirroring `src/` structure
50
+ - Use pytest as the testing framework
51
+ - All test functions should have proper type annotations
52
+ - Test classes should be named `Test<ClassName>`
53
+ - Test methods should be descriptive: `test_<method_name>_<scenario>`
54
+
55
+ ### Mocking
56
+ - Use `unittest.mock` for mocking external dependencies
57
+ - Mock at the adapter level, not domain level
58
+ - Use `MagicMock` for complex objects, `Mock` for simple ones
59
+
60
+ ### Test Patterns
61
+ ```python
62
+ from typing import Any
63
+ from unittest.mock import MagicMock, Mock
64
+ import pytest
65
+
66
+ class TestMyClass:
67
+ def test_method_name_success_scenario(self) -> None:
68
+ """Test description of what is being tested."""
69
+ # Arrange
70
+ mock_dependency = Mock()
71
+ instance = MyClass(mock_dependency)
72
+
73
+ # Act
74
+ result = instance.method_name()
75
+
76
+ # Assert
77
+ assert result == expected_value
78
+ mock_dependency.some_method.assert_called_once()
79
+ ```
80
+
81
+ ## Domain Models
82
+
83
+ ### Data Classes
84
+ - Use `@attrs.define` for data classes
85
+ - Implement `to_dict()` and `from_dict()` for serialization when needed
86
+ - Use proper type hints including `Self` for class methods
87
+
88
+ ```python
89
+ from typing import Any, Self
90
+ from attrs import define
91
+
92
+ @define
93
+ class MyModel:
94
+ field: str
95
+ optional_field: int | None = None
96
+
97
+ def to_dict(self) -> dict[str, Any]:
98
+ return {"field": self.field, "optional_field": self.optional_field}
99
+
100
+ @classmethod
101
+ def from_dict(cls, data: dict[str, Any]) -> Self:
102
+ return cls(field=data["field"], optional_field=data.get("optional_field"))
103
+ ```
104
+
105
+ ### Protocols
106
+ - Define interfaces as protocols in `domain/ports/`
107
+ - Use `typing.Protocol` for structural typing
108
+ - Mark implementation methods with `@override`
109
+
110
+ ## UI Frameworks
111
+
112
+ ### Rich (Progress & Prompts)
113
+ - Used for CLI progress bars and user prompts
114
+ - Implement `Progress` and `IO` protocols
115
+ - Handle console stop/start for prompts
116
+
117
+ ### Textual (TUI)
118
+ - Used for interactive terminal UI
119
+ - Follow Textual app patterns
120
+ - Use proper CSS for styling
121
+ - Handle async operations properly
122
+
123
+ ### Click (CLI Framework)
124
+ - Use Click for command-line interface
125
+ - Use `click-default-group` for default commands
126
+ - Implement proper help text and options
127
+ - Use context objects for passing settings
128
+
129
+ ## API Integration
130
+
131
+ ### YNAB API
132
+ - All API models should be in `adapters/ynab/models/`
133
+ - Use proper error handling for HTTP requests
134
+ - Implement rate limiting if needed
135
+ - Use `httpx` for HTTP client
136
+
137
+ ## File Organization
138
+
139
+ ### Import Order
140
+ 1. Standard library imports
141
+ 2. Third-party imports
142
+ 3. Local imports (domain, then adapters, then host)
143
+
144
+ ### Module Structure
145
+ - Keep modules focused and cohesive
146
+ - Use `__init__.py` for public APIs
147
+ - Private modules should have underscore prefix
148
+
149
+ ## Common Patterns
150
+
151
+ ### Progress Tracking
152
+ ```python
153
+ async def long_running_operation(io: IO) -> None:
154
+ await io.progress.update(total=100)
155
+ for i in range(100):
156
+ # Do work
157
+ await io.progress.update(advance=1)
158
+ ```
159
+
160
+ ### Settings & Configuration
161
+ - Use environment variables with `python-dotenv`
162
+ - Settings objects should be immutable
163
+ - Pass settings through dependency injection
164
+
165
+ ### Error Messages
166
+ - Provide actionable error messages
167
+ - Include context about what was being attempted
168
+ - Use appropriate log levels
169
+
170
+ ## Development Workflow
171
+
172
+ 1. Create feature branches from main
173
+ 2. Write tests first (TDD when possible)
174
+ 3. Implement functionality
175
+ 4. Ensure all tests pass
176
+ 5. Run linting and formatting (`make lint`)
177
+ 6. Create pull request with descriptive title and description
178
+
179
+ ## Performance Considerations
180
+
181
+ - Use async I/O for network operations
182
+ - Implement progress tracking for long operations
183
+ - Consider memory usage for large datasets
184
+ - Use appropriate data structures for lookups
185
+
186
+ ## Security
187
+
188
+ - Never log sensitive data (API keys, personal info)
189
+ - Use environment variables for secrets
190
+ - Validate user inputs
191
+ - Handle API rate limits gracefully
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v0.6.0 (2025-06-28)
9
+
10
+ ### Feat
11
+
12
+ - use anyio and uvloop
13
+
14
+ ### Fix
15
+
16
+ - **tests**: adding moar tests
17
+ - **tests**: added tests for a bunch of things
18
+ - **deps**: bump textual from 3.3.0 to 3.4.0
19
+ - **tui**: fix some ui issues and clean up some code
20
+
8
21
  ## v0.5.4 (2025-06-10)
9
22
 
10
23
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ynab-cli
3
- Version: 0.5.4
3
+ Version: 0.6.0
4
4
  Summary: YNAB Command Line Interface
5
5
  Project-URL: Changelog, https://github.com/jeffsawatzky/ynab-cli/blob/main/CHANGELOG.md
6
6
  Project-URL: Documentation, https://jeffsawatzky.github.io/ynab-cli/
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.13
19
19
  Classifier: Topic :: Office/Business :: Financial
20
20
  Classifier: Topic :: Utilities
21
21
  Requires-Python: >=3.11
22
+ Requires-Dist: anyio~=4.9
22
23
  Requires-Dist: attrs~=25.3
23
24
  Requires-Dist: click-default-group~=1.2
24
25
  Requires-Dist: click~=8.2
@@ -28,8 +29,9 @@ Requires-Dist: rapidfuzz~=3.13
28
29
  Requires-Dist: rich~=14.0
29
30
  Requires-Dist: rule-engine~=4.5
30
31
  Requires-Dist: textual-fspicker~=0.4
31
- Requires-Dist: textual~=3.3
32
+ Requires-Dist: textual~=3.5
32
33
  Requires-Dist: typing-extensions~=4.14
34
+ Requires-Dist: uvloop~=0.21
33
35
  Description-Content-Type: text/markdown
34
36
 
35
37
  # YNAB CLI
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ynab-cli"
3
- version = "0.5.4"
3
+ version = "0.6.0"
4
4
  description = "YNAB Command Line Interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -33,6 +33,7 @@ classifiers = [
33
33
  ]
34
34
 
35
35
  dependencies = [
36
+ "anyio~=4.9",
36
37
  "attrs~=25.3",
37
38
  "click-default-group~=1.2",
38
39
  "click~=8.2",
@@ -42,8 +43,9 @@ dependencies = [
42
43
  "rich~=14.0",
43
44
  "rule-engine~=4.5",
44
45
  "textual-fspicker~=0.4",
45
- "textual~=3.3",
46
+ "textual~=3.5",
46
47
  "typing-extensions~=4.14",
48
+ "uvloop~=0.21",
47
49
  ]
48
50
 
49
51
  [project.urls]
@@ -59,18 +61,18 @@ ynab-cli = "ynab_cli.host.cli:main"
59
61
 
60
62
  [dependency-groups]
61
63
  dev = [
62
- "commitizen>=4.8.2",
64
+ "commitizen>=4.8.3",
63
65
  "factory-boy>=3.3.3",
64
66
  "mkdocs-click>=0.9.0",
65
67
  "mkdocs-material>=9.6.14",
66
68
  "mkdocs>=1.6.1",
67
69
  "mypy>=1.16.0",
68
- "pytest-asyncio>=1.0.0",
69
- "pytest-cov>=6.1.1",
70
+ "pytest-cov>=6.2.1",
70
71
  "pytest-mock>=3.14.1",
71
- "pytest>=8.4.0",
72
- "ruff>=0.11.12",
72
+ "pytest>=8.4.1",
73
+ "ruff>=0.12.1",
73
74
  "textual-dev>=1.7.0",
75
+ "trio>=0.30.0",
74
76
  "types-python-dateutil>=2.9.0.20250516",
75
77
  ]
76
78
 
@@ -136,6 +138,12 @@ max-complexity = 70
136
138
  builtins-ignorelist = ["type", "id"]
137
139
 
138
140
 
141
+ [tool.coverage.run]
142
+ omit = [
143
+ "src/ynab_cli/adapters/ynab/**/*",
144
+ ]
145
+
146
+
139
147
  [tool.commitizen]
140
148
  name = "cz_conventional_commits"
141
149
  annotated_tag = true
@@ -24,7 +24,7 @@ async def list_all(
24
24
 
25
25
  async with client:
26
26
  budgets = (
27
- await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=True, client=client)
27
+ await util.get_asyncio_detailed(io, get_budgets.asyncio_detailed, include_accounts=False, client=client)
28
28
  ).data.budgets
29
29
  budgets.sort(key=lambda b: b.name)
30
30
 
@@ -40,6 +40,6 @@ async def list_all(
40
40
  elif isinstance(e, util.ApiError) and e.status_code == 429:
41
41
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
42
42
  else:
43
- await io.print(f"Exception when calling YNAB: {e}\n")
43
+ await io.print(f"Exception when calling YNAB: {e}")
44
44
  finally:
45
45
  await io.progress.update(total=progress_total, completed=progress_total)
@@ -75,7 +75,7 @@ async def list_unused(
75
75
  elif isinstance(e, util.ApiError) and e.status_code == 429:
76
76
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
77
77
  else:
78
- await io.print(f"Exception when calling YNAB: {e}\n")
78
+ await io.print(f"Exception when calling YNAB: {e}")
79
79
  finally:
80
80
  await io.progress.update(total=progress_total, completed=progress_total)
81
81
 
@@ -126,6 +126,6 @@ async def list_all(
126
126
  elif isinstance(e, util.ApiError) and e.status_code == 429:
127
127
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
128
128
  else:
129
- await io.print(f"Exception when calling YNAB: {e}\n")
129
+ await io.print(f"Exception when calling YNAB: {e}")
130
130
  finally:
131
131
  await io.progress.update(total=progress_total, completed=progress_total)
@@ -87,7 +87,7 @@ async def normalize_names(
87
87
  elif isinstance(e, util.ApiError) and e.status_code == 429:
88
88
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
89
89
  else:
90
- await io.print(f"Exception when calling YNAB: {e}\n")
90
+ await io.print(f"Exception when calling YNAB: {e}")
91
91
  finally:
92
92
  await io.progress.update(total=progress_total, completed=progress_total)
93
93
 
@@ -150,7 +150,7 @@ async def list_duplicates(
150
150
  elif isinstance(e, util.ApiError) and e.status_code == 429:
151
151
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
152
152
  else:
153
- await io.print(f"Exception when calling YNAB: {e}\n")
153
+ await io.print(f"Exception when calling YNAB: {e}")
154
154
  finally:
155
155
  await io.progress.update(total=progress_total, completed=progress_total)
156
156
 
@@ -215,7 +215,7 @@ async def list_unused(
215
215
  elif isinstance(e, util.ApiError) and e.status_code == 429:
216
216
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
217
217
  else:
218
- await io.print(f"Exception when calling YNAB: {e}\n")
218
+ await io.print(f"Exception when calling YNAB: {e}")
219
219
  finally:
220
220
  await io.progress.update(total=progress_total, completed=progress_total)
221
221
 
@@ -255,6 +255,6 @@ async def list_all(
255
255
  elif isinstance(e, util.ApiError) and e.status_code == 429:
256
256
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
257
257
  else:
258
- await io.print(f"Exception when calling YNAB: {e}\n")
258
+ await io.print(f"Exception when calling YNAB: {e}")
259
259
  finally:
260
260
  await io.progress.update(total=progress_total, completed=progress_total)
@@ -98,6 +98,6 @@ async def apply_rules(
98
98
  elif isinstance(e, util.ApiError) and e.status_code == 429:
99
99
  await io.print("API rate limit exceeded. Try again later, or get a new access token.")
100
100
  else:
101
- await io.print(f"Exception when calling YNAB: {e}\n")
101
+ await io.print(f"Exception when calling YNAB: {e}")
102
102
  finally:
103
103
  await io.progress.update(total=progress_total, completed=progress_total)
@@ -35,4 +35,4 @@ def main() -> None:
35
35
 
36
36
 
37
37
  if __name__ == "__main__":
38
- main()
38
+ main() # pragma: no cover
@@ -1,5 +1,4 @@
1
- import asyncio
2
-
1
+ import anyio
3
2
  import click
4
3
  from rich.table import Table
5
4
 
@@ -38,7 +37,11 @@ def list_all(ctx: click.Context) -> None:
38
37
  """List all budgets in YNAB."""
39
38
 
40
39
  ctx.ensure_object(dict)
41
- asyncio.run(_list_all(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings())))
40
+ anyio.run(
41
+ _list_all,
42
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
43
+ backend_options={"use_uvloop": True},
44
+ )
42
45
 
43
46
 
44
47
  @click.group()
@@ -1,5 +1,4 @@
1
- import asyncio
2
-
1
+ import anyio
3
2
  import click
4
3
  from rich.table import Table
5
4
 
@@ -68,7 +67,11 @@ def list_unused(ctx: click.Context) -> None:
68
67
  """List unused categories in the YNAB budget."""
69
68
 
70
69
  ctx.ensure_object(dict)
71
- asyncio.run(_list_unused(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings())))
70
+ anyio.run(
71
+ _list_unused,
72
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
73
+ backend_options={"use_uvloop": True},
74
+ )
72
75
 
73
76
 
74
77
  @click.command()
@@ -77,7 +80,11 @@ def list_all(ctx: click.Context) -> None:
77
80
  """List all categories in the YNAB budget."""
78
81
 
79
82
  ctx.ensure_object(dict)
80
- asyncio.run(_list_all(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings())))
83
+ anyio.run(
84
+ _list_all,
85
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
86
+ backend_options={"use_uvloop": True},
87
+ )
81
88
 
82
89
 
83
90
  @click.group()
@@ -1,5 +1,4 @@
1
- import asyncio
2
-
1
+ import anyio
3
2
  import click
4
3
  from rich.table import Table
5
4
 
@@ -112,7 +111,11 @@ def normalize_names(ctx: click.Context) -> None:
112
111
  """Normalize payee names in the YNAB budget."""
113
112
 
114
113
  ctx.ensure_object(dict)
115
- asyncio.run(_normalize_names(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings())))
114
+ anyio.run(
115
+ _normalize_names,
116
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
117
+ backend_options={"use_uvloop": True},
118
+ )
116
119
 
117
120
 
118
121
  @click.command()
@@ -121,7 +124,11 @@ def list_duplicates(ctx: click.Context) -> None:
121
124
  """List duplicate payees in the YNAB budget."""
122
125
 
123
126
  ctx.ensure_object(dict)
124
- asyncio.run(_list_duplicates(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings())))
127
+ anyio.run(
128
+ _list_duplicates,
129
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
130
+ backend_options={"use_uvloop": True},
131
+ )
125
132
 
126
133
 
127
134
  @click.command()
@@ -131,7 +138,12 @@ def list_unused(ctx: click.Context, prefix_unused: bool) -> None:
131
138
  """List unused payees in the YNAB budget."""
132
139
 
133
140
  ctx.ensure_object(dict)
134
- asyncio.run(_list_unused(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()), prefix_unused))
141
+ anyio.run(
142
+ _list_unused,
143
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
144
+ prefix_unused,
145
+ backend_options={"use_uvloop": True},
146
+ )
135
147
 
136
148
 
137
149
  @click.command()
@@ -140,7 +152,11 @@ def list_all(ctx: click.Context) -> None:
140
152
  """List all payees in the YNAB budget."""
141
153
 
142
154
  ctx.ensure_object(dict)
143
- asyncio.run(_list_all(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings())))
155
+ anyio.run(
156
+ _list_all,
157
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
158
+ backend_options={"use_uvloop": True},
159
+ )
144
160
 
145
161
 
146
162
  @click.group()
@@ -1,7 +1,7 @@
1
- import asyncio
2
1
  import json
3
2
  from typing import IO, Any
4
3
 
4
+ import anyio
5
5
  import click
6
6
  from rich.table import Table
7
7
 
@@ -62,7 +62,12 @@ def apply_rules(ctx: click.Context, rules_file: IO[Any]) -> None:
62
62
 
63
63
  transaction_rules = rules.TransactionRules.from_dict(json.load(rules_file))
64
64
 
65
- asyncio.run(_apply_rules(ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()), transaction_rules))
65
+ anyio.run(
66
+ _apply_rules,
67
+ ctx.obj.get(CONTEXT_KEY_SETTINGS, Settings()),
68
+ transaction_rules,
69
+ backend_options={"use_uvloop": True},
70
+ )
66
71
 
67
72
 
68
73
  @click.group()
@@ -5,38 +5,12 @@ 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 Checkbox, Footer, Header, Input, Label, TabbedContent, TabPane
8
+ from textual.widgets import Checkbox, Footer, Header, Input, Label, TabbedContent
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.tabs import CategoriesTabs
13
12
  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.tabs import PayeesTabs
16
- from ynab_cli.host.textual.widgets.transactions.tabs 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()
13
+ from ynab_cli.host.textual.widgets.tabs import CommandTabs
40
14
 
41
15
 
42
16
  class SettingsDialogForm(DialogForm[Settings]):
@@ -81,8 +55,9 @@ class YnabCliApp(App[None]):
81
55
 
82
56
  BINDINGS: ClassVar[list[BindingType]] = [
83
57
  Binding("ctrl+c", "quit", "Quit", show=True, priority=True),
84
- Binding("s", "settings", "Settings", show=True, priority=False),
85
- Binding("r", "run", "Run", show=True, priority=False),
58
+ Binding("s", "settings", "Application Settings", show=True, priority=False),
59
+ Binding("p", "parameters", "Command Parameters", show=True, priority=False),
60
+ Binding("r", "run", "Run Command", show=True, priority=False),
86
61
  ]
87
62
 
88
63
  settings: reactive[Settings] = reactive(Settings())
@@ -104,17 +79,21 @@ class YnabCliApp(App[None]):
104
79
  def on_mount(self) -> None:
105
80
  self.query_one(CommandTabs).query_one(TabbedContent).focus()
106
81
 
107
- async def action_run(self) -> None:
108
- command_tabs = self.query_one(CommandTabs)
109
- await command_tabs.run_command()
110
-
111
82
  async def action_settings(self) -> None:
112
- self._get_settings()
83
+ self._get_settings_worker()
113
84
 
114
85
  @work(exclusive=True)
115
- async def _get_settings(self) -> None:
86
+ async def _get_settings_worker(self) -> None:
116
87
  result = await self.push_screen_wait(
117
88
  SaveCancelDialogScreen(SettingsDialogForm(self.settings), title="Settings")
118
89
  )
119
90
  if result is not CANCELLED:
120
91
  self.settings = result
92
+
93
+ async def action_parameters(self) -> None:
94
+ command_tabs = self.query_one(CommandTabs)
95
+ await command_tabs.active_command().get_command_params()
96
+
97
+ async def action_run(self) -> None:
98
+ command_tabs = self.query_one(CommandTabs)
99
+ await command_tabs.active_command().run_command()
@@ -9,22 +9,22 @@ CommandTabs {
9
9
  height: auto;
10
10
  padding-top: 1;
11
11
 
12
- & DataTable {
13
- border: solid $secondary;
14
- height: 1fr;
15
- }
12
+ & CommandWidget {
13
+ height: auto;
16
14
 
17
- & Log {
18
- border: solid $secondary;
19
- height: 8;
15
+ & DataTable {
16
+ border: solid $secondary;
17
+ height: 1fr;
18
+ }
19
+
20
+ & Log {
21
+ border: solid $secondary;
22
+ height: 8;
23
+ }
20
24
  }
21
25
  }
22
26
  }
23
27
 
24
- RunnableWidget {
25
- height: auto;
26
- }
27
-
28
28
  DialogScreen {
29
29
  align: center middle;
30
30
 
@@ -1,6 +1,6 @@
1
- import asyncio
2
1
  import logging
3
2
 
3
+ import anyio
4
4
  import click
5
5
  from textual.logging import TextualHandler
6
6
 
@@ -39,4 +39,8 @@ def tui(
39
39
  handlers=[TextualHandler()],
40
40
  )
41
41
 
42
- asyncio.run(_tui(ctx.obj[CONTEXT_KEY_SETTINGS]))
42
+ anyio.run(
43
+ _tui,
44
+ ctx.obj[CONTEXT_KEY_SETTINGS],
45
+ backend_options={"use_uvloop": True},
46
+ )