dinary 1.2.2__tar.gz → 1.2.3__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.
- {dinary-1.2.2 → dinary-1.2.3}/PKG-INFO +1 -1
- dinary-1.2.3/src/dinary/__about__.py +1 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/__init__.py +2 -2
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/import_tasks.py +8 -8
- dinary-1.2.2/tasks/imports/income_original_export.py → dinary-1.2.3/tasks/imports/income_extract.py +56 -42
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/income_import.py +32 -0
- dinary-1.2.3/tests/imports/test_income_extract.py +330 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ProviderCard.vue +16 -3
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ProviderSheet.vue +25 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-provider-card.test.js +17 -1
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-provider-sheet.test.js +20 -0
- dinary-1.2.2/src/dinary/__about__.py +0 -1
- {dinary-1.2.2 → dinary-1.2.3}/.claudeignore +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.coveragerc +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.deploy.example/.env +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.deploy.example/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.deploy.example/import_sources.json +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.deploy.example/llm_providers.toml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.github/workflows/ci.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.github/workflows/docs.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.github/workflows/pip_publish.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.github/workflows/static.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.gitignore +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/.pre-commit-config.yaml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/AGENTS.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/CLAUDE.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/Dockerfile +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/LICENSE +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/activate.sh +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docker-compose.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/mkdocs.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/common/images/about.jpg +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/common/reference.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/cloudflare-setup.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/deploy-oracle.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/deploy-selfhost.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/development.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/google-sheets-setup.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/index.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/installation.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/operations.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/en/pwa-install.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/cloudflare-setup.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/deploy-oracle.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/deploy-selfhost.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/development.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/google-sheets-setup.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/index.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/installation.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/operations.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/pwa-install.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/docs/src/ru/taxonomy.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/invoke.yml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/pyproject.toml +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/pytest.ini +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/scripts/verup.sh +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/plans/analytics.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/architecture.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/catalog-api.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/classification-pipeline.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/currencies.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/frontend-cache.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/income-import.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/llm-providers.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/pwa-offline.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/receipt-fetching.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/sheets.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/sql-tool.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/stores.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/reference/timestamps.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/ui/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/ui/components.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/ui/design-language.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/ui/future-screens-guide.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/ui/patterns.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/specs/ui/screens.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/__init__.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/exchange_rates.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/llm_storage.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/llmbroker.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/nbp.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/nbs.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/rate_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/serbian_receipt_parser.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/adapters/sheets_client.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/catalog.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/catalog.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/catalog_writer.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/catalog_writer_categories.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/catalog_writer_errors.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/catalog_writer_events.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/catalog_writer_groups.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/expense_corrections.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/expenses.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/llm.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/qr_parser.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/controllers/rules.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/currencies.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/expense_corrections.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/expenses.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/llm.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/qr.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/receipts.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/api/rules.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/classification/item_normalizer.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/classification/persist.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/classification/receipt_classifier.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/classification/store_resolver.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/classification/task.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/rate_prefetch/task.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/sheet_logging/income_sheet_logging.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/sheet_logging/logging_jobs.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/sheet_logging/sheet_logging.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/sheet_logging/sheets_write.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/background/sheet_logging/task.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/config.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/catalog.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/classification_rules.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/currencies.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/db_migrations.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/expenses.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0001_initial_schema.rollback.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0001_initial_schema.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0002_exchange_rates_source_target.rollback.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0002_exchange_rates_source_target.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0003_app_currencies.rollback.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0003_app_currencies.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0004_receipt_pipeline.rollback.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0004_receipt_pipeline.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0005_income_logging.rollback.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/0005_income_logging.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/migrations/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/receipts.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/__init__.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/get_category_by_name.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/get_existing_expense.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/get_month_expenses.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/insert_expense.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/insert_income.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/list_categories.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/list_incomes.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/logging_projection.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/resolve_mapping_for_year.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql/seed_load_categories.sql +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/sql_loader.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/db/storage.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/main.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/sheets/sheet_mapping.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/src/dinary/sheets/sheets.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/backup_retention.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/backup_snapshots.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/backups_replica.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/backups_restore.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/backups_status.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/backups_yandex.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/backups/restore_utils.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/db.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/deploy.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/devtools/build_docs.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/devtools/constants.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/devtools/dev.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/devtools/env.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/healthcheck.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/README.md +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/expense_import.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/report_2d_3d.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/seed.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/seed_config.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/seed_derivation.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/verify_equivalence.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/imports/verify_income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/receipt.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/reports/expenses.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/reports/income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/reports/report_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/reports/report_tasks.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/reports/verify_budget.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/reports/verify_income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/server.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/setup.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/sql.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tasks/ssh_utils.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/_admin_catalog_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/_api_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_admin_catalog_add.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_admin_catalog_delete.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_admin_catalog_meta.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_admin_catalog_patch.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_admin_llm.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_catalog.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_concurrency.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_conflict.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_currencies.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_delete_expense.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_delete_receipt.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_expenses_recent_patch.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_get_expenses.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_post_expense.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_receipts.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_rules_approve.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_api_validation.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_receipt_pipeline_e2e.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_receipt_review.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/api/test_review_page_ux.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/conftest.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/currency/_currency_rates_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/currency/test_currency_rates_misc.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/currency/test_currency_rates_nbp.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/currency/test_currency_rates_resolve.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/currency/test_rate_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/currency/test_rate_prefetch_task.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/imports/test_expense_import.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/imports/test_seed_config.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/_catalog_writer_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/_ledger_repo_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_catalog_writer_invariants.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_catalog_writer_patch.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_income_db.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_ledger_repo_catalog.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_ledger_repo_expenses_insert.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_ledger_repo_expenses_lookup.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_ledger_repo_expenses_race.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_ledger_repo_jobs.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_ledger_repo_logging_projection.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/ledger/test_migrations.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/_report_2d_3d_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_report_2d_3d.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_report_2d_3d_aggregate.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_report_2d_3d_render.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_report_2d_3d_resolve.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_reports_expenses.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_reports_income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_reports_verify_budget.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/reports/test_reports_verify_income.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_classification_rules.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_item_normalizer.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_llm_storage.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_llmbroker.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_qr_parser.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_receipt_classification.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_receipt_classifier.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_receipt_parser.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_sql_loader.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/services/test_store_resolver.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/_sheet_logging_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/_sheet_mapping_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/_sheets_helpers.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_income_drain.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_logging.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_logging_derive.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_logging_drain.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_logging_drain_one.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_mapping_parse.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_mapping_reload.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheet_mapping_resolve.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheets_read.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/sheets/test_sheets_rows.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_receipt_drain.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_receipt_pipeline.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_reclassify_receipts.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_backups_restore.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_backups_retention.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_backups_status.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_backups_yandex_setup.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_db.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_deploy.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_dev.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_imports.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_reports.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_restore_utils.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_server.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_server_receipt.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_setup_replica.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_ssh_utils.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tasks_ssh_utils_scripts.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/tasks/test_tools_sql.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/test_config.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/test_main.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/tests/test_webapp_api_contract.py +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/uv.lock +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/index.html +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/package-lock.json +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/package.json +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/apple-touch-icon-precomposed.png +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/apple-touch-icon.png +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/favicon.ico +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/icons/icon-180.png +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/icons/icon-192.png +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/icons/icon-512.png +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/public/manifest.json +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/App.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/_request.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/adminLlm.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/catalog.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/currencies.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/expenseCorrections.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/expenses.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/income.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/receipts.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/api/review.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/assets/base.css +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/BaseModal.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/BaseSheet.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/CatalogSelectField.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/CategoryQuickPicks.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/CategorySheet.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ConfirmDeleteSheet.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/CorrectionSheet.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/CurrencyAmountRow.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/CurrencyPicker.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ExpenseEditSheet.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ExpenseForm.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ExpenseRow.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/HeaderSegmented.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/HealthSummaryCard.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/IconBtn.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/IncomeEditSheet.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/IncomeForm.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/IncomeRow.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/InlineCreateEvent.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/InlineCreateRow.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/KeyboardSaveBar.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ManageList.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/QrScanner.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/QueueModal.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ReceiptCascadeCard.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/RuleRow.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/ScopeSelector.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/StatusDot.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/components/TagPicker.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/addResult.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/catalogManage.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/flushQueue.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/flushReceiptQueue.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/receipt.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/useExpenseDeleteFlow.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/useKeyboardVisible.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/useOnline.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/useStaleCache.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/useSwipeRow.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/composables/zbar.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/legacy-pwa-cleanup.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/main.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/modals/EditModal.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/catalog.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/currency.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/frequentCategories.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/income.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/llm.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/queue.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/receiptQueue.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/review.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/stores/toast.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/views/AddView.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/views/IncomeView.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/views/LLMView.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/src/views/ReviewView.vue +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/BaseSheet.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/CategoryQuickPicks.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/CategorySheet.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/CurrencyAmountRow.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/ExpenseEditSheet.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/ExpenseRow.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/ReceiptCascadeCard.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/ScopeSelector.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-adminLlm.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-catalog.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-currencies.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-expenses.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-income.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-receipts.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-request.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/api-review.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-app.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-catalog-select-field.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-correction-sheet.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-currency-picker.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-edit-modal.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-expense-edit-sheet.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-expense-form.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-header-segmented.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-health-summary-card.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-inline-create-event.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-inline-create-row.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-manage-list.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-queue-modal.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-review-view.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-rule-row.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-status-dot.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/component-tag-picker.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-add-result.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-catalog-manage.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-flush-queue.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-flush-receipt-queue.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-keyboard-visible.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-receipt.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-stale-cache.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/composable-use-online.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/frequentCategories.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/legacy-pwa-cleanup.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/setup.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-catalog.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-currency.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-income.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-llm.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-queue.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-receipt-queue-durability.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-receipt-queue.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-review.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/store-toast.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/useExpenseDeleteFlow.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/tests/useSwipeRow.test.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/vite.config.js +0 -0
- {dinary-1.2.2 → dinary-1.2.3}/webapp/vitest.config.js +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dinary
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Server for [Dinary - your dinar diary](https://github.com/andgineer/dinary). Track expenses, scan receipts, analyze spending with AI
|
|
5
5
|
Project-URL: Homepage, https://andgineer.github.io/dinary/
|
|
6
6
|
Project-URL: Documentation, https://andgineer.github.io/dinary/
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.2.3"
|
|
@@ -20,10 +20,10 @@ from .devtools.constants import ALLOWED_VERSION_TYPES
|
|
|
20
20
|
from .devtools.dev import build_static, dev, pre, reqs, test, uv, ver_task_factory, version
|
|
21
21
|
from .healthcheck import healthcheck
|
|
22
22
|
from .imports.import_tasks import (
|
|
23
|
-
export_income_original,
|
|
24
23
|
import_budget,
|
|
25
24
|
import_budget_all,
|
|
26
25
|
import_catalog,
|
|
26
|
+
import_extract_income,
|
|
27
27
|
import_income,
|
|
28
28
|
import_income_all,
|
|
29
29
|
import_report_2d_3d,
|
|
@@ -48,11 +48,11 @@ __all__ = [
|
|
|
48
48
|
"dev",
|
|
49
49
|
"docs_task_factory",
|
|
50
50
|
"healthcheck",
|
|
51
|
-
"export_income_original",
|
|
52
51
|
"import_budget",
|
|
53
52
|
"import_budget_all",
|
|
54
53
|
"import_catalog",
|
|
55
54
|
"import_config",
|
|
55
|
+
"import_extract_income",
|
|
56
56
|
"import_income",
|
|
57
57
|
"import_income_all",
|
|
58
58
|
"import_report_2d_3d",
|
|
@@ -7,7 +7,7 @@ from datetime import datetime as _dt
|
|
|
7
7
|
|
|
8
8
|
from invoke import task
|
|
9
9
|
|
|
10
|
-
from tasks.imports import
|
|
10
|
+
from tasks.imports import income_extract
|
|
11
11
|
from tasks.imports import report_2d_3d as _report_2d_3d_module
|
|
12
12
|
from tasks.reports import verify_budget, verify_income
|
|
13
13
|
from tasks.ssh_utils import remote_snapshot_cmd, ssh_capture_bytes, ssh_json, ssh_run
|
|
@@ -250,16 +250,16 @@ def verify_income_equivalence_all(c, json=False): # noqa: A002
|
|
|
250
250
|
sys.exit(verify_income.exit_code_for_batch(results))
|
|
251
251
|
|
|
252
252
|
|
|
253
|
-
@task(name="
|
|
254
|
-
def
|
|
255
|
-
"""
|
|
253
|
+
@task(name="import-extract-income")
|
|
254
|
+
def import_extract_income(c, output=""):
|
|
255
|
+
"""Extract individual income records from all sheets in original currency to JSON.
|
|
256
256
|
|
|
257
257
|
Reads every registered income source from .deploy/import_sources.json and
|
|
258
|
-
writes
|
|
259
|
-
Default output path: data/
|
|
258
|
+
writes one entry per sheet row with predicted income_month.
|
|
259
|
+
Default output path: data/income_extract.json. Override with --output.
|
|
260
260
|
"""
|
|
261
|
-
dest = Path(output) if output else
|
|
262
|
-
count =
|
|
261
|
+
dest = Path(output) if output else income_extract.DEFAULT_OUTPUT
|
|
262
|
+
count = income_extract.export_to_file(dest)
|
|
263
263
|
print(f"Wrote {count} entries to {dest}")
|
|
264
264
|
|
|
265
265
|
|
dinary-1.2.2/tasks/imports/income_original_export.py → dinary-1.2.3/tasks/imports/income_extract.py
RENAMED
|
@@ -1,33 +1,36 @@
|
|
|
1
|
-
"""Extract
|
|
1
|
+
"""Extract individual income records from Google Sheets in original currency.
|
|
2
2
|
|
|
3
3
|
Reads every registered income source from ``.deploy/import_sources.json``
|
|
4
|
-
and
|
|
5
|
-
denominated in — RUB for years up to mid-2022, RSD from mid-2022 onward
|
|
6
|
-
(exact boundary is the ``transition_month`` in each year's ``IncomeLayout``).
|
|
4
|
+
and returns one entry per sheet row. Amounts are NOT summed by month.
|
|
7
5
|
|
|
8
|
-
|
|
6
|
+
Each entry carries the raw payment date and a predicted ``income_month``:
|
|
7
|
+
- day <= 25 -> previous month (salary paid at start of month for prior month)
|
|
8
|
+
- day > 25 -> current month (late payment belonging to the same month)
|
|
9
|
+
|
|
10
|
+
Output JSON:
|
|
9
11
|
|
|
10
12
|
.. code-block:: json
|
|
11
13
|
|
|
12
14
|
{
|
|
13
15
|
"generated_at": "2026-05-31",
|
|
14
16
|
"entries": [
|
|
15
|
-
{
|
|
16
|
-
|
|
17
|
+
{
|
|
18
|
+
"year": 2019, "month": 1, "day": 10,
|
|
19
|
+
"amount": "85000.00", "currency": "RUB",
|
|
20
|
+
"income_year": 2018, "income_month": 12
|
|
21
|
+
}
|
|
17
22
|
]
|
|
18
23
|
}
|
|
19
24
|
|
|
20
|
-
``inv
|
|
21
|
-
|
|
25
|
+
``inv import-extract-income`` is the operator entry point.
|
|
26
|
+
Can also be run with ``uv run python -m tasks.imports.income_extract``.
|
|
22
27
|
"""
|
|
23
28
|
|
|
24
29
|
import argparse
|
|
25
30
|
import json
|
|
26
31
|
import logging
|
|
27
32
|
import sys
|
|
28
|
-
from collections import defaultdict
|
|
29
33
|
from datetime import date
|
|
30
|
-
from decimal import Decimal
|
|
31
34
|
from pathlib import Path
|
|
32
35
|
|
|
33
36
|
from dinary.adapters.sheets_client import get_sheet
|
|
@@ -38,38 +41,49 @@ from tasks.imports.income_import import (
|
|
|
38
41
|
IncomeLayout,
|
|
39
42
|
_cell,
|
|
40
43
|
_parse_amount,
|
|
41
|
-
|
|
44
|
+
_parse_full_date,
|
|
42
45
|
)
|
|
43
46
|
|
|
44
47
|
logger = logging.getLogger(__name__)
|
|
45
48
|
|
|
46
|
-
DEFAULT_OUTPUT = Path("data/
|
|
49
|
+
DEFAULT_OUTPUT = Path("data/income_extract.json")
|
|
50
|
+
|
|
51
|
+
INCOME_MONTH_LATE_CUTOFF = 25
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _predict_income_month(year: int, month: int, day: int) -> tuple[int, int]:
|
|
55
|
+
"""Return (income_year, income_month) for a payment on the given date.
|
|
56
|
+
|
|
57
|
+
day <= 25 -> previous month; day > 25 -> current month.
|
|
58
|
+
"""
|
|
59
|
+
if day > INCOME_MONTH_LATE_CUTOFF:
|
|
60
|
+
return year, month
|
|
61
|
+
return (year - 1, 12) if month == 1 else (year, month - 1)
|
|
47
62
|
|
|
48
63
|
|
|
49
|
-
def
|
|
64
|
+
def extract_income_records(
|
|
50
65
|
year: int,
|
|
51
66
|
spreadsheet_id: str,
|
|
52
67
|
worksheet_name: str,
|
|
53
68
|
layout: IncomeLayout,
|
|
54
|
-
) -> dict
|
|
55
|
-
"""Read one Income/Balance worksheet; return
|
|
69
|
+
) -> list[dict]:
|
|
70
|
+
"""Read one Income/Balance worksheet; return one dict per income row.
|
|
56
71
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
``transition_currency`` from that month onward).
|
|
72
|
+
Each row is a separate entry with raw payment date and predicted
|
|
73
|
+
income_year/income_month. No aggregation.
|
|
60
74
|
"""
|
|
61
75
|
ss = get_sheet(spreadsheet_id)
|
|
62
76
|
ws = ss.worksheet(worksheet_name)
|
|
63
77
|
all_values = ws.get_all_values()
|
|
64
78
|
|
|
65
|
-
|
|
79
|
+
records: list[dict] = []
|
|
66
80
|
|
|
67
81
|
for row_idx in range(layout.header_rows, len(all_values)):
|
|
68
82
|
row = all_values[row_idx]
|
|
69
|
-
parsed =
|
|
83
|
+
parsed = _parse_full_date(_cell(row, layout.col_date))
|
|
70
84
|
if parsed is None:
|
|
71
85
|
continue
|
|
72
|
-
row_year, month = parsed
|
|
86
|
+
row_year, month, day = parsed
|
|
73
87
|
if row_year != year:
|
|
74
88
|
continue
|
|
75
89
|
if not 1 <= month <= MONTHS_IN_YEAR:
|
|
@@ -87,18 +101,24 @@ def aggregate_original_currency(
|
|
|
87
101
|
):
|
|
88
102
|
currency = layout.transition_currency
|
|
89
103
|
|
|
90
|
-
|
|
104
|
+
income_year, income_month = _predict_income_month(year, month, day)
|
|
105
|
+
records.append(
|
|
106
|
+
{
|
|
107
|
+
"year": year,
|
|
108
|
+
"month": month,
|
|
109
|
+
"day": day,
|
|
110
|
+
"amount": format(amount, "f"),
|
|
111
|
+
"currency": currency,
|
|
112
|
+
"income_year": income_year,
|
|
113
|
+
"income_month": income_month,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
91
116
|
|
|
92
|
-
return
|
|
117
|
+
return records
|
|
93
118
|
|
|
94
119
|
|
|
95
120
|
def extract_all_years() -> list[dict]:
|
|
96
|
-
"""Return a flat list of
|
|
97
|
-
|
|
98
|
-
Iterates every import source that has an income worksheet. Years with
|
|
99
|
-
no registered source or no income worksheet are silently skipped.
|
|
100
|
-
Amounts are ``str`` (canonical decimal notation) to avoid float rounding.
|
|
101
|
-
"""
|
|
121
|
+
"""Return a flat list of individual income records across all registered years."""
|
|
102
122
|
sources = read_import_sources()
|
|
103
123
|
entries: list[dict] = []
|
|
104
124
|
|
|
@@ -122,7 +142,7 @@ def extract_all_years() -> list[dict]:
|
|
|
122
142
|
)
|
|
123
143
|
|
|
124
144
|
try:
|
|
125
|
-
|
|
145
|
+
records = extract_income_records(
|
|
126
146
|
source.year,
|
|
127
147
|
source.spreadsheet_id,
|
|
128
148
|
source.income_worksheet_name,
|
|
@@ -132,21 +152,13 @@ def extract_all_years() -> list[dict]:
|
|
|
132
152
|
logger.exception("Failed to read year %d — skipping", source.year)
|
|
133
153
|
continue
|
|
134
154
|
|
|
135
|
-
|
|
136
|
-
entries.append(
|
|
137
|
-
{
|
|
138
|
-
"year": source.year,
|
|
139
|
-
"month": month,
|
|
140
|
-
"amount": format(total, "f"),
|
|
141
|
-
"currency": currency,
|
|
142
|
-
},
|
|
143
|
-
)
|
|
155
|
+
entries.extend(sorted(records, key=lambda r: (r["month"], r["day"])))
|
|
144
156
|
|
|
145
157
|
return entries
|
|
146
158
|
|
|
147
159
|
|
|
148
160
|
def export_to_file(output: Path) -> int:
|
|
149
|
-
"""Extract all income
|
|
161
|
+
"""Extract all income records and write to ``output``.
|
|
150
162
|
|
|
151
163
|
Returns the number of entries written. Raises on I/O errors.
|
|
152
164
|
"""
|
|
@@ -162,7 +174,9 @@ def export_to_file(output: Path) -> int:
|
|
|
162
174
|
|
|
163
175
|
def main(argv: list[str] | None = None) -> int:
|
|
164
176
|
parser = argparse.ArgumentParser(
|
|
165
|
-
description=
|
|
177
|
+
description=(
|
|
178
|
+
"Extract individual income records from all sheets in original currency to JSON."
|
|
179
|
+
),
|
|
166
180
|
)
|
|
167
181
|
parser.add_argument(
|
|
168
182
|
"--output",
|
|
@@ -106,6 +106,38 @@ def _parse_date(raw: str) -> tuple[int, int] | None:
|
|
|
106
106
|
return None
|
|
107
107
|
|
|
108
108
|
|
|
109
|
+
def _parse_full_date(raw: str) -> tuple[int, int, int] | None:
|
|
110
|
+
"""Extract (year, month, day) from a date string. Returns None if unparseable."""
|
|
111
|
+
if not raw:
|
|
112
|
+
return None
|
|
113
|
+
m = _LOOSE_DATE_RE.match(raw)
|
|
114
|
+
if m:
|
|
115
|
+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
|
116
|
+
for fmt in _DATE_FORMATS:
|
|
117
|
+
try:
|
|
118
|
+
dt = datetime.strptime(raw, fmt) # noqa: DTZ007
|
|
119
|
+
return dt.year, dt.month, dt.day
|
|
120
|
+
except ValueError:
|
|
121
|
+
continue
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _parse_full_date(raw: str) -> tuple[int, int, int] | None:
|
|
126
|
+
"""Extract (year, month, day) from a date string. Returns None if unparseable."""
|
|
127
|
+
if not raw:
|
|
128
|
+
return None
|
|
129
|
+
m = _LOOSE_DATE_RE.match(raw)
|
|
130
|
+
if m:
|
|
131
|
+
return int(m.group(1)), int(m.group(2)), int(m.group(3))
|
|
132
|
+
for fmt in _DATE_FORMATS:
|
|
133
|
+
try:
|
|
134
|
+
dt = datetime.strptime(raw, fmt) # noqa: DTZ007
|
|
135
|
+
return dt.year, dt.month, dt.day
|
|
136
|
+
except ValueError:
|
|
137
|
+
continue
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
|
|
109
141
|
def _convert_to_accounting_from_cache(
|
|
110
142
|
amount: Decimal,
|
|
111
143
|
currency: str,
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Tests for income_extract: individual-record extraction from sheets."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from unittest.mock import MagicMock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
import tasks.imports.income_extract as _mod
|
|
9
|
+
from dinary.config import ImportSourceRow
|
|
10
|
+
from tasks.imports.income_extract import (
|
|
11
|
+
_predict_income_month,
|
|
12
|
+
export_to_file,
|
|
13
|
+
extract_all_years,
|
|
14
|
+
extract_income_records,
|
|
15
|
+
)
|
|
16
|
+
from tasks.imports.income_import import INCOME_LAYOUTS
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Helpers
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _ws(rows: list[list[str]]) -> MagicMock:
|
|
25
|
+
ws = MagicMock()
|
|
26
|
+
ws.get_all_values.return_value = rows
|
|
27
|
+
return ws
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _ss(ws: MagicMock) -> MagicMock:
|
|
31
|
+
ss = MagicMock()
|
|
32
|
+
ss.worksheet.return_value = ws
|
|
33
|
+
return ss
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
# _predict_income_month
|
|
38
|
+
# ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestPredictIncomeMonth:
|
|
42
|
+
def test_day_25_is_prev_month(self):
|
|
43
|
+
assert _predict_income_month(2024, 3, 25) == (2024, 2)
|
|
44
|
+
|
|
45
|
+
def test_day_1_is_prev_month(self):
|
|
46
|
+
assert _predict_income_month(2024, 5, 1) == (2024, 4)
|
|
47
|
+
|
|
48
|
+
def test_day_26_is_current_month(self):
|
|
49
|
+
assert _predict_income_month(2024, 3, 26) == (2024, 3)
|
|
50
|
+
|
|
51
|
+
def test_day_31_is_current_month(self):
|
|
52
|
+
assert _predict_income_month(2024, 1, 31) == (2024, 1)
|
|
53
|
+
|
|
54
|
+
def test_january_day_le_25_wraps_to_december_prev_year(self):
|
|
55
|
+
assert _predict_income_month(2024, 1, 10) == (2023, 12)
|
|
56
|
+
|
|
57
|
+
def test_january_day_26_stays_january(self):
|
|
58
|
+
assert _predict_income_month(2024, 1, 26) == (2024, 1)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# extract_income_records
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestExtractIncomeRecords:
|
|
67
|
+
def test_returns_separate_record_per_row(self):
|
|
68
|
+
layout = INCOME_LAYOUTS["balance_rub"]
|
|
69
|
+
rows = [
|
|
70
|
+
["date", "amount"],
|
|
71
|
+
["2019-01-15", "50000"],
|
|
72
|
+
["2019-01-20", "35000"],
|
|
73
|
+
]
|
|
74
|
+
ss = _ss(_ws(rows))
|
|
75
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
76
|
+
result = extract_income_records(2019, "sid", "Income", layout)
|
|
77
|
+
|
|
78
|
+
assert len(result) == 2
|
|
79
|
+
assert result[0]["amount"] == "50000"
|
|
80
|
+
assert result[1]["amount"] == "35000"
|
|
81
|
+
|
|
82
|
+
def test_record_fields(self):
|
|
83
|
+
layout = INCOME_LAYOUTS["balance_rub"]
|
|
84
|
+
rows = [["date", "amount"], ["2019-03-10", "90000"]]
|
|
85
|
+
ss = _ss(_ws(rows))
|
|
86
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
87
|
+
result = extract_income_records(2019, "sid", "Income", layout)
|
|
88
|
+
|
|
89
|
+
rec = result[0]
|
|
90
|
+
assert rec["year"] == 2019
|
|
91
|
+
assert rec["month"] == 3
|
|
92
|
+
assert rec["day"] == 10
|
|
93
|
+
assert rec["currency"] == "RUB"
|
|
94
|
+
assert rec["income_year"] == 2019
|
|
95
|
+
assert rec["income_month"] == 2 # day 10 <= 25 -> prev month
|
|
96
|
+
|
|
97
|
+
def test_day_26_maps_to_current_month(self):
|
|
98
|
+
layout = INCOME_LAYOUTS["balance_rsd"]
|
|
99
|
+
rows = [["date", "amount"], ["2023-05-26", "120000"]]
|
|
100
|
+
ss = _ss(_ws(rows))
|
|
101
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
102
|
+
result = extract_income_records(2023, "sid", "Income", layout)
|
|
103
|
+
|
|
104
|
+
assert result[0]["income_month"] == 5
|
|
105
|
+
assert result[0]["income_year"] == 2023
|
|
106
|
+
|
|
107
|
+
def test_transition_layout_currency(self):
|
|
108
|
+
layout = INCOME_LAYOUTS["balance_rub_rsd"]
|
|
109
|
+
rows = [
|
|
110
|
+
["date", "amount"],
|
|
111
|
+
["2022-07-10", "60000"],
|
|
112
|
+
["2022-08-10", "120000"],
|
|
113
|
+
]
|
|
114
|
+
ss = _ss(_ws(rows))
|
|
115
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
116
|
+
result = extract_income_records(2022, "sid", "Balance", layout)
|
|
117
|
+
|
|
118
|
+
assert result[0]["currency"] == "RUB"
|
|
119
|
+
assert result[1]["currency"] == "RSD"
|
|
120
|
+
|
|
121
|
+
def test_skips_rows_from_other_years(self):
|
|
122
|
+
layout = INCOME_LAYOUTS["balance_rub"]
|
|
123
|
+
rows = [
|
|
124
|
+
["date", "amount"],
|
|
125
|
+
["2019-06-01", "10000"],
|
|
126
|
+
["2020-06-01", "99999"],
|
|
127
|
+
]
|
|
128
|
+
ss = _ss(_ws(rows))
|
|
129
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
130
|
+
result = extract_income_records(2019, "sid", "Balance", layout)
|
|
131
|
+
|
|
132
|
+
assert len(result) == 1
|
|
133
|
+
assert result[0]["month"] == 6
|
|
134
|
+
|
|
135
|
+
def test_skips_blank_and_zero_amounts(self):
|
|
136
|
+
layout = INCOME_LAYOUTS["balance_rsd"]
|
|
137
|
+
rows = [
|
|
138
|
+
["date", "amount"],
|
|
139
|
+
["2023-03-01", ""],
|
|
140
|
+
["2023-03-05", "0"],
|
|
141
|
+
["2023-03-10", "75000"],
|
|
142
|
+
]
|
|
143
|
+
ss = _ss(_ws(rows))
|
|
144
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
145
|
+
result = extract_income_records(2023, "sid", "Income", layout)
|
|
146
|
+
|
|
147
|
+
assert len(result) == 1
|
|
148
|
+
assert result[0]["amount"] == "75000"
|
|
149
|
+
|
|
150
|
+
def test_skips_unparseable_dates(self):
|
|
151
|
+
layout = INCOME_LAYOUTS["balance_rsd"]
|
|
152
|
+
rows = [
|
|
153
|
+
["date", "amount"],
|
|
154
|
+
["not-a-date", "50000"],
|
|
155
|
+
["2023-04-01", "50000"],
|
|
156
|
+
]
|
|
157
|
+
ss = _ss(_ws(rows))
|
|
158
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
159
|
+
result = extract_income_records(2023, "sid", "Income", layout)
|
|
160
|
+
|
|
161
|
+
assert len(result) == 1
|
|
162
|
+
assert result[0]["month"] == 4
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# extract_all_years
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class TestExtractAllYears:
|
|
171
|
+
@pytest.fixture(autouse=True)
|
|
172
|
+
def _stub_sources(self, monkeypatch):
|
|
173
|
+
sources = [
|
|
174
|
+
ImportSourceRow(
|
|
175
|
+
year=2021,
|
|
176
|
+
spreadsheet_id="sid21",
|
|
177
|
+
worksheet_name="Budget",
|
|
178
|
+
income_worksheet_name="Balance",
|
|
179
|
+
income_layout_key="balance_rub",
|
|
180
|
+
),
|
|
181
|
+
ImportSourceRow(
|
|
182
|
+
year=2023,
|
|
183
|
+
spreadsheet_id="sid23",
|
|
184
|
+
worksheet_name="Budget",
|
|
185
|
+
income_worksheet_name="Income",
|
|
186
|
+
income_layout_key="balance_rsd",
|
|
187
|
+
),
|
|
188
|
+
ImportSourceRow(
|
|
189
|
+
year=2025,
|
|
190
|
+
spreadsheet_id="sid25",
|
|
191
|
+
worksheet_name="Budget",
|
|
192
|
+
income_worksheet_name="",
|
|
193
|
+
income_layout_key="",
|
|
194
|
+
),
|
|
195
|
+
]
|
|
196
|
+
monkeypatch.setattr(_mod, "read_import_sources", lambda: list(sources))
|
|
197
|
+
|
|
198
|
+
def _make_get_sheet(self, data_by_sid: dict[str, list[list[str]]]):
|
|
199
|
+
def _get_sheet(spreadsheet_id: str):
|
|
200
|
+
return _ss(_ws(data_by_sid[spreadsheet_id]))
|
|
201
|
+
|
|
202
|
+
return _get_sheet
|
|
203
|
+
|
|
204
|
+
def test_returns_entries_for_years_with_income(self):
|
|
205
|
+
data = {
|
|
206
|
+
"sid21": [["date", "amount"], ["2021-05-10", "90000"]],
|
|
207
|
+
"sid23": [["date", "amount"], ["2023-06-10", "150000"]],
|
|
208
|
+
}
|
|
209
|
+
with patch(
|
|
210
|
+
"tasks.imports.income_extract.get_sheet", side_effect=self._make_get_sheet(data)
|
|
211
|
+
):
|
|
212
|
+
entries = extract_all_years()
|
|
213
|
+
|
|
214
|
+
assert len(entries) == 2
|
|
215
|
+
years = {e["year"] for e in entries}
|
|
216
|
+
assert years == {2021, 2023}
|
|
217
|
+
|
|
218
|
+
def test_skips_year_without_income_worksheet(self):
|
|
219
|
+
data = {
|
|
220
|
+
"sid21": [["date", "amount"], ["2021-01-01", "50000"]],
|
|
221
|
+
"sid23": [["date", "amount"], ["2023-01-01", "100000"]],
|
|
222
|
+
}
|
|
223
|
+
with patch(
|
|
224
|
+
"tasks.imports.income_extract.get_sheet", side_effect=self._make_get_sheet(data)
|
|
225
|
+
):
|
|
226
|
+
entries = extract_all_years()
|
|
227
|
+
|
|
228
|
+
assert all(e["year"] != 2025 for e in entries)
|
|
229
|
+
|
|
230
|
+
def test_sheet_failure_skips_year_continues(self):
|
|
231
|
+
call_count = 0
|
|
232
|
+
|
|
233
|
+
def _get_sheet(spreadsheet_id: str):
|
|
234
|
+
nonlocal call_count
|
|
235
|
+
call_count += 1
|
|
236
|
+
if spreadsheet_id == "sid21":
|
|
237
|
+
raise OSError("network error")
|
|
238
|
+
return _ss(_ws([["date", "amount"], ["2023-03-01", "120000"]]))
|
|
239
|
+
|
|
240
|
+
with patch("tasks.imports.income_extract.get_sheet", side_effect=_get_sheet):
|
|
241
|
+
entries = extract_all_years()
|
|
242
|
+
|
|
243
|
+
assert call_count == 2
|
|
244
|
+
assert all(e["year"] != 2021 for e in entries)
|
|
245
|
+
assert any(e["year"] == 2023 for e in entries)
|
|
246
|
+
|
|
247
|
+
def test_entries_sorted_by_month_then_day_within_year(self):
|
|
248
|
+
data = {
|
|
249
|
+
"sid21": [
|
|
250
|
+
["date", "amount"],
|
|
251
|
+
["2021-03-15", "1000"],
|
|
252
|
+
["2021-01-20", "2000"],
|
|
253
|
+
["2021-01-05", "3000"],
|
|
254
|
+
],
|
|
255
|
+
"sid23": [["date", "amount"], ["2023-02-01", "4000"]],
|
|
256
|
+
}
|
|
257
|
+
with patch(
|
|
258
|
+
"tasks.imports.income_extract.get_sheet", side_effect=self._make_get_sheet(data)
|
|
259
|
+
):
|
|
260
|
+
entries = extract_all_years()
|
|
261
|
+
|
|
262
|
+
yr21 = [e for e in entries if e["year"] == 2021]
|
|
263
|
+
assert yr21[0]["month"] == 1
|
|
264
|
+
assert yr21[0]["day"] == 5
|
|
265
|
+
assert yr21[1]["month"] == 1
|
|
266
|
+
assert yr21[1]["day"] == 20
|
|
267
|
+
assert yr21[2]["month"] == 3
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
# ---------------------------------------------------------------------------
|
|
271
|
+
# export_to_file
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
class TestExportToFile:
|
|
276
|
+
def test_writes_valid_json_with_all_fields(self, tmp_path, monkeypatch):
|
|
277
|
+
sources = [
|
|
278
|
+
ImportSourceRow(
|
|
279
|
+
year=2024,
|
|
280
|
+
spreadsheet_id="sid24",
|
|
281
|
+
worksheet_name="Budget",
|
|
282
|
+
income_worksheet_name="Income",
|
|
283
|
+
income_layout_key="income_rsd",
|
|
284
|
+
),
|
|
285
|
+
]
|
|
286
|
+
monkeypatch.setattr(_mod, "read_import_sources", lambda: list(sources))
|
|
287
|
+
ws = _ws([["date", "amount"], ["2024-04-10", "300000"]])
|
|
288
|
+
ss = _ss(ws)
|
|
289
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
290
|
+
dest = tmp_path / "out.json"
|
|
291
|
+
count = export_to_file(dest)
|
|
292
|
+
|
|
293
|
+
assert count == 1
|
|
294
|
+
payload = json.loads(dest.read_text())
|
|
295
|
+
assert "generated_at" in payload
|
|
296
|
+
entry = payload["entries"][0]
|
|
297
|
+
assert entry["year"] == 2024
|
|
298
|
+
assert entry["month"] == 4
|
|
299
|
+
assert entry["day"] == 10
|
|
300
|
+
assert entry["currency"] == "RSD"
|
|
301
|
+
assert entry["income_year"] == 2024
|
|
302
|
+
assert entry["income_month"] == 3 # day 10 <= 25 -> prev month
|
|
303
|
+
|
|
304
|
+
def test_creates_parent_directory(self, tmp_path, monkeypatch):
|
|
305
|
+
monkeypatch.setattr(_mod, "read_import_sources", lambda: [])
|
|
306
|
+
dest = tmp_path / "nested" / "dir" / "income.json"
|
|
307
|
+
export_to_file(dest)
|
|
308
|
+
assert dest.exists()
|
|
309
|
+
|
|
310
|
+
def test_amount_is_decimal_string(self, tmp_path, monkeypatch):
|
|
311
|
+
sources = [
|
|
312
|
+
ImportSourceRow(
|
|
313
|
+
year=2023,
|
|
314
|
+
spreadsheet_id="sid23",
|
|
315
|
+
worksheet_name="Budget",
|
|
316
|
+
income_worksheet_name="Income",
|
|
317
|
+
income_layout_key="balance_rsd",
|
|
318
|
+
),
|
|
319
|
+
]
|
|
320
|
+
monkeypatch.setattr(_mod, "read_import_sources", lambda: list(sources))
|
|
321
|
+
ws = _ws([["date", "amount"], ["2023-01-10", "123456.78"]])
|
|
322
|
+
ss = _ss(ws)
|
|
323
|
+
with patch("tasks.imports.income_extract.get_sheet", return_value=ss):
|
|
324
|
+
dest = tmp_path / "out.json"
|
|
325
|
+
export_to_file(dest)
|
|
326
|
+
|
|
327
|
+
payload = json.loads(dest.read_text())
|
|
328
|
+
amount = payload["entries"][0]["amount"]
|
|
329
|
+
assert isinstance(amount, str)
|
|
330
|
+
assert "." in amount
|
|
@@ -41,6 +41,18 @@ const latencyColor = computed(() => {
|
|
|
41
41
|
if (ms == null) return "var(--muted)";
|
|
42
42
|
return ms > 3000 ? "var(--warning)" : "var(--muted)";
|
|
43
43
|
});
|
|
44
|
+
|
|
45
|
+
const errorMessage = computed(() => {
|
|
46
|
+
const raw = props.provider.last_error_detail;
|
|
47
|
+
if (!raw) return null;
|
|
48
|
+
try {
|
|
49
|
+
let parsed = JSON.parse(raw);
|
|
50
|
+
if (Array.isArray(parsed)) parsed = parsed[0];
|
|
51
|
+
return parsed?.error?.message ?? raw;
|
|
52
|
+
} catch {
|
|
53
|
+
return raw;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
44
56
|
</script>
|
|
45
57
|
|
|
46
58
|
<template>
|
|
@@ -63,7 +75,7 @@ const latencyColor = computed(() => {
|
|
|
63
75
|
<span v-if="rateLimitSecsLeft > 0" class="rate-limit-pill">{{ rateLimitSecsLeft }}s</span>
|
|
64
76
|
</div>
|
|
65
77
|
<div class="card-model">{{ provider.model }}</div>
|
|
66
|
-
<div v-if="
|
|
78
|
+
<div v-if="errorMessage" class="error-detail">{{ errorMessage }}</div>
|
|
67
79
|
</div>
|
|
68
80
|
|
|
69
81
|
<div class="usage-row">
|
|
@@ -195,9 +207,10 @@ const latencyColor = computed(() => {
|
|
|
195
207
|
font-size: 0.7rem;
|
|
196
208
|
color: var(--danger, #ef4444);
|
|
197
209
|
margin-top: 0.2rem;
|
|
210
|
+
display: -webkit-box;
|
|
211
|
+
-webkit-line-clamp: 2;
|
|
212
|
+
-webkit-box-orient: vertical;
|
|
198
213
|
overflow: hidden;
|
|
199
|
-
text-overflow: ellipsis;
|
|
200
|
-
white-space: nowrap;
|
|
201
214
|
}
|
|
202
215
|
|
|
203
216
|
.card-model {
|