ynab-cli 0.5.4__tar.gz → 0.6.1__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 (287) hide show
  1. ynab_cli-0.6.1/.github/.copilot-instructions.md +191 -0
  2. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/CHANGELOG.md +23 -0
  3. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/Makefile +1 -1
  4. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/PKG-INFO +5 -2
  5. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/pyproject.toml +16 -7
  6. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/rich/io.py +5 -5
  7. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/util.py +12 -0
  8. ynab_cli-0.6.1/src/ynab_cli/domain/use_cases/budgets.py +47 -0
  9. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/categories.py +48 -47
  10. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/payees.py +103 -87
  11. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/transactions.py +26 -24
  12. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/cli.py +1 -1
  13. ynab_cli-0.6.1/src/ynab_cli/host/click/commands/budgets.py +69 -0
  14. ynab_cli-0.6.1/src/ynab_cli/host/click/commands/categories.py +129 -0
  15. ynab_cli-0.6.1/src/ynab_cli/host/click/commands/payees.py +225 -0
  16. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/commands/rich/progress_table.py +2 -2
  17. ynab_cli-0.6.1/src/ynab_cli/host/click/commands/transactions.py +94 -0
  18. ynab_cli-0.6.1/src/ynab_cli/host/click/container.py +88 -0
  19. ynab_cli-0.6.1/src/ynab_cli/host/container.py +68 -0
  20. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/app.py +28 -37
  21. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/app.tcss +11 -11
  22. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/cli.py +13 -6
  23. ynab_cli-0.6.1/src/ynab_cli/host/textual/container.py +40 -0
  24. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/list_all.py +11 -9
  25. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/list_unused.py +11 -9
  26. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/tabs.py +12 -6
  27. ynab_cli-0.6.1/src/ynab_cli/host/textual/widgets/common/command_widget.py +61 -0
  28. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/screen.py +4 -2
  29. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/list_all.py +11 -7
  30. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/list_duplicates.py +11 -9
  31. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/list_unused.py +12 -20
  32. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/normalize_names.py +11 -9
  33. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/tabs.py +14 -8
  34. ynab_cli-0.6.1/src/ynab_cli/host/textual/widgets/tabs.py +37 -0
  35. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/transactions/apply_rules.py +13 -20
  36. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/transactions/tabs.py +11 -5
  37. ynab_cli-0.6.1/tests/conftest.py +13 -0
  38. ynab_cli-0.6.1/tests/factories/ynab.py +18 -0
  39. ynab_cli-0.6.1/tests/unit/adapters/rich/test_io.py +177 -0
  40. ynab_cli-0.6.1/tests/unit/adapters/textual/__init__.py +0 -0
  41. ynab_cli-0.6.1/tests/unit/adapters/textual/test_io.py +308 -0
  42. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/unit/conftest.py +4 -4
  43. ynab_cli-0.6.1/tests/unit/domain/__init__.py +0 -0
  44. ynab_cli-0.6.1/tests/unit/domain/models/__init__.py +0 -0
  45. ynab_cli-0.6.1/tests/unit/domain/models/test_rules.py +306 -0
  46. ynab_cli-0.6.1/tests/unit/domain/ports/__init__.py +0 -0
  47. ynab_cli-0.6.1/tests/unit/domain/ports/test_io.py +502 -0
  48. ynab_cli-0.6.1/tests/unit/domain/use_cases/__init__.py +0 -0
  49. ynab_cli-0.6.1/tests/unit/domain/use_cases/test_budgets.py +64 -0
  50. ynab_cli-0.6.1/tests/unit/domain/use_cases/test_categories.py +258 -0
  51. ynab_cli-0.6.1/tests/unit/domain/use_cases/test_payees.py +366 -0
  52. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/unit/domain/use_cases/test_transactions.py +78 -4
  53. ynab_cli-0.6.1/tests/unit/host/__init__.py +0 -0
  54. ynab_cli-0.6.1/tests/unit/host/click/__init__.py +0 -0
  55. ynab_cli-0.6.1/tests/unit/host/click/commands/__init__.py +0 -0
  56. ynab_cli-0.6.1/tests/unit/host/click/commands/test_budgets.py +25 -0
  57. ynab_cli-0.6.1/tests/unit/host/click/commands/test_categories.py +64 -0
  58. ynab_cli-0.6.1/tests/unit/host/click/commands/test_payees.py +107 -0
  59. ynab_cli-0.6.1/tests/unit/host/click/commands/test_transactions.py +65 -0
  60. ynab_cli-0.6.1/tests/unit/host/click/conftest.py +21 -0
  61. ynab_cli-0.6.1/tests/unit/host/conftest.py +13 -0
  62. ynab_cli-0.6.1/tests/unit/host/test_cli.py +35 -0
  63. ynab_cli-0.6.1/tests/unit/host/textual/__init__.py +0 -0
  64. ynab_cli-0.6.1/tests/unit/host/textual/conftest.py +21 -0
  65. ynab_cli-0.6.1/tests/unit/host/textual/test_app.py +49 -0
  66. ynab_cli-0.6.1/tests/unit/host/textual/test_cli.py +19 -0
  67. ynab_cli-0.6.1/uv.lock +1792 -0
  68. ynab_cli-0.5.4/src/ynab_cli/domain/use_cases/budgets.py +0 -45
  69. ynab_cli-0.5.4/src/ynab_cli/host/click/commands/budgets.py +0 -54
  70. ynab_cli-0.5.4/src/ynab_cli/host/click/commands/categories.py +0 -96
  71. ynab_cli-0.5.4/src/ynab_cli/host/click/commands/payees.py +0 -161
  72. ynab_cli-0.5.4/src/ynab_cli/host/click/commands/transactions.py +0 -80
  73. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/base_command.py +0 -47
  74. ynab_cli-0.5.4/src/ynab_cli/host/textual/widgets/common/runnable_widget.py +0 -11
  75. ynab_cli-0.5.4/tests/factories/ynab.py +0 -18
  76. ynab_cli-0.5.4/tests/unit/domain/use_cases/test_categories.py +0 -68
  77. ynab_cli-0.5.4/tests/unit/domain/use_cases/test_payees.py +0 -72
  78. ynab_cli-0.5.4/uv.lock +0 -1659
  79. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.env-sample +0 -0
  80. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/FUNDING.yml +0 -0
  81. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/dependabot.yml +0 -0
  82. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/workflows/pr.yml +0 -0
  83. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.github/workflows/publish-to-pypi.yml +0 -0
  84. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.gitignore +0 -0
  85. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/.python-version +0 -0
  86. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/LICENSE.md +0 -0
  87. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/README.md +0 -0
  88. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/docs/index.md +0 -0
  89. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/docs/transaction_rules.md +0 -0
  90. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/mkdocs.yml +0 -0
  91. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/__init__.py +0 -0
  92. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/__init__.py +0 -0
  93. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/rich/__init__.py +0 -0
  94. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/textual/__init__.py +0 -0
  95. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/textual/io.py +0 -0
  96. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/__init__.py +0 -0
  97. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/__init__.py +0 -0
  98. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/__init__.py +0 -0
  99. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/create_account.py +0 -0
  100. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/get_account_by_id.py +0 -0
  101. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/accounts/get_accounts.py +0 -0
  102. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/__init__.py +0 -0
  103. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_by_id.py +0 -0
  104. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/get_budget_settings_by_id.py +0 -0
  105. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/budgets/get_budgets.py +0 -0
  106. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/__init__.py +0 -0
  107. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/get_categories.py +0 -0
  108. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/get_category_by_id.py +0 -0
  109. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/get_month_category_by_id.py +0 -0
  110. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/update_category.py +0 -0
  111. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/categories/update_month_category.py +0 -0
  112. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/months/__init__.py +0 -0
  113. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/months/get_budget_month.py +0 -0
  114. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/months/get_budget_months.py +0 -0
  115. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payee_locations/__init__.py +0 -0
  116. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_location_by_id.py +0 -0
  117. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations.py +0 -0
  118. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payee_locations/get_payee_locations_by_payee.py +0 -0
  119. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/__init__.py +0 -0
  120. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/get_payee_by_id.py +0 -0
  121. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/get_payees.py +0 -0
  122. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/payees/update_payee.py +0 -0
  123. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/__init__.py +0 -0
  124. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
  125. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
  126. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
  127. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
  128. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
  129. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/__init__.py +0 -0
  130. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/create_transaction.py +0 -0
  131. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/delete_transaction.py +0 -0
  132. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transaction_by_id.py +0 -0
  133. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions.py +0 -0
  134. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_account.py +0 -0
  135. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_category.py +0 -0
  136. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_month.py +0 -0
  137. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/get_transactions_by_payee.py +0 -0
  138. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/import_transactions.py +0 -0
  139. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/update_transaction.py +0 -0
  140. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/transactions/update_transactions.py +0 -0
  141. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/user/__init__.py +0 -0
  142. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/api/user/get_user.py +0 -0
  143. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/client.py +0 -0
  144. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/errors.py +0 -0
  145. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/__init__.py +0 -0
  146. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account.py +0 -0
  147. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account_response.py +0 -0
  148. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account_response_data.py +0 -0
  149. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/account_type.py +0 -0
  150. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/accounts_response.py +0 -0
  151. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/accounts_response_data.py +0 -0
  152. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_detail.py +0 -0
  153. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_detail_response.py +0 -0
  154. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_detail_response_data.py +0 -0
  155. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_settings.py +0 -0
  156. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_settings_response.py +0 -0
  157. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_settings_response_data.py +0 -0
  158. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_summary.py +0 -0
  159. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_summary_response.py +0 -0
  160. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/budget_summary_response_data.py +0 -0
  161. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_response.py +0 -0
  162. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_response_data.py +0 -0
  163. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_response_data_bulk.py +0 -0
  164. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/bulk_transactions.py +0 -0
  165. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/categories_response.py +0 -0
  166. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/categories_response_data.py +0 -0
  167. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category.py +0 -0
  168. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_1.py +0 -0
  169. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_2_type_1.py +0 -0
  170. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_goal_type_type_3_type_1.py +0 -0
  171. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_group.py +0 -0
  172. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_group_with_categories.py +0 -0
  173. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_response.py +0 -0
  174. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/category_response_data.py +0 -0
  175. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/currency_format_type_0.py +0 -0
  176. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/date_format_type_0.py +0 -0
  177. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/error_detail.py +0 -0
  178. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/error_response.py +0 -0
  179. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/existing_transaction.py +0 -0
  180. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_account_type.py +0 -0
  181. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_category_type.py +0 -0
  182. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_month_type.py +0 -0
  183. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_by_payee_type.py +0 -0
  184. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/get_transactions_type.py +0 -0
  185. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transaction.py +0 -0
  186. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transaction_type.py +0 -0
  187. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response.py +0 -0
  188. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/hybrid_transactions_response_data.py +0 -0
  189. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/loan_account_periodic_value_type_0.py +0 -0
  190. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_detail.py +0 -0
  191. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_detail_response.py +0 -0
  192. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_detail_response_data.py +0 -0
  193. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_summaries_response.py +0 -0
  194. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_summaries_response_data.py +0 -0
  195. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/month_summary.py +0 -0
  196. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/new_transaction.py +0 -0
  197. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_category_wrapper.py +0 -0
  198. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_month_category_wrapper.py +0 -0
  199. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_payee_wrapper.py +0 -0
  200. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/patch_transactions_wrapper.py +0 -0
  201. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee.py +0 -0
  202. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_location.py +0 -0
  203. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_location_response.py +0 -0
  204. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_location_response_data.py +0 -0
  205. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_locations_response.py +0 -0
  206. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_locations_response_data.py +0 -0
  207. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_response.py +0 -0
  208. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payee_response_data.py +0 -0
  209. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payees_response.py +0 -0
  210. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/payees_response_data.py +0 -0
  211. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/post_account_wrapper.py +0 -0
  212. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/post_scheduled_transaction_wrapper.py +0 -0
  213. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/post_transactions_wrapper.py +0 -0
  214. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/put_scheduled_transaction_wrapper.py +0 -0
  215. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/put_transaction_wrapper.py +0 -0
  216. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_account.py +0 -0
  217. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_category.py +0 -0
  218. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_category_response.py +0 -0
  219. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_category_response_data.py +0 -0
  220. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_month_category.py +0 -0
  221. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_payee.py +0 -0
  222. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_payee_response.py +0 -0
  223. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_payee_response_data.py +0 -0
  224. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_scheduled_transaction.py +0 -0
  225. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_sub_transaction.py +0 -0
  226. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_transaction_with_id_or_import_id.py +0 -0
  227. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_transaction_with_optional_fields.py +0 -0
  228. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_transactions_response.py +0 -0
  229. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/save_transactions_response_data.py +0 -0
  230. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_sub_transaction.py +0 -0
  231. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_detail.py +0 -0
  232. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_frequency.py +0 -0
  233. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response.py +0 -0
  234. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_response_data.py +0 -0
  235. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary.py +0 -0
  236. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transaction_summary_frequency.py +0 -0
  237. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response.py +0 -0
  238. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/scheduled_transactions_response_data.py +0 -0
  239. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/sub_transaction.py +0 -0
  240. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_cleared_status.py +0 -0
  241. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_detail.py +0 -0
  242. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_1.py +0 -0
  243. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_2_type_1.py +0 -0
  244. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_flag_color_type_3_type_1.py +0 -0
  245. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_response.py +0 -0
  246. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_response_data.py +0 -0
  247. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_summary.py +0 -0
  248. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_1.py +0 -0
  249. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_2_type_1.py +0 -0
  250. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transaction_summary_debt_transaction_type_type_3_type_1.py +0 -0
  251. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_import_response.py +0 -0
  252. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_import_response_data.py +0 -0
  253. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_response.py +0 -0
  254. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/transactions_response_data.py +0 -0
  255. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/user.py +0 -0
  256. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/user_response.py +0 -0
  257. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/models/user_response_data.py +0 -0
  258. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/adapters/ynab/types.py +0 -0
  259. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/__init__.py +0 -0
  260. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/constants.py +0 -0
  261. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/models/__init__.py +0 -0
  262. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/models/rules.py +0 -0
  263. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/ports/__init__.py +0 -0
  264. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/ports/io.py +0 -0
  265. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/settings.py +0 -0
  266. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/domain/use_cases/__init__.py +0 -0
  267. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/__init__.py +0 -0
  268. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/__init__.py +0 -0
  269. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/cli.py +0 -0
  270. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/commands/__init__.py +0 -0
  271. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/click/commands/rich/__init__.py +0 -0
  272. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/constants.py +0 -0
  273. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/__init__.py +0 -0
  274. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/__init__.py +0 -0
  275. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/categories/__init__.py +0 -0
  276. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/__init__.py +0 -0
  277. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/__init__.py +0 -0
  278. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/dialog.py +0 -0
  279. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/common/dialogs/form.py +0 -0
  280. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/payees/__init__.py +0 -0
  281. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/host/textual/widgets/transactions/__init__.py +0 -0
  282. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/src/ynab_cli/py.typed +0 -0
  283. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/__init__.py +0 -0
  284. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/factories/__init__.py +0 -0
  285. {ynab_cli-0.5.4 → ynab_cli-0.6.1}/tests/unit/__init__.py +0 -0
  286. {ynab_cli-0.5.4/tests/unit/domain → ynab_cli-0.6.1/tests/unit/adapters}/__init__.py +0 -0
  287. {ynab_cli-0.5.4/tests/unit/domain/use_cases → ynab_cli-0.6.1/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,29 @@ 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.1 (2025-07-09)
9
+
10
+ ### Fix
11
+
12
+ - fix issue with providing new token
13
+
14
+ ### Refactor
15
+
16
+ - work towards using dependency injection container
17
+
18
+ ## v0.6.0 (2025-06-28)
19
+
20
+ ### Feat
21
+
22
+ - use anyio and uvloop
23
+
24
+ ### Fix
25
+
26
+ - **tests**: adding moar tests
27
+ - **tests**: added tests for a bunch of things
28
+ - **deps**: bump textual from 3.3.0 to 3.4.0
29
+ - **tui**: fix some ui issues and clean up some code
30
+
8
31
  ## v0.5.4 (2025-06-10)
9
32
 
10
33
  ### Fix
@@ -10,7 +10,7 @@ lint: .venv # run linters and type checkers
10
10
  uv run mypy .
11
11
 
12
12
  test: .venv lint # run tests with coverage
13
- uv run pytest --cov=src tests
13
+ uv run pytest --cov-report term-missing:skip-covered --cov=src tests
14
14
 
15
15
  clean: # clean up build artifacts and caches
16
16
  rm -f .coverage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ynab-cli
3
- Version: 0.5.4
3
+ Version: 0.6.1
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,17 +19,20 @@ 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
25
26
  Requires-Dist: httpx~=0.28
27
+ Requires-Dist: lagom~=2.7
26
28
  Requires-Dist: python-dotenv~=1.1
27
29
  Requires-Dist: rapidfuzz~=3.13
28
30
  Requires-Dist: rich~=14.0
29
31
  Requires-Dist: rule-engine~=4.5
30
32
  Requires-Dist: textual-fspicker~=0.4
31
- Requires-Dist: textual~=3.3
33
+ Requires-Dist: textual~=3.6
32
34
  Requires-Dist: typing-extensions~=4.14
35
+ Requires-Dist: uvloop~=0.21
33
36
  Description-Content-Type: text/markdown
34
37
 
35
38
  # YNAB CLI
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ynab-cli"
3
- version = "0.5.4"
3
+ version = "0.6.1"
4
4
  description = "YNAB Command Line Interface"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -33,17 +33,20 @@ 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",
39
40
  "httpx~=0.28",
41
+ "lagom~=2.7",
40
42
  "python-dotenv~=1.1",
41
43
  "rapidfuzz~=3.13",
42
44
  "rich~=14.0",
43
45
  "rule-engine~=4.5",
44
46
  "textual-fspicker~=0.4",
45
- "textual~=3.3",
47
+ "textual~=3.6",
46
48
  "typing-extensions~=4.14",
49
+ "uvloop~=0.21",
47
50
  ]
