ynab-cli 0.5.3__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 (281) hide show
  1. ynab_cli-0.6.0/.github/.copilot-instructions.md +191 -0
  2. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/CHANGELOG.md +19 -0
  3. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/PKG-INFO +4 -2
  4. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/pyproject.toml +15 -7
  5. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/rules.py +21 -8
  6. ynab_cli-0.6.0/src/ynab_cli/domain/use_cases/budgets.py +45 -0
  7. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/categories.py +36 -20
  8. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/payees.py +57 -45
  9. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/transactions.py +16 -10
  10. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/cli.py +1 -1
  11. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/budgets.py +6 -3
  12. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/categories.py +11 -4
  13. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/payees.py +22 -6
  14. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/transactions.py +7 -2
  15. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/app.py +18 -45
  16. ynab_cli-0.6.0/src/ynab_cli/host/textual/app.tcss +67 -0
  17. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/cli.py +6 -2
  18. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories/list_all.py +33 -0
  19. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories/list_unused.py +33 -0
  20. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories/tabs.py +33 -0
  21. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/command_widget.py +61 -0
  22. {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/dialog.py +2 -2
  23. {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/screen.py +19 -4
  24. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/list_all.py +29 -0
  25. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +35 -0
  26. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/list_unused.py +61 -0
  27. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +33 -0
  28. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees/tabs.py +43 -0
  29. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/tabs.py +37 -0
  30. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +108 -0
  31. ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/transactions/tabs.py +28 -0
  32. ynab_cli-0.6.0/tests/conftest.py +13 -0
  33. ynab_cli-0.6.0/tests/factories/__init__.py +0 -0
  34. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/tests/factories/ynab.py +1 -1
  35. ynab_cli-0.6.0/tests/unit/__init__.py +0 -0
  36. ynab_cli-0.6.0/tests/unit/adapters/__init__.py +0 -0
  37. ynab_cli-0.6.0/tests/unit/adapters/rich/__init__.py +0 -0
  38. ynab_cli-0.6.0/tests/unit/adapters/rich/test_io.py +128 -0
  39. ynab_cli-0.6.0/tests/unit/adapters/textual/__init__.py +0 -0
  40. ynab_cli-0.6.0/tests/unit/adapters/textual/test_io.py +308 -0
  41. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/tests/unit/conftest.py +4 -4
  42. ynab_cli-0.6.0/tests/unit/domain/__init__.py +0 -0
  43. ynab_cli-0.6.0/tests/unit/domain/models/__init__.py +0 -0
  44. ynab_cli-0.6.0/tests/unit/domain/models/test_rules.py +306 -0
  45. ynab_cli-0.6.0/tests/unit/domain/ports/__init__.py +0 -0
  46. ynab_cli-0.6.0/tests/unit/domain/ports/test_io.py +502 -0
  47. ynab_cli-0.6.0/tests/unit/domain/use_cases/__init__.py +0 -0
  48. ynab_cli-0.6.0/tests/unit/domain/use_cases/test_budgets.py +62 -0
  49. ynab_cli-0.6.0/tests/unit/domain/use_cases/test_categories.py +258 -0
  50. ynab_cli-0.6.0/tests/unit/domain/use_cases/test_payees.py +366 -0
  51. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/tests/unit/domain/use_cases/test_transactions.py +78 -4
  52. ynab_cli-0.6.0/tests/unit/host/__init__.py +0 -0
  53. ynab_cli-0.6.0/tests/unit/host/click/__init__.py +0 -0
  54. ynab_cli-0.6.0/tests/unit/host/click/commands/__init__.py +0 -0
  55. ynab_cli-0.6.0/tests/unit/host/click/commands/test_budgets.py +28 -0
  56. ynab_cli-0.6.0/tests/unit/host/click/commands/test_categories.py +66 -0
  57. ynab_cli-0.6.0/tests/unit/host/click/commands/test_payees.py +108 -0
  58. ynab_cli-0.6.0/tests/unit/host/click/commands/test_transactions.py +67 -0
  59. ynab_cli-0.6.0/tests/unit/host/test_cli.py +35 -0
  60. ynab_cli-0.6.0/tests/unit/host/textual/__init__.py +0 -0
  61. ynab_cli-0.6.0/tests/unit/host/textual/test_app.py +44 -0
  62. ynab_cli-0.6.0/tests/unit/host/textual/test_cli.py +21 -0
  63. ynab_cli-0.6.0/uv.lock +1766 -0
  64. ynab_cli-0.5.3/src/ynab_cli/domain/use_cases/budgets.py +0 -37
  65. ynab_cli-0.5.3/src/ynab_cli/host/textual/app.tcss +0 -96
  66. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/categories.py +0 -91
  67. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -47
  68. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -10
  69. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -11
  70. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/payees.py +0 -153
  71. ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/transactions.py +0 -90
  72. ynab_cli-0.5.3/tests/unit/domain/use_cases/test_categories.py +0 -68
  73. ynab_cli-0.5.3/tests/unit/domain/use_cases/test_payees.py +0 -72
  74. ynab_cli-0.5.3/uv.lock +0 -1659
  75. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.env-sample +0 -0
  76. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/FUNDING.yml +0 -0
  77. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/dependabot.yml +0 -0
  78. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/workflows/pr.yml +0 -0
  79. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.github/workflows/publish-to-pypi.yml +0 -0
  80. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.gitignore +0 -0
  81. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/.python-version +0 -0
  82. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/LICENSE.md +0 -0
  83. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/Makefile +0 -0
  84. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/README.md +0 -0
  85. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/docs/index.md +0 -0
  86. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/docs/transaction_rules.md +0 -0
  87. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/mkdocs.yml +0 -0
  88. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/__init__.py +0 -0
  89. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/__init__.py +0 -0
  90. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/__init__.py +0 -0
  91. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/rich/io.py +0 -0
  92. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/__init__.py +0 -0
  93. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/textual/io.py +0 -0
  94. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
  95. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
  96. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
  97. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
  98. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
  99. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
  100. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
  101. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
  102. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
  103. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
  104. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
  105. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
  106. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
  107. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
  108. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
  109. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
  110. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
  111. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
  112. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
  113. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
  114. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +0 -0
  115. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
  116. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +0 -0
  117. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
  118. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
  119. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
  120. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
  121. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
  122. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
  123. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
  124. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
  125. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
  126. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
  127. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
  128. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
  129. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
  130. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
  131. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
  132. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
  133. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
  134. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
  135. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
  136. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
  137. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
  138. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
  139. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
  140. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
  141. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/client.py +0 -0
  142. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/errors.py +0 -0
  143. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
  144. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
  145. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
  146. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
  147. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
  148. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
  149. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
  150. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
  151. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
  152. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
  153. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
  154. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
  155. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
  156. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
  157. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
  158. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
  159. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
  160. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
  161. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
  162. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
  163. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
  164. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
  165. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
  166. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
  167. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +0 -0
  168. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +0 -0
  169. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
  170. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
  171. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
  172. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
  173. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
  174. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
  175. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
  176. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
  177. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
  178. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
  179. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
  180. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
  181. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
  182. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
  183. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
  184. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
  185. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
  186. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
  187. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
  188. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
  189. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
  190. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
  191. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
  192. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
  193. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
  194. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
  195. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
  196. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
  197. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
  198. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
  199. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
  200. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
  201. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
  202. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
  203. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
  204. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
  205. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
  206. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
  207. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
  208. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
  209. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
  210. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
  211. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
  212. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
  213. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
  214. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
  215. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
  216. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
  217. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
  218. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
  219. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
  220. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
  221. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
  222. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
  223. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
  224. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +0 -0
  225. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
  226. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
  227. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
  228. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
  229. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
  230. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
  231. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
  232. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
  233. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
  234. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
  235. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
  236. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
  237. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
  238. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
  239. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
  240. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
  241. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +0 -0
  242. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +0 -0
  243. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
  244. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
  245. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
  246. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +0 -0
  247. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +0 -0
  248. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +0 -0
  249. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
  250. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
  251. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
  252. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
  253. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
  254. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
  255. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
  256. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/types.py +0 -0
  257. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/adapters/ynab/util.py +0 -0
  258. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/__init__.py +0 -0
  259. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/constants.py +0 -0
  260. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/models/__init__.py +0 -0
  261. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/__init__.py +0 -0
  262. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/ports/io.py +0 -0
  263. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/settings.py +0 -0
  264. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
  265. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/__init__.py +0 -0
  266. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/__init__.py +0 -0
  267. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/cli.py +0 -0
  268. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/__init__.py +0 -0
  269. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
  270. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/click/commands/rich/progress_table.py +0 -0
  271. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/constants.py +0 -0
  272. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/host/textual/__init__.py +0 -0
  273. {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets}/__init__.py +0 -0
  274. {ynab_cli-0.5.3/tests → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/categories}/__init__.py +0 -0
  275. {ynab_cli-0.5.3/tests/factories → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common}/__init__.py +0 -0
  276. {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/__init__.py +0 -0
  277. {ynab_cli-0.5.3/src/ynab_cli/host/textual/widgets/common/dialogs/base → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/common/dialogs}/form.py +0 -0
  278. {ynab_cli-0.5.3/tests/unit → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/payees}/__init__.py +0 -0
  279. {ynab_cli-0.5.3/tests/unit/domain → ynab_cli-0.6.0/src/ynab_cli/host/textual/widgets/transactions}/__init__.py +0 -0
  280. {ynab_cli-0.5.3 → ynab_cli-0.6.0}/src/ynab_cli/py.typed +0 -0
  281. {ynab_cli-0.5.3/tests/unit/domain/use_cases → ynab_cli-0.6.0/tests}/__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,25 @@ 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
+
21
+ ## v0.5.4 (2025-06-10)
22
+
23
+ ### Fix
24
+
25
+ - **tui**: added some command parameter forms
26
+
8
27
  ## v0.5.3 (2025-06-09)
9
28
 
10
29
  ### Fix
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ynab-cli
3
- Version: 0.5.3
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.3"
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
@@ -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=False, 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}")
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}")
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}")
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}")
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}")
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}")
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}")
259
+ finally:
260
+ await io.progress.update(total=progress_total, completed=progress_total)