skilldrop-cli 0.16.29 → 0.16.30
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 +7 -3
- package/model-routing.json +16 -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/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/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,21 @@
|
|
|
1
|
+
openapi: 3.0.3
|
|
2
|
+
info:
|
|
3
|
+
title: shipwright-api
|
|
4
|
+
version: 2.4.0
|
|
5
|
+
paths:
|
|
6
|
+
/shipments:
|
|
7
|
+
get:
|
|
8
|
+
summary: List shipments
|
|
9
|
+
parameters:
|
|
10
|
+
- name: status
|
|
11
|
+
in: query
|
|
12
|
+
schema:
|
|
13
|
+
type: string
|
|
14
|
+
enum: [pending, in_transit, delivered]
|
|
15
|
+
- name: limit
|
|
16
|
+
in: query
|
|
17
|
+
schema:
|
|
18
|
+
type: integer
|
|
19
|
+
responses:
|
|
20
|
+
"200":
|
|
21
|
+
description: A page of shipments
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "shipwright-api",
|
|
3
|
+
"version": "2.4.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/server.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc -p tsconfig.json",
|
|
9
|
+
"start": "node dist/server.js",
|
|
10
|
+
"start:watch": "tsx watch src/server.ts",
|
|
11
|
+
"lint": "eslint src tests",
|
|
12
|
+
"typecheck": "tsc --noEmit",
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"gen:types": "openapi-typescript openapi.yaml -o src/generated/api-types.ts"
|
|
15
|
+
},
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"express": "^4.19.2",
|
|
18
|
+
"pg": "^8.11.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/express": "^4.17.21",
|
|
22
|
+
"eslint": "^8.57.0",
|
|
23
|
+
"openapi-typescript": "^6.7.4",
|
|
24
|
+
"supertest": "^6.3.4",
|
|
25
|
+
"tsx": "^4.7.1",
|
|
26
|
+
"typescript": "^5.4.2",
|
|
27
|
+
"vitest": "^1.4.0"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface Config {
|
|
2
|
+
port: number;
|
|
3
|
+
databaseUrl: string;
|
|
4
|
+
maxPageSize: number;
|
|
5
|
+
legacySort: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function loadConfig(): Config {
|
|
9
|
+
const databaseUrl = process.env.DATABASE_URL;
|
|
10
|
+
if (!databaseUrl) {
|
|
11
|
+
throw new Error("DATABASE_URL is required");
|
|
12
|
+
}
|
|
13
|
+
return {
|
|
14
|
+
port: Number(process.env.PORT ?? 3000),
|
|
15
|
+
databaseUrl,
|
|
16
|
+
maxPageSize: Number(process.env.MAX_PAGE_SIZE ?? 100),
|
|
17
|
+
// Old consoles sort by created_at ascending; remove once v1 console is retired.
|
|
18
|
+
legacySort: process.env.SHIPMENTS_LEGACY_SORT === "1",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Config } from "../../config.js";
|
|
2
|
+
import type { ListShipmentsQuery } from "../../generated/api-types.js";
|
|
3
|
+
|
|
4
|
+
export interface ShipmentFilter {
|
|
5
|
+
where: string[];
|
|
6
|
+
params: unknown[];
|
|
7
|
+
orderBy: string;
|
|
8
|
+
limit: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function buildShipmentFilter(query: ListShipmentsQuery, config: Config): ShipmentFilter {
|
|
12
|
+
const where: string[] = [];
|
|
13
|
+
const params: unknown[] = [];
|
|
14
|
+
if (query.status) {
|
|
15
|
+
params.push(query.status);
|
|
16
|
+
where.push(`status = $${params.length}`);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
where,
|
|
20
|
+
params,
|
|
21
|
+
orderBy: config.legacySort ? "created_at ASC" : "created_at DESC",
|
|
22
|
+
limit: query.limit ?? 50,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
import type { Config } from "../../config.js";
|
|
3
|
+
import type { ShipmentFilter } from "./filter.js";
|
|
4
|
+
|
|
5
|
+
export async function queryShipments(config: Config, filter: ShipmentFilter) {
|
|
6
|
+
const client = new pg.Client({ connectionString: config.databaseUrl });
|
|
7
|
+
await client.connect();
|
|
8
|
+
try {
|
|
9
|
+
const where = filter.where.length ? "WHERE " + filter.where.join(" AND ") : "";
|
|
10
|
+
const sql = `SELECT id, status, carrier, created_at FROM shipments ${where} ORDER BY ${filter.orderBy} LIMIT ${filter.limit}`;
|
|
11
|
+
return (await client.query(sql, filter.params)).rows;
|
|
12
|
+
} finally {
|
|
13
|
+
await client.end();
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export class HttpError extends Error {
|
|
2
|
+
constructor(public status: number, public code: string, message: string) {
|
|
3
|
+
super(message);
|
|
4
|
+
}
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function badRequest(code: string, message: string): HttpError {
|
|
8
|
+
return new HttpError(400, code, message);
|
|
9
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Request, Response, NextFunction } from "express";
|
|
2
|
+
import type { Config } from "../../config.js";
|
|
3
|
+
import type { ListShipmentsQuery } from "../../generated/api-types.js";
|
|
4
|
+
import { badRequest } from "../../errors.js";
|
|
5
|
+
import { buildShipmentFilter } from "../../domain/shipments/filter.js";
|
|
6
|
+
import { queryShipments } from "../../domain/shipments/repository.js";
|
|
7
|
+
|
|
8
|
+
const STATUSES = ["pending", "in_transit", "delivered"];
|
|
9
|
+
|
|
10
|
+
export function parseListQuery(raw: Record<string, unknown>, config: Config): ListShipmentsQuery {
|
|
11
|
+
const query: ListShipmentsQuery = {};
|
|
12
|
+
if (raw.status !== undefined) {
|
|
13
|
+
if (typeof raw.status !== "string" || !STATUSES.includes(raw.status)) {
|
|
14
|
+
throw badRequest("invalid_status", "status must be one of " + STATUSES.join(", "));
|
|
15
|
+
}
|
|
16
|
+
query.status = raw.status as ListShipmentsQuery["status"];
|
|
17
|
+
}
|
|
18
|
+
if (raw.limit !== undefined) {
|
|
19
|
+
const limit = Number(raw.limit);
|
|
20
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > config.maxPageSize) {
|
|
21
|
+
throw badRequest("invalid_limit", "limit must be 1.." + config.maxPageSize);
|
|
22
|
+
}
|
|
23
|
+
query.limit = limit;
|
|
24
|
+
}
|
|
25
|
+
return query;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function listShipments(config: Config) {
|
|
29
|
+
return async (req: Request, res: Response, next: NextFunction) => {
|
|
30
|
+
try {
|
|
31
|
+
const query = parseListQuery(req.query, config);
|
|
32
|
+
const filter = buildShipmentFilter(query, config);
|
|
33
|
+
res.json({ items: await queryShipments(config, filter) });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
next(err);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Express } from "express";
|
|
2
|
+
import type { Config } from "../config.js";
|
|
3
|
+
import { listShipments } from "./handlers/shipments.js";
|
|
4
|
+
|
|
5
|
+
export function registerRoutes(app: Express, config: Config): void {
|
|
6
|
+
app.get("/shipments", listShipments(config));
|
|
7
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Nightly CSV export for the finance team. Runs from the scheduler, not the API.
|
|
2
|
+
import pg from "pg";
|
|
3
|
+
import { loadConfig } from "../config.js";
|
|
4
|
+
|
|
5
|
+
export async function exportShipments(status?: string): Promise<string> {
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const client = new pg.Client({ connectionString: config.databaseUrl });
|
|
8
|
+
await client.connect();
|
|
9
|
+
try {
|
|
10
|
+
// Same filtering as the list endpoint, kept here so the job has no HTTP dependency.
|
|
11
|
+
const params: unknown[] = [];
|
|
12
|
+
let where = "";
|
|
13
|
+
if (status) {
|
|
14
|
+
params.push(status);
|
|
15
|
+
where = "WHERE status = $1";
|
|
16
|
+
}
|
|
17
|
+
const rows = (await client.query(`SELECT id, status, carrier FROM shipments ${where} ORDER BY created_at DESC`, params)).rows;
|
|
18
|
+
return rows.map((r) => [r.id, r.status, r.carrier].join(",")).join("\n");
|
|
19
|
+
} finally {
|
|
20
|
+
await client.end();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import { loadConfig } from "./config.js";
|
|
3
|
+
import { registerRoutes } from "./http/routes.js";
|
|
4
|
+
import { HttpError } from "./errors.js";
|
|
5
|
+
|
|
6
|
+
const config = loadConfig();
|
|
7
|
+
const app = express();
|
|
8
|
+
registerRoutes(app, config);
|
|
9
|
+
|
|
10
|
+
app.use((err: unknown, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
|
|
11
|
+
if (err instanceof HttpError) {
|
|
12
|
+
res.status(err.status).json({ error: err.code, message: err.message });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
res.status(500).json({ error: "internal" });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
app.listen(config.port);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildShipmentFilter } from "../../src/domain/shipments/filter.js";
|
|
3
|
+
|
|
4
|
+
const config = { port: 0, databaseUrl: "postgres://unused", maxPageSize: 100, legacySort: false };
|
|
5
|
+
|
|
6
|
+
describe("buildShipmentFilter", () => {
|
|
7
|
+
it("adds a status clause with a positional parameter", () => {
|
|
8
|
+
const f = buildShipmentFilter({ status: "pending" }, config);
|
|
9
|
+
expect(f.where).toEqual(["status = $1"]);
|
|
10
|
+
expect(f.params).toEqual(["pending"]);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("sorts ascending when legacySort is on", () => {
|
|
14
|
+
expect(buildShipmentFilter({}, { ...config, legacySort: true }).orderBy).toBe("created_at ASC");
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { parseListQuery } from "../../src/http/handlers/shipments.js";
|
|
3
|
+
|
|
4
|
+
const config = { port: 0, databaseUrl: "postgres://unused", maxPageSize: 100, legacySort: false };
|
|
5
|
+
|
|
6
|
+
describe("parseListQuery", () => {
|
|
7
|
+
it("accepts a known status", () => {
|
|
8
|
+
expect(parseListQuery({ status: "delivered" }, config)).toEqual({ status: "delivered" });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rejects an unknown status with invalid_status", () => {
|
|
12
|
+
expect(() => parseListQuery({ status: "lost" }, config)).toThrowError(/status must be one of/);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("rejects a limit above maxPageSize", () => {
|
|
16
|
+
expect(() => parseListQuery({ limit: "500" }, config)).toThrowError(/limit must be/);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
jobs:
|
|
4
|
+
check:
|
|
5
|
+
runs-on: ubuntu-latest
|
|
6
|
+
steps:
|
|
7
|
+
- uses: actions/checkout@v4
|
|
8
|
+
with:
|
|
9
|
+
fetch-depth: 0
|
|
10
|
+
- uses: actions/setup-python@v5
|
|
11
|
+
with:
|
|
12
|
+
python-version: |
|
|
13
|
+
3.10
|
|
14
|
+
3.12
|
|
15
|
+
- run: pip install nox
|
|
16
|
+
- run: nox -s lint tests
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# ledgerline
|
|
2
|
+
|
|
3
|
+
Turns bank-statement CSV exports into one ledger format.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
pip install -e .
|
|
8
|
+
|
|
9
|
+
## Running the tests
|
|
10
|
+
|
|
11
|
+
python setup.py test
|
|
12
|
+
|
|
13
|
+
## Adding a bank
|
|
14
|
+
|
|
15
|
+
Drop a new module in `src/ledgerline/parsers/` and it is picked up automatically.
|
|
16
|
+
Each parser subclasses `BaseParser` and implements `parse_row`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import nox
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
@nox.session(python=["3.10", "3.12"])
|
|
5
|
+
def tests(session):
|
|
6
|
+
session.install("-e", ".[dev]")
|
|
7
|
+
session.run("pytest", "-m", "not slow", *session.posargs)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@nox.session
|
|
11
|
+
def lint(session):
|
|
12
|
+
session.install("ruff")
|
|
13
|
+
session.run("ruff", "check", "src", "tests")
|
|
14
|
+
session.run("ruff", "format", "--check", "src", "tests")
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "setuptools-scm>=8"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ledgerline"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
requires-python = ">=3.10"
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[project.optional-dependencies]
|
|
12
|
+
dev = ["pytest>=8", "ruff>=0.4", "nox>=2024.3"]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
ledgerline = "ledgerline.cli:main"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools_scm]
|
|
18
|
+
version_file = "src/ledgerline/_version.py"
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
22
|
+
markers = ["slow: reads the large sample statements"]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import csv
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ledgerline.detect import detect_bank
|
|
7
|
+
from ledgerline.errors import ParseError
|
|
8
|
+
from ledgerline.parsers import PARSERS
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv=None):
|
|
12
|
+
ap = argparse.ArgumentParser(prog="ledgerline")
|
|
13
|
+
ap.add_argument("statement")
|
|
14
|
+
ap.add_argument("--bank", help="skip detection and use this parser")
|
|
15
|
+
args = ap.parse_args(argv)
|
|
16
|
+
currency = os.environ.get("LEDGERLINE_CURRENCY", "GBP")
|
|
17
|
+
with open(args.statement, newline="", encoding="utf-8-sig") as fh:
|
|
18
|
+
rows = list(csv.reader(fh))
|
|
19
|
+
bank = args.bank or detect_bank(rows[0])
|
|
20
|
+
parser = PARSERS[bank](currency=currency)
|
|
21
|
+
writer = csv.writer(sys.stdout)
|
|
22
|
+
for n, row in enumerate(rows[1:], start=2):
|
|
23
|
+
try:
|
|
24
|
+
writer.writerow(parser.parse_row(row).as_csv())
|
|
25
|
+
except ParseError as e:
|
|
26
|
+
print(f"line {n}: {e}", file=sys.stderr)
|
|
27
|
+
return 1
|
|
28
|
+
return 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
sys.exit(main())
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from ledgerline.errors import ParseError
|
|
2
|
+
|
|
3
|
+
# Header row of each bank's export -> parser name in PARSERS.
|
|
4
|
+
HEADER_SIGNATURES = {
|
|
5
|
+
("Date", "Description", "Amount", "Balance"): "acme",
|
|
6
|
+
("Posted", "Payee", "Debit", "Credit"): "harbor",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def detect_bank(header):
|
|
11
|
+
key = tuple(cell.strip() for cell in header)
|
|
12
|
+
try:
|
|
13
|
+
return HEADER_SIGNATURES[key]
|
|
14
|
+
except KeyError:
|
|
15
|
+
raise ParseError(f"unrecognised header {list(key)}; pass --bank")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from decimal import Decimal, InvalidOperation
|
|
2
|
+
|
|
3
|
+
from ledgerline.errors import ParseError
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def parse_amount(text: str) -> Decimal:
|
|
7
|
+
"""Parse '1,234.50' or '(12.00)' into a Decimal. Parentheses mean a debit."""
|
|
8
|
+
cleaned = text.strip().replace(",", "")
|
|
9
|
+
negative = cleaned.startswith("(") and cleaned.endswith(")")
|
|
10
|
+
try:
|
|
11
|
+
value = Decimal(cleaned.strip("()"))
|
|
12
|
+
except InvalidOperation:
|
|
13
|
+
raise ParseError(f"amount {text!r} is not a number")
|
|
14
|
+
return -value if negative else value
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
|
|
3
|
+
from ledgerline.errors import ParseError
|
|
4
|
+
from ledgerline.money import parse_amount
|
|
5
|
+
from ledgerline.parsers.base import BaseParser, Entry
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AcmeParser(BaseParser):
|
|
9
|
+
date_format = "%d/%m/%Y"
|
|
10
|
+
|
|
11
|
+
def parse_row(self, row):
|
|
12
|
+
if len(row) != 4:
|
|
13
|
+
raise ParseError(f"expected 4 columns, got {len(row)}")
|
|
14
|
+
try:
|
|
15
|
+
on = datetime.strptime(row[0], self.date_format).date()
|
|
16
|
+
except ValueError:
|
|
17
|
+
raise ParseError(f"Date {row[0]!r} is not DD/MM/YYYY")
|
|
18
|
+
return Entry(on=on, payee=row[1].strip(), amount=parse_amount(row[2]), currency=self.currency)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True)
|
|
7
|
+
class Entry:
|
|
8
|
+
on: date
|
|
9
|
+
payee: str
|
|
10
|
+
amount: Decimal
|
|
11
|
+
currency: str
|
|
12
|
+
|
|
13
|
+
def as_csv(self):
|
|
14
|
+
return [self.on.isoformat(), self.payee, f"{self.amount:.2f}", self.currency]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseParser:
|
|
18
|
+
date_format = "%Y-%m-%d"
|
|
19
|
+
|
|
20
|
+
def __init__(self, currency: str):
|
|
21
|
+
self.currency = currency
|
|
22
|
+
|
|
23
|
+
def parse_row(self, row) -> Entry:
|
|
24
|
+
raise NotImplementedError
|
|
@@ -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
|
+
}
|