48
51
 
49
52
  [project.urls]
@@ -59,18 +62,18 @@ ynab-cli = "ynab_cli.host.cli:main"
59
62
 
60
63
  [dependency-groups]
61
64
  dev = [
62
- "commitizen>=4.8.2",
65
+ "commitizen>=4.8.3",
63
66
  "factory-boy>=3.3.3",
64
67
  "mkdocs-click>=0.9.0",
65
68
  "mkdocs-material>=9.6.14",
66
69
  "mkdocs>=1.6.1",
67
70
  "mypy>=1.16.0",
68
- "pytest-asyncio>=1.0.0",
69
- "pytest-cov>=6.1.1",
71
+ "pytest-cov>=6.2.1",
70
72
  "pytest-mock>=3.14.1",
71
- "pytest>=8.4.0",
72
- "ruff>=0.11.12",
73
+ "pytest>=8.4.1",
74
+ "ruff>=0.12.1",
73
75
  "textual-dev>=1.7.0",
76
+ "trio>=0.30.0",
74
77
  "types-python-dateutil>=2.9.0.20250516",
75
78
  ]
76
79
 
@@ -136,6 +139,12 @@ max-complexity = 70
136
139
  builtins-ignorelist = ["type", "id"]
137
140
 
138
141
 
142
+ [tool.coverage.run]
143
+ omit = [
144
+ "src/ynab_cli/adapters/ynab/**/*",
145
+ ]
146
+
147
+
139
148
  [tool.commitizen]
