ynab-api-cli 0.1.0__tar.gz → 0.1.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.
- ynab_api_cli-0.1.1/.github/workflows/live.yml +35 -0
- ynab_api_cli-0.1.1/.github/workflows/release.yml +47 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/PKG-INFO +14 -5
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/README.md +8 -0
- ynab_api_cli-0.1.1/docs/live-ci.md +41 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/docs/live-testing.md +8 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/pyproject.toml +6 -5
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/pyproject.toml.orig +6 -5
- ynab_api_cli-0.1.1/src/ynab_api_cli/__init__.py +16 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/__main__.py +10 -0
- ynab_api_cli-0.1.1/src/ynab_api_cli/ci_live.py +74 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/live.py +7 -0
- ynab_api_cli-0.1.1/tests/test_ci_live.py +166 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_cli.py +25 -0
- ynab_api_cli-0.1.0/.github/workflows/release.yml +0 -172
- ynab_api_cli-0.1.0/src/ynab_api_cli/__init__.py +0 -3
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/.github/workflows/ci.yml +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/.release-please-manifest.json +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/CHANGELOG.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/docs/maintenance.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/docs/privacy-review.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/docs/publishing.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/docs/python-development-review.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/docs/verification.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/release-please-config.json +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/skills/ynab-api-cli/SKILL.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/skills/ynab-api-cli/references/operations.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/skills/ynab-api-cli/references/setup.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/cli.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/compat.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/config.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/contract.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/contract_testing.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/development.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/distribution.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/exceptions.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/accounts/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/accounts/create_account.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/accounts/get_account_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/accounts/get_accounts.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/create_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/create_category_group.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/get_categories.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/get_category_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/get_month_category_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/update_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/update_category_group.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/categories/update_month_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/money_movements/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/money_movements/get_money_movement_groups.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/money_movements/get_money_movement_groups_by_month.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/money_movements/get_money_movements.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/money_movements/get_money_movements_by_month.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/months/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/months/get_plan_month.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/months/get_plan_months.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payee_locations/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payee_locations/get_payee_location_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payee_locations/get_payee_locations.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payee_locations/get_payee_locations_by_payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payees/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payees/create_payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payees/get_payee_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payees/get_payees.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/payees/update_payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/plans/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/plans/get_plan_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/plans/get_plan_settings_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/plans/get_plans.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/scheduled_transactions/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/scheduled_transactions/create_scheduled_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/scheduled_transactions/delete_scheduled_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/scheduled_transactions/get_scheduled_transaction_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/scheduled_transactions/get_scheduled_transactions.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/scheduled_transactions/update_scheduled_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/create_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/delete_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/get_transaction_by_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/get_transactions.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/get_transactions_by_account.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/get_transactions_by_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/get_transactions_by_month.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/get_transactions_by_payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/import_transactions.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/update_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/transactions/update_transactions.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/user/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/api/user/get_user.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/bindings.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/account.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/account_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/account_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/account_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/account_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/accounts_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/accounts_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/bulk_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/bulk_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/bulk_response_data_bulk.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/bulk_transactions.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/categories_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/categories_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/category_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/category_group.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/category_group_with_categories.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/category_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/category_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/currency_format_type_0.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/date_format_type_0.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/debt_1.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/debt_2.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/debt_3.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/error_detail.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/error_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/existing_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/existing_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/flag_1.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/flag_2.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/flag_3.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/get_transactions_by_account_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/get_transactions_by_category_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/get_transactions_by_month_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/get_transactions_by_payee_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/get_transactions_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/goal_1.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/goal_2.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/goal_3.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/hybrid_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/hybrid_transaction_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/hybrid_transactions_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/hybrid_transactions_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/loan_account_periodic_value_type_0.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movement.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movement_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movement_group.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movement_groups_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movement_groups_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movements_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/money_movements_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_detail.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_detail_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_detail_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_detail_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_summaries_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_summaries_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_summary.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/month_summary_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/new_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/new_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/patch_category_group_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/patch_category_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/patch_month_category_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/patch_payee_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/patch_transactions_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_location.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_location_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_location_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_locations_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_locations_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payee_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payees_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/payees_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_detail.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_detail_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_detail_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_settings.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_settings_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_settings_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_summary.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_summary_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/plan_summary_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_account_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_category_group_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_category_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_payee_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_scheduled_transaction_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/post_transactions_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/put_scheduled_transaction_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/put_transaction_wrapper.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_account.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_account_type.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category_goal_frequency.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category_group.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category_group_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category_group_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_category_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_month_category.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_payee.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_payee_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_payee_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_scheduled_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_sub_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_transaction_with_id_or_import_id.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_transaction_with_optional_fields.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_transactions_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/save_transactions_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_sub_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_sub_transaction_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_detail.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_frequency.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_summary.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_summary_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transaction_summary_base_frequency.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transactions_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/scheduled_transactions_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/sub_transaction.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/sub_transaction_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transaction_cleared_status.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transaction_detail.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transaction_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transaction_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transaction_summary.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transaction_summary_base.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transactions_import_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transactions_import_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transactions_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/transactions_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/user.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/user_response.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/generated/models/user_response_data.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/py.typed +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/__init__.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/catalog.json +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/openapi.yaml +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/provenance.json +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/schema_features.json +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/skill/SKILL.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/skill/references/operations.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/resources/skill/references/setup.md +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/security.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/transport.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/types.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/src/ynab_api_cli/verification.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/templates/endpoint_module.py.jinja +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/templates/model.py.jinja +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/conftest.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/fixtures/wire_cases.json +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/support.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_compat.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_config.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_contract.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_development.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_distribution.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_features.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_live.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_transport.py +0 -0
- {ynab_api_cli-0.1.0 → ynab_api_cli-0.1.1}/tests/test_typing.py +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: Read-only live YNAB tests
|
|
2
|
+
|
|
3
|
+
# Environment secrets must be configured by the repository owner separately.
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
permissions:
|
|
8
|
+
contents: read
|
|
9
|
+
|
|
10
|
+
concurrency:
|
|
11
|
+
group: ynab-live
|
|
12
|
+
cancel-in-progress: false
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
live:
|
|
16
|
+
if: github.ref == 'refs/heads/main'
|
|
17
|
+
runs-on: ubuntu-latest
|
|
18
|
+
timeout-minutes: 10
|
|
19
|
+
environment: ynab-e2e
|
|
20
|
+
steps:
|
|
21
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
22
|
+
with:
|
|
23
|
+
persist-credentials: false
|
|
24
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
25
|
+
with:
|
|
26
|
+
enable-cache: false
|
|
27
|
+
- name: Install reviewed locked dependencies without YNAB secrets
|
|
28
|
+
run: uv sync --locked --no-editable
|
|
29
|
+
- name: Run read-only probes with step-scoped secrets
|
|
30
|
+
env:
|
|
31
|
+
YNAB_ACCESS_TOKEN: ${{ secrets.YNAB_ACCESS_TOKEN }}
|
|
32
|
+
YNAB_TEST_PLAN_ID: ${{ secrets.YNAB_TEST_PLAN_ID }}
|
|
33
|
+
run: |
|
|
34
|
+
ulimit -c 0
|
|
35
|
+
.venv/bin/python -m ynab_api_cli.ci_live
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
name: Release (legacy ynab-api-cli)
|
|
2
|
+
# Publishes the final, deprecated ynab-api-cli release from the legacy/ynab-api-cli branch.
|
|
3
|
+
# The maintained package is codapact-ynab, released from main.
|
|
4
|
+
on:
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
concurrency:
|
|
9
|
+
group: release-legacy
|
|
10
|
+
cancel-in-progress: false
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
if: github.ref == 'refs/heads/legacy/ynab-api-cli'
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
17
|
+
with:
|
|
18
|
+
persist-credentials: false
|
|
19
|
+
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
20
|
+
- run: uv sync --locked
|
|
21
|
+
- run: uv run poe check
|
|
22
|
+
- run: uv build
|
|
23
|
+
- run: uvx --from twine==6.2.0 twine check --strict dist/*.whl dist/*.tar.gz
|
|
24
|
+
- run: uv run poe distribution-check
|
|
25
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
26
|
+
with:
|
|
27
|
+
name: python-distributions
|
|
28
|
+
path: |
|
|
29
|
+
dist/*.whl
|
|
30
|
+
dist/*.tar.gz
|
|
31
|
+
if-no-files-found: error
|
|
32
|
+
|
|
33
|
+
publish-pypi:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment:
|
|
37
|
+
name: pypi
|
|
38
|
+
url: https://pypi.org/project/ynab-api-cli/
|
|
39
|
+
permissions:
|
|
40
|
+
id-token: write
|
|
41
|
+
steps:
|
|
42
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
43
|
+
with:
|
|
44
|
+
name: python-distributions
|
|
45
|
+
path: dist/
|
|
46
|
+
- name: Publish through trusted publishing
|
|
47
|
+
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: ynab-api-cli
|
|
3
|
-
Version: 0.1.
|
|
4
|
-
Summary:
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: DEPRECATED: use codapact-ynab. Contract-driven REST CLI for YNAB
|
|
5
5
|
Author: Mike Hill
|
|
6
|
+
Classifier: Development Status :: 7 - Inactive
|
|
6
7
|
Requires-Dist: httpx>=0.28.1
|
|
7
8
|
Requires-Dist: jsonschema[format]>=4.26.0
|
|
8
9
|
Requires-Dist: pydantic>=2.14.1
|
|
9
10
|
Requires-Dist: pyyaml>=6.0.3
|
|
10
11
|
Requires-Python: >=3.12
|
|
11
|
-
Project-URL: Homepage, https://github.com/mikejhill/ynab
|
|
12
|
-
Project-URL: Repository, https://github.com/mikejhill/ynab
|
|
13
|
-
Project-URL: Issues, https://github.com/mikejhill/ynab
|
|
12
|
+
Project-URL: Homepage, https://github.com/mikejhill/codapact-ynab
|
|
13
|
+
Project-URL: Repository, https://github.com/mikejhill/codapact-ynab
|
|
14
|
+
Project-URL: Issues, https://github.com/mikejhill/codapact-ynab/issues
|
|
14
15
|
Description-Content-Type: text/markdown
|
|
15
16
|
|
|
17
|
+
> [!WARNING]
|
|
18
|
+
> **Deprecated.** `ynab-api-cli` receives no further updates. Use
|
|
19
|
+
> [`codapact-ynab`](https://pypi.org/project/codapact-ynab/) instead:
|
|
20
|
+
> `uv tool install codapact-ynab`, then run `ynab --help`.
|
|
21
|
+
> Source: https://github.com/mikejhill/codapact-ynab
|
|
22
|
+
|
|
16
23
|
# YNAB API CLI
|
|
17
24
|
|
|
18
25
|
Complete, synchronous REST CLI for the reviewed YNAB API contract. The official SDK is not a dependency. Generated, strictly typed endpoint classes and models are included for Python callers.
|
|
@@ -74,6 +81,8 @@ The contract snapshot, reviewed operation catalog, and provenance are packaged r
|
|
|
74
81
|
|
|
75
82
|
See [maintenance](docs/maintenance.md) for reviewed upgrades, test coverage, and CI. See [live testing](docs/live-testing.md) before enabling writes against a dedicated test plan.
|
|
76
83
|
|
|
84
|
+
See [secure live CI](docs/live-ci.md) for the separate, manually approved read-only workflow. Ordinary CI requires no YNAB credentials. No local token is uploaded automatically.
|
|
85
|
+
|
|
77
86
|
## Supplementary Codex skill
|
|
78
87
|
|
|
79
88
|
```text
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
> [!WARNING]
|
|
2
|
+
> **Deprecated.** `ynab-api-cli` receives no further updates. Use
|
|
3
|
+
> [`codapact-ynab`](https://pypi.org/project/codapact-ynab/) instead:
|
|
4
|
+
> `uv tool install codapact-ynab`, then run `ynab --help`.
|
|
5
|
+
> Source: https://github.com/mikejhill/codapact-ynab
|
|
6
|
+
|
|
1
7
|
# YNAB API CLI
|
|
2
8
|
|
|
3
9
|
Complete, synchronous REST CLI for the reviewed YNAB API contract. The official SDK is not a dependency. Generated, strictly typed endpoint classes and models are included for Python callers.
|
|
@@ -59,6 +65,8 @@ The contract snapshot, reviewed operation catalog, and provenance are packaged r
|
|
|
59
65
|
|
|
60
66
|
See [maintenance](docs/maintenance.md) for reviewed upgrades, test coverage, and CI. See [live testing](docs/live-testing.md) before enabling writes against a dedicated test plan.
|
|
61
67
|
|
|
68
|
+
See [secure live CI](docs/live-ci.md) for the separate, manually approved read-only workflow. Ordinary CI requires no YNAB credentials. No local token is uploaded automatically.
|
|
69
|
+
|
|
62
70
|
## Supplementary Codex skill
|
|
63
71
|
|
|
64
72
|
```text
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Live tests without publishing credentials
|
|
2
|
+
|
|
3
|
+
The ordinary PR, push, scheduled contract, and release checks do not use YNAB credentials. The separate `live.yml` workflow is manual, restricted to main, and read-only. It checks the explicit disposable plan, validates available responses against the reviewed contract, and reports operation names and gaps. It does not print plan IDs, journal paths, response bodies, or fixture IDs.
|
|
4
|
+
|
|
5
|
+
## Security boundary
|
|
6
|
+
|
|
7
|
+
GitHub encrypts environment secrets, but the approved runner and application necessarily receive the plaintext token in memory. Secret masking is not an exfiltration barrier: malicious workflow code, actions, or dependencies can steal a token. If your requirement is that the token never leaves this computer, keep live tests local and do not configure a GitHub secret. See [GitHub's secret guidance](https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets).
|
|
8
|
+
|
|
9
|
+
YNAB personal access tokens are account credentials. Selecting a dummy plan in the CLI does not restrict the token's API permissions. For CI, use a dedicated test account containing only disposable data where possible; a separately issued token on the same account is independently revocable but still has account access. See [YNAB authentication](https://api.ynab.com/#personal-access-tokens).
|
|
10
|
+
|
|
11
|
+
## Configure the protected environment before enabling the workflow
|
|
12
|
+
|
|
13
|
+
1. Open repository **Settings → Environments → New environment**, and name it `ynab-e2e`. Do not reuse `pypi`.
|
|
14
|
+
2. Add a trusted maintainer as a required reviewer. For a single-maintainer repository, allow that maintainer to review their own manual run; preventing self-review requires another reviewer. Restrict deployment branches to the selected branch `main`, and prevent bypassing protection rules where available.
|
|
15
|
+
3. Protect main through branch rules, require code review, and review changes to `live.yml`, `ci_live.py`, `live.py`, transport code, actions, and `uv.lock` before approving a live job. Pin actions to reviewed commit SHAs.
|
|
16
|
+
4. Create environment secrets `YNAB_ACCESS_TOKEN` and `YNAB_TEST_PLAN_ID`. The latter must be the explicit dummy-plan UUID. The plan name must begin `YNAB API CLI TEST`.
|
|
17
|
+
|
|
18
|
+
Use GitHub's environment-secret UI or hidden local GitHub CLI prompts:
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
gh secret set YNAB_ACCESS_TOKEN --repo mikejhill/ynab-api-cli --env ynab-e2e
|
|
22
|
+
gh secret set YNAB_TEST_PLAN_ID --repo mikejhill/ynab-api-cli --env ynab-e2e
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Enter each value only at its local prompt. Do not use `--body TOKEN`, shell literals, command substitution that could echo it, chat, or a committed file. This setup sends the token to GitHub's encrypted secret store; it does not keep the token exclusively local.
|
|
26
|
+
|
|
27
|
+
5. After merging the reviewed workflow into main, open **Actions → Read-only live YNAB tests → Run workflow**, choose main, inspect the exact commit and changes, then approve its environment deployment. [Environment protection rules](https://docs.github.com/en/actions/reference/workflows-and-actions/deployments-and-environments) gate when environment secrets become available.
|
|
28
|
+
|
|
29
|
+
The workflow has no PR trigger, no `pull_request_target`, no user-selected checkout ref, no artifact uploads, and no dependency caching. Dependency installation runs before the token-bearing step. Only that step receives the secrets. The helper consumes them from its process environment, creates a permission-protected temporary auth file, and removes its temporary files on success or failure. The job uses a fresh GitHub-hosted runner. Reports contain only fixed status fields, reviewed operation names, and capability gaps. Do not enable shell tracing, HTTP/debug logging, step debugging, core dumps, response uploads, or caches containing configuration.
|
|
30
|
+
|
|
31
|
+
Account-wide plan listing and user reads are excluded. This reduces application scope; it does not reduce token permissions. Read-only CI intentionally reports missing write and fixture-dependent operations, so success does not mean all-feature live coverage.
|
|
32
|
+
|
|
33
|
+
## Keep mutation tests local
|
|
34
|
+
|
|
35
|
+
The full runner creates reusable accounts, a category group, a category, and a payee, and exercises transaction and budgeting changes. Some fixtures cannot be deleted through the API. It persists private mutation intents and stops on uncertain outcomes. Keep that journal outside the repository and reconcile uncertain writes before rerunning.
|
|
36
|
+
|
|
37
|
+
Write tests are deliberately absent from `live.yml`. Ephemeral CI journals disappear when a job ends; blindly rerunning a failed write job can duplicate an uncertain mutation. Adding CI writes requires a reviewed design for durable private journal storage, restoration before every run, concurrency, reconciliation, retention, and cleanup. Do not upload an auth file or mutation journal as a public workflow artifact.
|
|
38
|
+
|
|
39
|
+
## Rotation and incident response
|
|
40
|
+
|
|
41
|
+
Revoke the CI token in YNAB Developer Settings and replace the environment secret to rotate it. Remove the secret and disable the live workflow when no longer needed. If a token may have appeared in output or an untrusted runner, revoke it immediately; deleting logs alone is insufficient. No local token is uploaded automatically by this project.
|
|
@@ -25,3 +25,11 @@ Before each mutation, the runner writes a private intent journal. An uncertain o
|
|
|
25
25
|
Payee-location reads require location fixtures. Direct Import needs a linked bank account; manual test accounts cannot verify actual bank imports. The report lists these gaps and never claims full live coverage when they remain.
|
|
26
26
|
|
|
27
27
|
No live account connection is provisioned by installing this project. Supply credentials and a designated plan explicitly to run these tests.
|
|
28
|
+
|
|
29
|
+
## Verified live run, 2026-10-18
|
|
30
|
+
|
|
31
|
+
An explicitly designated disposable plan passed the real read/write lifecycle run: 43 of 44 operations completed. The run covered accounts, categories, payees, transaction creation and bulk updates, duplicate import IDs, splits, transfers, scheduled transactions, budgeting, and deletion checks. The missing operation was `getPayeeLocationById` because no suitable location fixture existed. The import endpoint completed, but actual bank import remains unverified for manual accounts.
|
|
32
|
+
|
|
33
|
+
Subsequent reads confirmed that both fixture account balances returned to zero, and the private journal had no unresolved mutation. Reusable accounts, category group, category, and payee remain in the disposable plan. No token, plan UUID, account data, or response bodies are included in this report.
|
|
34
|
+
|
|
35
|
+
The proposed CI-specific read-only probe also passed 15 selected-plan operations against that plan. See [secure live CI](live-ci.md) before putting credentials on a remote runner. These results do not claim full live coverage.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ynab-api-cli"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "DEPRECATED: use codapact-ynab. Contract-driven REST CLI for YNAB"
|
|
5
|
+
classifiers = ["Development Status :: 7 - Inactive"]
|
|
5
6
|
readme = "README.md"
|
|
6
7
|
requires-python = ">=3.12"
|
|
7
8
|
dependencies = [
|
|
@@ -18,9 +19,9 @@ name = "Mike Hill"
|
|
|
18
19
|
ynab-api-cli = "ynab_api_cli.__main__:Application.main"
|
|
19
20
|
|
|
20
21
|
[project.urls]
|
|
21
|
-
Homepage = "https://github.com/mikejhill/ynab
|
|
22
|
-
Repository = "https://github.com/mikejhill/ynab
|
|
23
|
-
Issues = "https://github.com/mikejhill/ynab
|
|
22
|
+
Homepage = "https://github.com/mikejhill/codapact-ynab"
|
|
23
|
+
Repository = "https://github.com/mikejhill/codapact-ynab"
|
|
24
|
+
Issues = "https://github.com/mikejhill/codapact-ynab/issues"
|
|
24
25
|
|
|
25
26
|
[build-system]
|
|
26
27
|
requires = ["uv_build>=0.11.32,<0.12.0"]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "ynab-api-cli"
|
|
3
|
-
version = "0.1.
|
|
4
|
-
description = "
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "DEPRECATED: use codapact-ynab. Contract-driven REST CLI for YNAB"
|
|
5
|
+
classifiers = ["Development Status :: 7 - Inactive"]
|
|
5
6
|
readme = "README.md"
|
|
6
7
|
authors = [
|
|
7
8
|
{ name = "Mike Hill" }
|
|
@@ -18,9 +19,9 @@ dependencies = [
|
|
|
18
19
|
ynab-api-cli = "ynab_api_cli.__main__:Application.main"
|
|
19
20
|
|
|
20
21
|
[project.urls]
|
|
21
|
-
Homepage = "https://github.com/mikejhill/ynab
|
|
22
|
-
Repository = "https://github.com/mikejhill/ynab
|
|
23
|
-
Issues = "https://github.com/mikejhill/ynab
|
|
22
|
+
Homepage = "https://github.com/mikejhill/codapact-ynab"
|
|
23
|
+
Repository = "https://github.com/mikejhill/codapact-ynab"
|
|
24
|
+
Issues = "https://github.com/mikejhill/codapact-ynab/issues"
|
|
24
25
|
|
|
25
26
|
[build-system]
|
|
26
27
|
requires = ["uv_build>=0.11.32,<0.12.0"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Deprecated: use codapact-ynab (https://pypi.org/project/codapact-ynab/)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import warnings
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
# The console script prints its own notice; warn library importers only.
|
|
10
|
+
if Path(sys.argv[0] if sys.argv else "").stem != "ynab-api-cli":
|
|
11
|
+
warnings.warn(
|
|
12
|
+
"ynab-api-cli is deprecated; switch to codapact-ynab "
|
|
13
|
+
"(import codapact_ynab, command `ynab`): https://pypi.org/project/codapact-ynab/",
|
|
14
|
+
DeprecationWarning,
|
|
15
|
+
stacklevel=2,
|
|
16
|
+
)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import getpass
|
|
6
|
+
import io
|
|
6
7
|
import json
|
|
7
8
|
import logging
|
|
8
9
|
import shutil
|
|
@@ -24,6 +25,12 @@ from ynab_api_cli.types import JsonCodec, JsonValue, RequestValues
|
|
|
24
25
|
|
|
25
26
|
logger = logging.getLogger(__name__)
|
|
26
27
|
|
|
28
|
+
DEPRECATION_NOTICE = (
|
|
29
|
+
"ynab-api-cli is deprecated and receives no further updates. "
|
|
30
|
+
"Switch to codapact-ynab: `uv tool install codapact-ynab`, then run `ynab`. "
|
|
31
|
+
"https://pypi.org/project/codapact-ynab/"
|
|
32
|
+
)
|
|
33
|
+
|
|
27
34
|
|
|
28
35
|
class Application:
|
|
29
36
|
"""Coordinate connection setup and reviewed typed operation dispatch."""
|
|
@@ -38,6 +45,9 @@ class Application:
|
|
|
38
45
|
@classmethod
|
|
39
46
|
def main(cls, argv: Sequence[str] | None = None) -> None:
|
|
40
47
|
"""Run the console entry point and return its documented exit status."""
|
|
48
|
+
if isinstance(sys.stdout, io.TextIOWrapper):
|
|
49
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
50
|
+
sys.stderr.write(f"warning: {DEPRECATION_NOTICE}\n")
|
|
41
51
|
sys.exit(cls().run(argv))
|
|
42
52
|
|
|
43
53
|
def run(self, argv: Sequence[str] | None = None) -> int:
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Run read-only CI probes with temporary credentials and sanitized output."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
import tempfile
|
|
10
|
+
from collections.abc import MutableMapping
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
|
|
16
|
+
from ynab_api_cli.config import ConfigStore
|
|
17
|
+
from ynab_api_cli.live import LiveOptions, LiveRunner
|
|
18
|
+
from ynab_api_cli.transport import Connection, Transport
|
|
19
|
+
from ynab_api_cli.types import JsonValue
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Application:
|
|
25
|
+
"""Consume CI secrets once and expose only operation-level test results."""
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def main(cls, client: httpx.Client | None = None) -> None:
|
|
29
|
+
"""Run the probe without emitting exception details or API payloads."""
|
|
30
|
+
logging.basicConfig(level=logging.WARNING)
|
|
31
|
+
try:
|
|
32
|
+
report = cls.run(os.environ, client)
|
|
33
|
+
except KeyboardInterrupt:
|
|
34
|
+
raise SystemExit(130) from None
|
|
35
|
+
except Exception: # noqa: BLE001 - redact unexpected failures at the process boundary.
|
|
36
|
+
logger.error( # noqa: TRY400 - tracebacks may expose credentials.
|
|
37
|
+
"Live CI probe failed; no response or credential details logged."
|
|
38
|
+
)
|
|
39
|
+
raise SystemExit(1) from None
|
|
40
|
+
sys.stdout.write(json.dumps(report, indent=2) + "\n")
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def run(
|
|
44
|
+
environment: MutableMapping[str, str], client: httpx.Client | None = None
|
|
45
|
+
) -> dict[str, JsonValue]:
|
|
46
|
+
"""Use explicit environment secrets and remove private files on all exits."""
|
|
47
|
+
token = environment.pop("YNAB_ACCESS_TOKEN", "")
|
|
48
|
+
plan = environment.pop("YNAB_TEST_PLAN_ID", "")
|
|
49
|
+
connection = Connection(token, str(UUID(plan)))
|
|
50
|
+
with tempfile.TemporaryDirectory(prefix="ynab-api-cli-live-") as temporary:
|
|
51
|
+
root = Path(temporary)
|
|
52
|
+
store = ConfigStore(root / "config")
|
|
53
|
+
store.configure(connection.token)
|
|
54
|
+
options = LiveOptions(
|
|
55
|
+
plan_id=connection.plan_id or "",
|
|
56
|
+
auth_file=store.root / "auth.json",
|
|
57
|
+
state_directory=root / "state",
|
|
58
|
+
enable_writes=False,
|
|
59
|
+
account_reads=False,
|
|
60
|
+
)
|
|
61
|
+
runner = LiveRunner(options, Transport(connection, client=client))
|
|
62
|
+
result = runner.run()
|
|
63
|
+
return {
|
|
64
|
+
"ok": True,
|
|
65
|
+
"mode": "read-only",
|
|
66
|
+
"completed_operations": result["completed_operations"],
|
|
67
|
+
"missing_operations": result["missing_operations"],
|
|
68
|
+
"gaps": result["gaps"],
|
|
69
|
+
"complete_live_coverage": False,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
Application.main()
|
|
@@ -30,6 +30,7 @@ class LiveOptions:
|
|
|
30
30
|
auth_file: Path
|
|
31
31
|
state_directory: Path
|
|
32
32
|
enable_writes: bool
|
|
33
|
+
account_reads: bool = True
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class LiveRunner:
|
|
@@ -405,6 +406,12 @@ class LiveRunner:
|
|
|
405
406
|
for operation in self.contract.operations.values():
|
|
406
407
|
if operation.write or operation.identifier in self.completed:
|
|
407
408
|
continue
|
|
409
|
+
if not self.options.account_reads and operation.identifier in {
|
|
410
|
+
"getPlans",
|
|
411
|
+
"getUser",
|
|
412
|
+
}:
|
|
413
|
+
self.gaps.append(operation.identifier + ": account-wide read excluded")
|
|
414
|
+
continue
|
|
408
415
|
required = {
|
|
409
416
|
str(param["name"])
|
|
410
417
|
for param in self.contract.parameters(operation)
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Network-boundary tests for CI confinement, redaction, and credential cleanup."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import tempfile
|
|
9
|
+
from enum import StrEnum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from uuid import UUID
|
|
12
|
+
|
|
13
|
+
import httpx
|
|
14
|
+
import pytest
|
|
15
|
+
from tests.support import Server
|
|
16
|
+
|
|
17
|
+
from ynab_api_cli.ci_live import Application
|
|
18
|
+
from ynab_api_cli.contract import Contract
|
|
19
|
+
from ynab_api_cli.exceptions import AppError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Failure(StrEnum):
|
|
23
|
+
"""Adversarial API outcomes that must not disclose request secrets."""
|
|
24
|
+
|
|
25
|
+
NONE = "none"
|
|
26
|
+
STATUS = "status"
|
|
27
|
+
MALFORMED = "malformed"
|
|
28
|
+
NETWORK = "network"
|
|
29
|
+
UNEXPECTED = "unexpected"
|
|
30
|
+
PLAN_NAME = "plan-name"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class LiveServer:
|
|
34
|
+
"""Serve reviewed response schemas at an observable HTTP boundary."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, contract: Contract, *, failure: Failure = Failure.NONE) -> None:
|
|
37
|
+
self._contract = contract
|
|
38
|
+
self._failure = failure
|
|
39
|
+
self.requests: list[httpx.Request] = []
|
|
40
|
+
|
|
41
|
+
def handle(self, request: httpx.Request) -> httpx.Response:
|
|
42
|
+
"""Return representative responses or deliberately secret-bearing failures."""
|
|
43
|
+
self.requests.append(request)
|
|
44
|
+
secret = request.headers["Authorization"]
|
|
45
|
+
if self._failure == Failure.STATUS:
|
|
46
|
+
return httpx.Response(502, json={"detail": secret})
|
|
47
|
+
if self._failure == Failure.MALFORMED:
|
|
48
|
+
return httpx.Response(200, text=secret)
|
|
49
|
+
if self._failure == Failure.NETWORK:
|
|
50
|
+
raise httpx.ReadTimeout(secret)
|
|
51
|
+
if self._failure == Failure.UNEXPECTED:
|
|
52
|
+
raise ValueError(secret)
|
|
53
|
+
path = request.url.path.removeprefix("/v1")
|
|
54
|
+
operation = next(
|
|
55
|
+
item
|
|
56
|
+
for item in self._contract.operations.values()
|
|
57
|
+
if item.method == request.method
|
|
58
|
+
and re.fullmatch(re.sub(r"\{[^}]+\}", "[^/]+", item.path), path)
|
|
59
|
+
)
|
|
60
|
+
response = Server(self._contract, operation.identifier).handle(request)
|
|
61
|
+
if operation.identifier != "getPlanById":
|
|
62
|
+
return response
|
|
63
|
+
data = response.json()
|
|
64
|
+
data["data"]["plan"]["name"] = (
|
|
65
|
+
"ordinary plan"
|
|
66
|
+
if self._failure == Failure.PLAN_NAME
|
|
67
|
+
else "YNAB API CLI TEST synthetic"
|
|
68
|
+
)
|
|
69
|
+
return httpx.Response(response.status_code, json=data)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class TestApplication:
|
|
73
|
+
"""Exercise real orchestration while replacing only the HTTP boundary."""
|
|
74
|
+
|
|
75
|
+
CREDENTIAL = "synthetic-ci-secret-do-not-log"
|
|
76
|
+
PLAN = str(UUID(int=123))
|
|
77
|
+
|
|
78
|
+
def environment(self, monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
79
|
+
"""Supply fake step secrets and confine temporary files to the test."""
|
|
80
|
+
monkeypatch.setenv("YNAB_ACCESS_TOKEN", self.CREDENTIAL)
|
|
81
|
+
monkeypatch.setenv("YNAB_TEST_PLAN_ID", self.PLAN)
|
|
82
|
+
monkeypatch.setattr(tempfile, "tempdir", str(tmp_path))
|
|
83
|
+
|
|
84
|
+
def test_read_only_scope_and_summary(
|
|
85
|
+
self,
|
|
86
|
+
contract: Contract,
|
|
87
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
88
|
+
tmp_path: Path,
|
|
89
|
+
capsys: pytest.CaptureFixture[str],
|
|
90
|
+
) -> None:
|
|
91
|
+
"""CI submits only selected-plan GETs and exports no private record data."""
|
|
92
|
+
self.environment(monkeypatch, tmp_path)
|
|
93
|
+
server = LiveServer(contract)
|
|
94
|
+
with httpx.Client(transport=httpx.MockTransport(server.handle)) as client:
|
|
95
|
+
Application.main(client)
|
|
96
|
+
|
|
97
|
+
output = capsys.readouterr().out
|
|
98
|
+
report = json.loads(output)
|
|
99
|
+
assert report["ok"] is True
|
|
100
|
+
assert report["mode"] == "read-only"
|
|
101
|
+
assert report["complete_live_coverage"] is False
|
|
102
|
+
assert "getPlanById" in report["completed_operations"]
|
|
103
|
+
assert "getPlans" not in report["completed_operations"]
|
|
104
|
+
assert "getUser" not in report["completed_operations"]
|
|
105
|
+
assert all(request.method == "GET" for request in server.requests)
|
|
106
|
+
assert all(
|
|
107
|
+
request.url.path.startswith("/v1/plans/" + self.PLAN)
|
|
108
|
+
for request in server.requests
|
|
109
|
+
)
|
|
110
|
+
assert self.CREDENTIAL not in output
|
|
111
|
+
assert self.PLAN not in output
|
|
112
|
+
assert "YNAB_ACCESS_TOKEN" not in os.environ
|
|
113
|
+
assert "YNAB_TEST_PLAN_ID" not in os.environ
|
|
114
|
+
assert not list(tmp_path.rglob("auth.json"))
|
|
115
|
+
assert not list(tmp_path.iterdir())
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize(
|
|
118
|
+
"failure", [item for item in Failure if item != Failure.NONE]
|
|
119
|
+
)
|
|
120
|
+
def test_failure_redaction_and_cleanup(
|
|
121
|
+
self,
|
|
122
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
123
|
+
tmp_path: Path,
|
|
124
|
+
capsys: pytest.CaptureFixture[str],
|
|
125
|
+
caplog: pytest.LogCaptureFixture,
|
|
126
|
+
failure: Failure,
|
|
127
|
+
) -> None:
|
|
128
|
+
"""API payloads, nested exception details, and credentials stay out of logs."""
|
|
129
|
+
self.environment(monkeypatch, tmp_path)
|
|
130
|
+
server = LiveServer(Contract(), failure=failure)
|
|
131
|
+
with (
|
|
132
|
+
httpx.Client(transport=httpx.MockTransport(server.handle)) as client,
|
|
133
|
+
pytest.raises(SystemExit) as caught,
|
|
134
|
+
):
|
|
135
|
+
Application.main(client)
|
|
136
|
+
|
|
137
|
+
captured = capsys.readouterr()
|
|
138
|
+
assert caught.value.code == 1
|
|
139
|
+
assert self.CREDENTIAL not in captured.out + captured.err + caplog.text
|
|
140
|
+
assert self.PLAN not in captured.out + captured.err + caplog.text
|
|
141
|
+
assert captured.out == ""
|
|
142
|
+
assert "Live CI probe failed" in caplog.text
|
|
143
|
+
assert not list(tmp_path.iterdir())
|
|
144
|
+
|
|
145
|
+
@pytest.mark.parametrize(
|
|
146
|
+
("token", "plan"),
|
|
147
|
+
[
|
|
148
|
+
("", PLAN),
|
|
149
|
+
("contains whitespace", PLAN),
|
|
150
|
+
(CREDENTIAL, ""),
|
|
151
|
+
(CREDENTIAL, "default"),
|
|
152
|
+
],
|
|
153
|
+
)
|
|
154
|
+
def test_invalid_settings_prevent_network(
|
|
155
|
+
self, contract: Contract, token: str, plan: str
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Missing secrets, invalid headers, and implicit plan aliases fail locally."""
|
|
158
|
+
environment = {"YNAB_ACCESS_TOKEN": token, "YNAB_TEST_PLAN_ID": plan}
|
|
159
|
+
server = LiveServer(contract)
|
|
160
|
+
with (
|
|
161
|
+
httpx.Client(transport=httpx.MockTransport(server.handle)) as client,
|
|
162
|
+
pytest.raises((ValueError, AppError)),
|
|
163
|
+
):
|
|
164
|
+
Application.run(environment, client)
|
|
165
|
+
assert environment == {}
|
|
166
|
+
assert server.requests == []
|
|
@@ -4,6 +4,9 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import io
|
|
6
6
|
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
7
10
|
from pathlib import Path
|
|
8
11
|
from unittest.mock import Mock
|
|
9
12
|
|
|
@@ -22,6 +25,28 @@ from ynab_api_cli.transport import Transport
|
|
|
22
25
|
class TestApplication:
|
|
23
26
|
"""Exercise the public process behavior with isolated settings and HTTP."""
|
|
24
27
|
|
|
28
|
+
def test_utf8_redirected_output(self, tmp_path: Path) -> None:
|
|
29
|
+
"""A real CLI subprocess emits Unicode JSON through a legacy-encoded pipe."""
|
|
30
|
+
xdg = tmp_path / "home-\U0001f3e0"
|
|
31
|
+
store = ConfigStore(xdg / "ynab-api-cli")
|
|
32
|
+
store.configure("synthetic-subprocess-credential")
|
|
33
|
+
environment = dict(os.environ)
|
|
34
|
+
environment.update({"XDG_CONFIG_HOME": str(xdg), "PYTHONIOENCODING": "cp1252"})
|
|
35
|
+
|
|
36
|
+
result = subprocess.run(
|
|
37
|
+
[sys.executable, "-m", "ynab_api_cli", "doctor", "--offline"],
|
|
38
|
+
cwd=tmp_path,
|
|
39
|
+
env=environment,
|
|
40
|
+
capture_output=True,
|
|
41
|
+
check=False,
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
assert result.returncode == 0
|
|
45
|
+
report = json.loads(result.stdout.decode("utf-8"))
|
|
46
|
+
assert report["ok"] is True
|
|
47
|
+
assert report["api_checked"] is False
|
|
48
|
+
assert report["directory"] == str(store.root)
|
|
49
|
+
|
|
25
50
|
@pytest.mark.parametrize(
|
|
26
51
|
"argv",
|
|
27
52
|
[
|