skilldrop-cli 0.16.29 → 0.16.31
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.
- package/bin/skilldrop.js +160 -33
- package/catalogue.json +10 -4
- package/model-routing.json +24 -0
- package/package.json +1 -1
- package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
- package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
- package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
- package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
- package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
- package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
- package/packs/dev-team/loops/build/LOOP.md +4 -2
- package/packs/dev-team/loops/build/loop.json +2 -2
- package/packs/dev-team/loops/release/LOOP.md +7 -5
- package/packs/dev-team/loops/release/loop.json +2 -2
- package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
- package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
- package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
- package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
- package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
- package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
- package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
- package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
- package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
- package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
- package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
- package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
- package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
- package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
- package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
- package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
- package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
- package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
- package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
- package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
- package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
- package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
- package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
- package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
- package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
- package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
- package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
- package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
- package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
- package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
- package/packs/product-manager/loops/discover/LOOP.md +5 -4
- package/packs/product-manager/loops/discover/loop.json +2 -2
- package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
- package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
- package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
- package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
- package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
- package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
- package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
- package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
- package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from ledgerline.parsers.base import BaseParser, Entry
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class HarborParser(BaseParser):
|
|
8
|
+
def parse_row(self, row):
|
|
9
|
+
on = datetime.strptime(row[0], self.date_format).date()
|
|
10
|
+
# Harbor splits debit and credit into two columns.
|
|
11
|
+
debit = Decimal(row[2].replace(",", "") or "0")
|
|
12
|
+
credit = Decimal(row[3].replace(",", "") or "0")
|
|
13
|
+
return Entry(on=on, payee=row[1].strip(), amount=credit - debit, currency=self.currency)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from datetime import date
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import csv
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from ledgerline.errors import ParseError
|
|
9
|
+
from ledgerline.parsers.acme import AcmeParser
|
|
10
|
+
|
|
11
|
+
FIXTURES = Path(__file__).parent / "fixtures"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def test_parses_sample_rows():
|
|
15
|
+
rows = list(csv.reader(open(FIXTURES / "acme_sample.csv", encoding="utf-8")))
|
|
16
|
+
entries = [AcmeParser(currency="GBP").parse_row(r) for r in rows[1:]]
|
|
17
|
+
assert entries[0].on == date(2026, 3, 2)
|
|
18
|
+
assert entries[1].amount == Decimal("-12.00")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_bad_date_names_the_column():
|
|
22
|
+
with pytest.raises(ParseError, match="Date"):
|
|
23
|
+
AcmeParser(currency="GBP").parse_row(["2026-03-02", "Cafe", "3.10", "0"])
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from ledgerline.detect import detect_bank
|
|
4
|
+
from ledgerline.errors import ParseError
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_detects_acme_header():
|
|
8
|
+
assert detect_bank(["Date", "Description", "Amount", "Balance"]) == "acme"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_unknown_header_tells_the_user_to_pass_bank():
|
|
12
|
+
with pytest.raises(ParseError, match="--bank"):
|
|
13
|
+
detect_bank(["When", "What", "How much"])
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
unit:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- uses: actions/checkout@v4
|
|
8
|
+
- uses: actions/setup-go@v5
|
|
9
|
+
with:
|
|
10
|
+
go-version: "1.22"
|
|
11
|
+
- run: make lint
|
|
12
|
+
- run: make test
|
|
13
|
+
integration:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
services:
|
|
16
|
+
postgres:
|
|
17
|
+
image: postgres:16
|
|
18
|
+
env:
|
|
19
|
+
POSTGRES_PASSWORD: example-only
|
|
20
|
+
ports: ["5432:5432"]
|
|
21
|
+
env:
|
|
22
|
+
TIDEPOOL_DATABASE_URL: postgres://postgres:example-only@localhost:5432/postgres?sslmode=disable
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-go@v5
|
|
26
|
+
with:
|
|
27
|
+
go-version: "1.22"
|
|
28
|
+
- run: make test-integration
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.PHONY: build test test-integration generate lint
|
|
2
|
+
|
|
3
|
+
build:
|
|
4
|
+
go build -o bin/tidepool ./cmd/tidepool
|
|
5
|
+
|
|
6
|
+
test:
|
|
7
|
+
go test ./...
|
|
8
|
+
|
|
9
|
+
test-integration:
|
|
10
|
+
go test -tags=integration ./internal/store/...
|
|
11
|
+
|
|
12
|
+
generate:
|
|
13
|
+
sqlc generate
|
|
14
|
+
|
|
15
|
+
lint:
|
|
16
|
+
golangci-lint run
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# tidepool
|
|
2
|
+
|
|
3
|
+
Delivers webhook events to tenant endpoints, with retries.
|
|
4
|
+
|
|
5
|
+
## Configuration
|
|
6
|
+
|
|
7
|
+
Retries are set in `config.yaml`:
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
max_attempts: 5
|
|
11
|
+
retry_delay: 2s
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Development
|
|
15
|
+
|
|
16
|
+
`make test` runs the full test suite, including the database tests.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"log"
|
|
6
|
+
"net/http"
|
|
7
|
+
|
|
8
|
+
"example.com/tidepool/internal/config"
|
|
9
|
+
"example.com/tidepool/internal/dispatch"
|
|
10
|
+
"example.com/tidepool/internal/store"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func main() {
|
|
14
|
+
cfg, err := config.FromEnv()
|
|
15
|
+
if err != nil {
|
|
16
|
+
log.Fatalf("config: %v", err)
|
|
17
|
+
}
|
|
18
|
+
db, err := store.Open(context.Background(), cfg.DatabaseURL)
|
|
19
|
+
if err != nil {
|
|
20
|
+
log.Fatalf("store: %v", err)
|
|
21
|
+
}
|
|
22
|
+
d := dispatch.New(cfg, store.New(db), dispatch.NewHTTPSender(http.DefaultClient))
|
|
23
|
+
log.Fatal(d.Run(context.Background()))
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package config
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"os"
|
|
6
|
+
"strconv"
|
|
7
|
+
"time"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type Config struct {
|
|
11
|
+
DatabaseURL string
|
|
12
|
+
MaxAttempts int
|
|
13
|
+
RetryDelay time.Duration
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// FromEnv reads every setting from the environment. There is no config file.
|
|
17
|
+
func FromEnv() (Config, error) {
|
|
18
|
+
cfg := Config{MaxAttempts: 5, RetryDelay: 2 * time.Second}
|
|
19
|
+
cfg.DatabaseURL = os.Getenv("TIDEPOOL_DATABASE_URL")
|
|
20
|
+
if cfg.DatabaseURL == "" {
|
|
21
|
+
return cfg, fmt.Errorf("TIDEPOOL_DATABASE_URL is required")
|
|
22
|
+
}
|
|
23
|
+
if v, ok := os.LookupEnv("TIDEPOOL_MAX_ATTEMPTS"); ok {
|
|
24
|
+
n, err := strconv.Atoi(v)
|
|
25
|
+
if err != nil {
|
|
26
|
+
return cfg, fmt.Errorf("TIDEPOOL_MAX_ATTEMPTS: %w", err)
|
|
27
|
+
}
|
|
28
|
+
cfg.MaxAttempts = n
|
|
29
|
+
}
|
|
30
|
+
if v, ok := os.LookupEnv("TIDEPOOL_RETRY_DELAY"); ok {
|
|
31
|
+
d, err := time.ParseDuration(v)
|
|
32
|
+
if err != nil {
|
|
33
|
+
return cfg, fmt.Errorf("TIDEPOOL_RETRY_DELAY: %w", err)
|
|
34
|
+
}
|
|
35
|
+
cfg.RetryDelay = d
|
|
36
|
+
}
|
|
37
|
+
return cfg, nil
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
package dispatch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"time"
|
|
7
|
+
|
|
8
|
+
"example.com/tidepool/internal/config"
|
|
9
|
+
"example.com/tidepool/internal/store"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
type Dispatcher struct {
|
|
13
|
+
cfg config.Config
|
|
14
|
+
q *store.Queries
|
|
15
|
+
sender Sender
|
|
16
|
+
sleep func(time.Duration)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func New(cfg config.Config, q *store.Queries, s Sender) *Dispatcher {
|
|
20
|
+
return &Dispatcher{cfg: cfg, q: q, sender: s, sleep: time.Sleep}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Deliver posts one event, retrying with a fixed delay until MaxAttempts.
|
|
24
|
+
func (d *Dispatcher) Deliver(ctx context.Context, ev store.Event) error {
|
|
25
|
+
var last error
|
|
26
|
+
for attempt := 1; attempt <= d.cfg.MaxAttempts; attempt++ {
|
|
27
|
+
if last = d.sender.Send(ctx, ev.EndpointURL, ev.Payload); last == nil {
|
|
28
|
+
return nil
|
|
29
|
+
}
|
|
30
|
+
if attempt < d.cfg.MaxAttempts {
|
|
31
|
+
d.sleep(d.cfg.RetryDelay)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return fmt.Errorf("event %d: gave up after %d attempts: %w", ev.ID, d.cfg.MaxAttempts, last)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
func (d *Dispatcher) Run(ctx context.Context) error {
|
|
38
|
+
for {
|
|
39
|
+
events, err := d.q.PendingEvents(ctx, 100)
|
|
40
|
+
if err != nil {
|
|
41
|
+
return fmt.Errorf("pending events: %w", err)
|
|
42
|
+
}
|
|
43
|
+
for _, ev := range events {
|
|
44
|
+
if err := d.Deliver(ctx, ev); err != nil {
|
|
45
|
+
_ = d.q.MarkDead(ctx, ev.ID)
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
_ = d.q.MarkDelivered(ctx, ev.ID)
|
|
49
|
+
}
|
|
50
|
+
d.sleep(time.Second)
|
|
51
|
+
}
|
|
52
|
+
}
|
package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
package dispatch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"errors"
|
|
6
|
+
"testing"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"example.com/tidepool/internal/config"
|
|
10
|
+
"example.com/tidepool/internal/store"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
type flakySender struct{ failures, calls int }
|
|
14
|
+
|
|
15
|
+
func (f *flakySender) Send(context.Context, string, []byte) error {
|
|
16
|
+
f.calls++
|
|
17
|
+
if f.calls <= f.failures {
|
|
18
|
+
return errors.New("boom")
|
|
19
|
+
}
|
|
20
|
+
return nil
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func TestDeliverRetriesUntilSuccess(t *testing.T) {
|
|
24
|
+
var slept []time.Duration
|
|
25
|
+
s := &flakySender{failures: 2}
|
|
26
|
+
d := &Dispatcher{cfg: config.Config{MaxAttempts: 5, RetryDelay: time.Second}, sender: s,
|
|
27
|
+
sleep: func(t time.Duration) { slept = append(slept, t) }}
|
|
28
|
+
if err := d.Deliver(context.Background(), store.Event{ID: 1}); err != nil {
|
|
29
|
+
t.Fatalf("unexpected error: %v", err)
|
|
30
|
+
}
|
|
31
|
+
if s.calls != 3 || len(slept) != 2 {
|
|
32
|
+
t.Fatalf("calls=%d slept=%v", s.calls, slept)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func TestDeliverGivesUpAtMaxAttempts(t *testing.T) {
|
|
37
|
+
s := &flakySender{failures: 10}
|
|
38
|
+
d := &Dispatcher{cfg: config.Config{MaxAttempts: 3}, sender: s, sleep: func(time.Duration) {}}
|
|
39
|
+
if err := d.Deliver(context.Background(), store.Event{ID: 7}); err == nil || s.calls != 3 {
|
|
40
|
+
t.Fatalf("err=%v calls=%d", err, s.calls)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
package dispatch
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"context"
|
|
6
|
+
"fmt"
|
|
7
|
+
"net/http"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
type Sender interface {
|
|
11
|
+
Send(ctx context.Context, url string, body []byte) error
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type HTTPSender struct{ client *http.Client }
|
|
15
|
+
|
|
16
|
+
func NewHTTPSender(c *http.Client) *HTTPSender { return &HTTPSender{client: c} }
|
|
17
|
+
|
|
18
|
+
func (s *HTTPSender) Send(ctx context.Context, url string, body []byte) error {
|
|
19
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
|
|
20
|
+
if err != nil {
|
|
21
|
+
return fmt.Errorf("build request: %w", err)
|
|
22
|
+
}
|
|
23
|
+
resp, err := s.client.Do(req)
|
|
24
|
+
if err != nil {
|
|
25
|
+
return fmt.Errorf("post %s: %w", url, err)
|
|
26
|
+
}
|
|
27
|
+
defer resp.Body.Close()
|
|
28
|
+
if resp.StatusCode >= 300 {
|
|
29
|
+
return fmt.Errorf("post %s: status %d", url, resp.StatusCode)
|
|
30
|
+
}
|
|
31
|
+
return nil
|
|
32
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Package replay re-sends dead-lettered events when an operator asks for it.
|
|
2
|
+
package replay
|
|
3
|
+
|
|
4
|
+
import (
|
|
5
|
+
"context"
|
|
6
|
+
"fmt"
|
|
7
|
+
"time"
|
|
8
|
+
|
|
9
|
+
"example.com/tidepool/internal/config"
|
|
10
|
+
"example.com/tidepool/internal/dispatch"
|
|
11
|
+
"example.com/tidepool/internal/store"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
// Replay sends each dead event again. It keeps its own retry loop so an operator
|
|
15
|
+
// replay never blocks on the dispatcher's queue.
|
|
16
|
+
func Replay(ctx context.Context, cfg config.Config, q *store.Queries, s dispatch.Sender) error {
|
|
17
|
+
events, err := q.DeadEvents(ctx)
|
|
18
|
+
if err != nil {
|
|
19
|
+
return fmt.Errorf("dead events: %w", err)
|
|
20
|
+
}
|
|
21
|
+
for _, ev := range events {
|
|
22
|
+
for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ {
|
|
23
|
+
if err := s.Send(ctx, ev.EndpointURL, ev.Payload); err == nil {
|
|
24
|
+
_ = q.MarkDelivered(ctx, ev.ID)
|
|
25
|
+
break
|
|
26
|
+
}
|
|
27
|
+
time.Sleep(cfg.RetryDelay)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return nil
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
package store
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
|
|
6
|
+
"github.com/jackc/pgx/v5/pgxpool"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
type Queries struct{ pool *pgxpool.Pool }
|
|
10
|
+
|
|
11
|
+
func Open(ctx context.Context, url string) (*pgxpool.Pool, error) { return pgxpool.New(ctx, url) }
|
|
12
|
+
|
|
13
|
+
func New(pool *pgxpool.Pool) *Queries { return &Queries{pool: pool} }
|
|
14
|
+
|
|
15
|
+
func (q *Queries) list(ctx context.Context, sql string, args ...any) ([]Event, error) {
|
|
16
|
+
rows, err := q.pool.Query(ctx, sql, args...)
|
|
17
|
+
if err != nil {
|
|
18
|
+
return nil, err
|
|
19
|
+
}
|
|
20
|
+
defer rows.Close()
|
|
21
|
+
var out []Event
|
|
22
|
+
for rows.Next() {
|
|
23
|
+
var e Event
|
|
24
|
+
if err := rows.Scan(&e.ID, &e.EndpointURL, &e.Payload); err != nil {
|
|
25
|
+
return nil, err
|
|
26
|
+
}
|
|
27
|
+
out = append(out, e)
|
|
28
|
+
}
|
|
29
|
+
return out, rows.Err()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (q *Queries) exec(ctx context.Context, sql string, args ...any) error {
|
|
33
|
+
_, err := q.pool.Exec(ctx, sql, args...)
|
|
34
|
+
return err
|
|
35
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
-- name: PendingEvents :many
|
|
2
|
+
SELECT id, endpoint_url, payload FROM events WHERE state = 'pending' ORDER BY id LIMIT $1;
|
|
3
|
+
|
|
4
|
+
-- name: DeadEvents :many
|
|
5
|
+
SELECT id, endpoint_url, payload FROM events WHERE state = 'dead' ORDER BY id;
|
|
6
|
+
|
|
7
|
+
-- name: MarkDelivered :exec
|
|
8
|
+
UPDATE events SET state = 'delivered' WHERE id = $1;
|
|
9
|
+
|
|
10
|
+
-- name: MarkDead :exec
|
|
11
|
+
UPDATE events SET state = 'dead' WHERE id = $1;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Code generated by sqlc. DO NOT EDIT.
|
|
2
|
+
// versions:
|
|
3
|
+
// sqlc v1.26.0
|
|
4
|
+
// source: query.sql
|
|
5
|
+
|
|
6
|
+
package store
|
|
7
|
+
|
|
8
|
+
import "context"
|
|
9
|
+
|
|
10
|
+
type Event struct {
|
|
11
|
+
ID int64
|
|
12
|
+
EndpointURL string
|
|
13
|
+
Payload []byte
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func (q *Queries) PendingEvents(ctx context.Context, limit int32) ([]Event, error) {
|
|
17
|
+
return q.list(ctx, "SELECT id, endpoint_url, payload FROM events WHERE state = 'pending' ORDER BY id LIMIT $1", limit)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func (q *Queries) DeadEvents(ctx context.Context) ([]Event, error) {
|
|
21
|
+
return q.list(ctx, "SELECT id, endpoint_url, payload FROM events WHERE state = 'dead' ORDER BY id")
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func (q *Queries) MarkDelivered(ctx context.Context, id int64) error {
|
|
25
|
+
return q.exec(ctx, "UPDATE events SET state = 'delivered' WHERE id = $1", id)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func (q *Queries) MarkDead(ctx context.Context, id int64) error {
|
|
29
|
+
return q.exec(ctx, "UPDATE events SET state = 'dead' WHERE id = $1", id)
|
|
30
|
+
}
|
package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
//go:build integration
|
|
2
|
+
|
|
3
|
+
package store
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"context"
|
|
7
|
+
"os"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func TestMarkDeadMovesEventOutOfPending(t *testing.T) {
|
|
12
|
+
ctx := context.Background()
|
|
13
|
+
pool, err := Open(ctx, os.Getenv("TIDEPOOL_DATABASE_URL"))
|
|
14
|
+
if err != nil {
|
|
15
|
+
t.Fatal(err)
|
|
16
|
+
}
|
|
17
|
+
q := New(pool)
|
|
18
|
+
if _, err := q.PendingEvents(ctx, 1); err != nil {
|
|
19
|
+
t.Fatal(err)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
jobs:
|
|
7
|
+
test:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-python@v5
|
|
12
|
+
with:
|
|
13
|
+
python-version: "3.12"
|
|
14
|
+
- run: make install
|
|
15
|
+
- run: make check-generated
|
|
16
|
+
- run: make test
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
.PHONY: install run test gen check-generated
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
pip install -e ".[dev]"
|
|
5
|
+
|
|
6
|
+
run:
|
|
7
|
+
SHIPCALC_API_KEY=dev-placeholder uvicorn shipcalc.main:app --reload --port 8080
|
|
8
|
+
|
|
9
|
+
test:
|
|
10
|
+
SHIPCALC_API_KEY=test-placeholder pytest -q
|
|
11
|
+
|
|
12
|
+
gen:
|
|
13
|
+
python scripts/gen_rates.py data/rates.csv src/shipcalc/generated/rates_table.py
|
|
14
|
+
|
|
15
|
+
check-generated: gen
|
|
16
|
+
git diff --exit-code src/shipcalc/generated/
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# shipcalc
|
|
2
|
+
|
|
3
|
+
Shipping quote service for the storefront. Given a cart it returns the shipping
|
|
4
|
+
cost, loyalty discount and VAT.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
pip install -r requirements.txt
|
|
9
|
+
|
|
10
|
+
## Run locally
|
|
11
|
+
|
|
12
|
+
export FLASK_APP=shipcalc
|
|
13
|
+
flask run
|
|
14
|
+
|
|
15
|
+
## Tests
|
|
16
|
+
|
|
17
|
+
python -m unittest discover
|
|
18
|
+
|
|
19
|
+
## Layout
|
|
20
|
+
|
|
21
|
+
- `src/shipcalc/shipping.py` - shipping cost and free-shipping rules
|
|
22
|
+
- `src/shipcalc/discounts.py` - loyalty discounts
|
|
23
|
+
- `src/shipcalc/tax.py` - VAT
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "shipcalc"
|
|
7
|
+
version = "2.3.0"
|
|
8
|
+
requires-python = ">=3.11"
|
|
9
|
+
dependencies = ["fastapi>=0.110", "uvicorn>=0.29"]
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
dev = ["pytest>=8", "httpx>=0.27"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools.packages.find]
|
|
15
|
+
where = ["src"]
|
|
16
|
+
|
|
17
|
+
[tool.pytest.ini_options]
|
|
18
|
+
testpaths = ["tests"]
|
|
19
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Generate src/shipcalc/generated/rates_table.py from data/rates.csv.
|
|
2
|
+
|
|
3
|
+
Usage: python scripts/gen_rates.py data/rates.csv src/shipcalc/generated/rates_table.py
|
|
4
|
+
"""
|
|
5
|
+
import csv
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
HEADER = """# DO NOT EDIT. This file is generated by scripts/gen_rates.py from data/rates.csv.
|
|
9
|
+
# Edit the CSV and run `make gen` instead. CI fails if this file is out of date.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def main(src: str, dest: str) -> None:
|
|
14
|
+
with open(src, newline="") as fh:
|
|
15
|
+
rows = list(csv.DictReader(fh))
|
|
16
|
+
lines = [HEADER, "RATES = {"]
|
|
17
|
+
for row in rows:
|
|
18
|
+
lines.append(
|
|
19
|
+
f' "{row["region"]}": {{"currency": "{row["currency"]}", '
|
|
20
|
+
f'"base_rate": {float(row["base_rate"])}, "per_kg": {float(row["per_kg"])}, '
|
|
21
|
+
f'"free_threshold": {float(row["free_threshold"])}}},'
|
|
22
|
+
)
|
|
23
|
+
lines.append("}")
|
|
24
|
+
with open(dest, "w") as fh:
|
|
25
|
+
fh.write("\n".join(lines) + "\n")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main(sys.argv[1], sys.argv[2])
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Copy shown in the cart drawer ("Spend X more for free shipping")."""
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
# Kept here so the banner doesn't need the rates table at import time.
|
|
5
|
+
FREE_SHIPPING_MIN = {"EU": Decimal("50.00"), "UK": Decimal("40.00"), "US": Decimal("75.00")}
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def free_shipping_banner(region: str, subtotal: Decimal) -> str:
|
|
9
|
+
threshold = FREE_SHIPPING_MIN.get(region)
|
|
10
|
+
if threshold is None:
|
|
11
|
+
return ""
|
|
12
|
+
if subtotal >= threshold:
|
|
13
|
+
return "You qualify for free shipping!"
|
|
14
|
+
return f"Spend {threshold - subtotal} more for free shipping."
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
from shipcalc.api.preview import free_shipping_banner
|
|
7
|
+
from shipcalc.discounts import loyalty_discount
|
|
8
|
+
from shipcalc.shipping import shipping_cost
|
|
9
|
+
from shipcalc.tax import vat
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class QuoteRequest(BaseModel):
|
|
15
|
+
region: str
|
|
16
|
+
subtotal: Decimal
|
|
17
|
+
weight_kg: Decimal
|
|
18
|
+
tier: str = "bronze"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@router.post("/quote")
|
|
22
|
+
def quote(req: QuoteRequest) -> dict:
|
|
23
|
+
discount = loyalty_discount(req.tier, req.subtotal)
|
|
24
|
+
shipping = shipping_cost(req.region, req.subtotal, req.weight_kg)
|
|
25
|
+
tax = vat(req.region, req.subtotal - discount + shipping)
|
|
26
|
+
return {
|
|
27
|
+
"discount": str(discount),
|
|
28
|
+
"shipping": str(shipping),
|
|
29
|
+
"tax": str(tax),
|
|
30
|
+
"total": str(req.subtotal - discount + shipping + tax),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@router.get("/preview/banner")
|
|
35
|
+
def banner(region: str, subtotal: Decimal) -> dict:
|
|
36
|
+
return {"banner": free_shipping_banner(region, subtotal)}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass(frozen=True)
|
|
6
|
+
class Settings:
|
|
7
|
+
api_key: str
|
|
8
|
+
log_level: str
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def load_settings() -> Settings:
|
|
12
|
+
api_key = os.environ.get("SHIPCALC_API_KEY")
|
|
13
|
+
if not api_key:
|
|
14
|
+
raise RuntimeError("SHIPCALC_API_KEY must be set (see .env.example)")
|
|
15
|
+
return Settings(api_key=api_key, log_level=os.environ.get("SHIPCALC_LOG_LEVEL", "info"))
|