140
149
  name = "cz_conventional_commits"
141
150
  annotated_tag = true
@@ -6,8 +6,9 @@ from ynab_cli.domain.ports.io import IO, Progress
6
6
 
7
7
 
8
8
  class RichProgress(Progress):
9
- def __init__(self, progress_info: tuple[rich_progress.Progress, rich_progress.TaskID]) -> None:
10
- self._progress, self._task_id = progress_info
9
+ def __init__(self, progress: rich_progress.Progress) -> None:
10
+ self._progress = progress
11
+ self._task_id = progress.add_task("Loading...")
11
12
 
12
13
  @override
13
14
  async def update(
@@ -20,10 +21,9 @@ class RichProgress(Progress):
20
21
 
21
22
 
22
23
  class RichIO(IO):
23
- def __init__(self, progress_info: tuple[rich_progress.Progress, rich_progress.TaskID]) -> None:
24
- progress, _ = progress_info
24
+ def __init__(self, progress: rich_progress.Progress) -> None:
25
25
  self._progress = progress
26
- self.progress = RichProgress(progress_info)
26
+ self.progress = RichProgress(self._progress)
27
27
 
28
28
  @override
29
29
  async def prompt(self, prompt: str, password: bool = False) -> str:
@@ -58,7 +58,19 @@ async def run_asyncio_detailed(
58
58
  if not new_access_token:
59
59
  raise e
60
60
 
61
+ #
62
+ # Update the client with the new access token
63
+ # Also need to update the headers in the underlying
64
+ # HTTPX clients if they are set up.
65
+ #
61
66
  client.token = new_access_token
67
+ auth_header = f"{client.prefix} {client.token}" if client.prefix else client.token
68
+ client._headers[client.auth_header_name] = auth_header
69
+ if client._async_client:
70
+ client._async_client.headers[client.auth_header_name] = auth_header
71
+ if client._client:
72
+ client._client.headers[client.auth_header_name] = auth_header
73
+
62
74
  response = await asyncio_detailed(*args, **kwargs)
63
75
  parsed_data = get_parsed_response_data(response)
64
76
  else:
@@ -0,0 +1,47 @@
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.settings import Settings
9
+
10
+
11
+ class ListAllParams(TypedDict):
12
+ pass
13
+
14
+
15
+ class ListAll:
16
+ """Use case for listing all budgets."""
17
+
18
+ def __init__(self, io: ports.IO, client: ynab.AuthenticatedClient):
19
+ self._io = io
20
+ self._client = client
21
+
22
+ async def __call__(self, settings: Settings, params: ListAllParams) -> AsyncIterator[models.BudgetSummary]:
23
+ try:
24
+ progress_total = 0
25
+
26
+ budgets = (
27
+ await util.get_asyncio_detailed(
28
+ self._io, get_budgets.asyncio_detailed, include_accounts=False, client=self._client
29
+ )
30
+ ).data.budgets
31
+ budgets.sort(key=lambda b: b.name)
32
+
33
+ progress_total = len(budgets)
34
+ await self._io.progress.update(total=progress_total)
35
+ for budget in budgets:
36
+ await self._io.progress.update(advance=1)
37
+ yield budget
38
+
39
+ except Exception as e:
40
+ if isinstance(e, util.ApiError) and e.status_code == 401:
41
+ await self._io.print("Invalid or expired access token. Please update your settings.")
42
+ elif isinstance(e, util.ApiError) and e.status_code == 429:
43
+ await self._io.print("API rate limit exceeded. Try again later, or get a new access token.")
44
+ else:
45
+ await self._io.print(f"Exception when calling YNAB: {e}")
46
+ finally:
47
+ await self._io.progress.update(total=progress_total, completed=progress_total)
@@ -6,7 +6,6 @@ from ynab_cli.adapters.ynab import models, util
6
6
  from ynab_cli.adapters.ynab.api.categories import get_categories
7
7
  from ynab_cli.adapters.ynab.api.transactions import get_transactions_by_category
8
8
  from ynab_cli.domain import ports
9
- from ynab_cli.domain.constants import YNAB_API_URL
10
9
  from ynab_cli.domain.settings import Settings
11
10
 
12
11
 
@@ -20,47 +19,48 @@ class ListUnusedParams(TypedDict):
20
19
  pass
21
20
 
22
21
 
23
- async def list_unused(
24
- settings: Settings, io: ports.IO, params: ListUnusedParams, *, client: ynab.AuthenticatedClient | None = None
25
- ) -> AsyncIterator[models.Category]:
26
- try:
27
- progress_total = 0
22
+ class ListUnused:
23
+ """Use case for listing unused categories."""
28
24
 
29
- if client is None:
30
- client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
25
+ def __init__(self, io: ports.IO, client: ynab.AuthenticatedClient):
26
+ self._io = io
27
+ self._client = client
28
+
29
+ async def __call__(self, settings: Settings, params: ListUnusedParams) -> AsyncIterator[models.Category]:
30
+ try:
31
+ progress_total = 0
31
32
 
32
- async with client:
33
33
  category_groups = (
34
34
  await util.get_asyncio_detailed(
35
- io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=client
35
+ self._io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=self._client
36
36
  )
37
37
  ).data.category_groups
38
38
  category_groups.sort(key=lambda cg: cg.name)
39
39
 
40
40
  progress_total = len(category_groups)
41
- await io.progress.update(total=progress_total)
41
+ await self._io.progress.update(total=progress_total)
42
42
  for category_group in category_groups:
43
- await io.progress.update(advance=1)
43
+ await self._io.progress.update(advance=1)
44
44
 
45
45
  if _should_skip_category_or_group(category_or_group=category_group):
46
46
  continue
47
47
 
48
48
  progress_total += len(category_group.categories)
49
- await io.progress.update(total=progress_total)
49
+ await self._io.progress.update(total=progress_total)
50
50
  category_group.categories.sort(key=lambda c: c.name)
51
51
  for category in category_group.categories:
52
- await io.progress.update(advance=1)
52
+ await self._io.progress.update(advance=1)
53
53
 
54
54
  if _should_skip_category_or_group(category_or_group=category):
55
55
  continue
56
56
 
57
57
  transactions = (
58
58
  await util.get_asyncio_detailed(
59
- io,
59
+ self._io,
60
60
  get_transactions_by_category.asyncio_detailed,
61
61
  settings.ynab.budget_id,
62
62
  str(category.id),
63
- client=client,
63
+ client=self._client,
64
64
  )
65
65
  ).data.transactions
66
66
  num_transactions = len(transactions)
@@ -69,63 +69,64 @@ async def list_unused(
69
69
  if not num_transactions:
70
70
  yield category
71
71
 
72
- except Exception as e:
73
- if isinstance(e, util.ApiError) and e.status_code == 401:
74
- await io.print("Invalid or expired access token. Please update your settings.")
75
- elif isinstance(e, util.ApiError) and e.status_code == 429:
76
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
77
- else:
78
- await io.print(f"Exception when calling YNAB: {e}\n")
79
- finally:
80
- await io.progress.update(total=progress_total, completed=progress_total)
72
+ except Exception as e:
73
+ if isinstance(e, util.ApiError) and e.status_code == 401:
74
+ await self._io.print("Invalid or expired access token. Please update your settings.")
75
+ elif isinstance(e, util.ApiError) and e.status_code == 429:
76
+ await self._io.print("API rate limit exceeded. Try again later, or get a new access token.")
77
+ else:
78
+ await self._io.print(f"Exception when calling YNAB: {e}")
79
+ finally:
80
+ await self._io.progress.update(total=progress_total, completed=progress_total)
81
81
 
82
82
 
83
83
  class ListAllParams(TypedDict):
84
84
  pass
85
85
 
86
86
 
87
- async def list_all(
88
- settings: Settings, io: ports.IO, params: ListAllParams, *, client: ynab.AuthenticatedClient | None = None
89
- ) -> AsyncIterator[models.Category]:
90
- try:
91
- progress_total = 0
87
+ class ListAll:
88
+ """Use case for listing all categories."""
89
+
90
+ def __init__(self, io: ports.IO, client: ynab.AuthenticatedClient):
91
+ self._io = io
92
+ self._client = client
92
93
 
93
- if client is None:
94
- client = ynab.AuthenticatedClient(base_url=YNAB_API_URL, token=settings.ynab.access_token)
94
+ async def __call__(self, settings: Settings, params: ListAllParams) -> AsyncIterator[models.Category]:
95
+ try:
96
+ progress_total = 0
95
97
 
96
- async with client:
97
98
  category_groups = (
98
99
  await util.get_asyncio_detailed(
99
- io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=client
100
+ self._io, get_categories.asyncio_detailed, settings.ynab.budget_id, client=self._client
100
101
  )
101
102
  ).data.category_groups
102
103
  category_groups.sort(key=lambda cg: cg.name)
103
104
 
104
105
  progress_total = len(category_groups)
105
- await io.progress.update(total=progress_total)
106
+ await self._io.progress.update(total=progress_total)
106
107
  for category_group in category_groups:
107
- await io.progress.update(advance=1)
108
+ await self._io.progress.update(advance=1)
108
109
 
109
110
  if _should_skip_category_or_group(category_or_group=category_group):
110
111
  continue
111
112
 
112
113
  progress_total += len(category_group.categories)
113
- await io.progress.update(total=progress_total)
114
+ await self._io.progress.update(total=progress_total)
114
115
  category_group.categories.sort(key=lambda c: c.name)
115
116
  for category in category_group.categories:
116
- await io.progress.update(advance=1)
117
+ await self._io.progress.update(advance=1)
117
118
 
118
119
  if _should_skip_category_or_group(category_or_group=category):
119
120
  continue
120
121
 
121
122
  yield category
122
123
 
123
- except Exception as e:
124
- if isinstance(e, util.ApiError) and e.status_code == 401:
125
- await io.print("Invalid or expired access token. Please update your settings.")
126
- elif isinstance(e, util.ApiError) and e.status_code == 429:
127
- await io.print("API rate limit exceeded. Try again later, or get a new access token.")
128
- else:
129
- await io.print(f"Exception when calling YNAB: {e}\n")
130
- finally:
131
- await io.progress.update(total=progress_total, completed=progress_total)
124
+ except Exception as e:
125
+ if isinstance(e, util.ApiError) and e.status_code == 401:
126
+ await self._io.print("Invalid or expired access token. Please update your settings.")
127
+ elif isinstance(e, util.ApiError) and e.status_code == 429:
128
+ await self._io.print("API rate limit exceeded. Try again later, or get a new access token.")
129
+ else:
130
+ await self._io.print(f"Exception when calling YNAB: {e}")
131
+ finally:
132
+ await self._io.progress.update(total=progress_total, completed=progress_total)