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.
Files changed (174) hide show
  1. package/bin/skilldrop.js +160 -33
  2. package/catalogue.json +7 -3
  3. package/model-routing.json +16 -0
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
  6. package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
  7. package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
  8. package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
  9. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
  10. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
  11. package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
  12. package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
  13. package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
  14. package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
  15. package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
  16. package/packs/dev-team/loops/build/LOOP.md +4 -2
  17. package/packs/dev-team/loops/build/loop.json +2 -2
  18. package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
  19. package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
  20. package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
  21. package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
  22. package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
  23. package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
  24. package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
  25. package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
  26. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
  27. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
  28. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
  29. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
  30. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
  31. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
  32. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
  33. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
  34. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
  35. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
  36. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
  37. package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
  38. package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
  39. package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
  40. package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
  41. package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
  42. package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
  43. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
  44. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
  45. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
  46. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
  47. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
  48. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
  49. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
  50. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
  51. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
  52. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
  53. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
  54. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
  55. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
  56. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
  57. package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
  58. package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
  59. package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
  60. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
  61. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
  62. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
  63. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
  64. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
  65. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
  66. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
  67. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
  68. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
  69. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
  70. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
  71. package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
  72. package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
  73. package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
  74. package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
  75. package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
  76. package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
  77. package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
  78. package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
  79. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
  80. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
  81. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
  82. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
  83. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
  84. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
  85. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
  86. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
  87. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
  88. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
  89. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
  90. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
  91. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
  92. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
  93. package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
  94. package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
  95. package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
  96. package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
  97. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
  98. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
  99. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
  100. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
  101. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
  102. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
  103. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
  104. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
  105. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
  106. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
  107. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
  108. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
  109. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
  110. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
  111. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
  112. package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
  113. package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
  114. package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
  115. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
  116. package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
  117. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
  118. package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
  119. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
  120. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
  121. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
  122. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
  123. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
  124. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
  125. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
  126. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
  127. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
  128. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
  129. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
  130. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
  131. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
  132. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
  133. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
  134. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
  135. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
  136. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
  137. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
  138. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
  139. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
  140. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
  141. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
  142. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
  143. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
  144. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
  145. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
  146. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
  147. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
  148. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
  149. package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
  150. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
  151. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
  152. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
  153. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
  154. package/packs/product-manager/loops/discover/LOOP.md +5 -4
  155. package/packs/product-manager/loops/discover/loop.json +2 -2
  156. package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
  157. package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
  158. package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
  159. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
  160. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
  161. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
  162. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
  163. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
  164. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
  165. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
  166. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
  167. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
  168. package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
  169. package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
  170. package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
  171. package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
  172. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
  173. package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
  174. package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
@@ -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
+ }
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ CREATE TABLE events (
2
+ id BIGSERIAL PRIMARY KEY,
3
+ endpoint_url TEXT NOT NULL,
4
+ payload BYTEA NOT NULL,
5
+ state TEXT NOT NULL DEFAULT 'pending'
6
+ );
@@ -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,9 @@
1
+ version: "2"
2
+ sql:
3
+ - engine: postgresql
4
+ queries: internal/store/query.sql
5
+ schema: internal/store/schema.sql
6
+ gen:
7
+ go:
8
+ package: store
9
+ out: internal/store
@@ -0,0 +1,4 @@
1
+ # Required: the service refuses to start without it (see src/shipcalc/config.py)
2
+ SHIPCALC_API_KEY=replace-me
3
+ # Optional
4
+ SHIPCALC_LOG_LEVEL=info
@@ -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,4 @@
1
+ region,currency,base_rate,per_kg,free_threshold
2
+ EU,EUR,4.90,1.20,50.00
3
+ UK,GBP,3.95,1.10,40.00
4
+ US,USD,5.50,0.95,75.00
@@ -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])
@@ -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"))
@@ -0,0 +1,8 @@
1
+ from decimal import Decimal
2
+
3
+ LOYALTY_RATES = {"bronze": Decimal("0.00"), "silver": Decimal("0.03"), "gold": Decimal("0.05")}
4
+
5
+
6
+ def loyalty_discount(tier: str, subtotal: Decimal) -> Decimal:
7
+ rate = LOYALTY_RATES.get(tier, Decimal("0.00"))
8
+ return (subtotal * rate).quantize(Decimal("0.01"))
@@ -0,0 +1,8 @@
1
+ # DO NOT EDIT. This file is generated by scripts/gen_rates.py from data/rates.csv.
2
+ # Edit the CSV and run `make gen` instead. CI fails if this file is out of date.
3
+
4
+ RATES = {
5
+ "EU": {"currency": "EUR", "base_rate": 4.9, "per_kg": 1.2, "free_threshold": 50.0},
6
+ "UK": {"currency": "GBP", "base_rate": 3.95, "per_kg": 1.1, "free_threshold": 40.0},
7
+ "US": {"currency": "USD", "base_rate": 5.5, "per_kg": 0.95, "free_threshold": 75.0},
8
+ }
@@ -0,0 +1,8 @@
1
+ from fastapi import FastAPI
2
+
3
+ from shipcalc.api.routes import router
4
+ from shipcalc.config import load_settings
5
+
6
+ settings = load_settings()
7
+ app = FastAPI(title="shipcalc")
8
+ app.include_router(router)
@@ -0,0 +1,18 @@
1
+ """Shipping cost for a cart. Rates come from the generated table."""
2
+ import os
3
+ from decimal import Decimal, ROUND_HALF_UP
4
+
5
+ from shipcalc.generated.rates_table import RATES
6
+
7
+
8
+ def _free_shipping_enabled() -> bool:
9
+ # Rolled out per environment; off unless explicitly turned on.
10
+ return os.environ.get("SHIPCALC_FF_FREE_SHIPPING", "off") == "on"
11
+
12
+
13
+ def shipping_cost(region: str, subtotal: Decimal, weight_kg: Decimal) -> Decimal:
14
+ rate = RATES[region]
15
+ if _free_shipping_enabled() and subtotal >= Decimal(str(rate["free_threshold"])):
16
+ return Decimal("0.00")
17
+ cost = Decimal(str(rate["base_rate"])) + Decimal(str(rate["per_kg"])) * weight_kg
18
+ return cost.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
@@ -0,0 +1,7 @@
1
+ from decimal import Decimal
2
+
3
+ VAT = {"EU": Decimal("0.21"), "UK": Decimal("0.20"), "US": Decimal("0.00")}
4
+
5
+
6
+ def vat(region: str, taxable: Decimal) -> Decimal:
7
+ return (taxable * VAT[region]).quantize(Decimal("0.01"))
@@ -0,0 +1,3 @@
1
+ import os
2
+
3
+ os.environ.setdefault("SHIPCALC_API_KEY", "test-placeholder")
@@ -0,0 +1,11 @@
1
+ from decimal import Decimal
2
+
3
+ from shipcalc.discounts import loyalty_discount
4
+
5
+
6
+ def test_gold_gets_five_percent():
7
+ assert loyalty_discount("gold", Decimal("100.00")) == Decimal("5.00")
8
+
9
+
10
+ def test_unknown_tier_gets_nothing():
11
+ assert loyalty_discount("platinum", Decimal("100.00")) == Decimal("0.00")
@@ -0,0 +1,13 @@
1
+ from decimal import Decimal
2
+
3
+ import pytest
4
+
5
+ from shipcalc.tax import vat
6
+
7
+
8
+ @pytest.mark.parametrize(
9
+ "region,taxable,expected",
10
+ [("EU", Decimal("100.00"), Decimal("21.00")), ("UK", Decimal("100.00"), Decimal("20.00")), ("US", Decimal("100.00"), Decimal("0.00"))],
11
+ )
12
+ def test_vat_by_region(region, taxable, expected):
13
+ assert vat(region, taxable) == expected
@@ -0,0 +1,3 @@
1
+ # Copy to .env (read by `npm run dev:api`). Placeholder values only.
2
+ DATABASE_URL=postgres://REPLACE_ME@localhost:5432/cabinbook_dev
3
+ PORT=3000
@@ -0,0 +1,13 @@
1
+ name: CI
2
+ on: [push, pull_request]
3
+ jobs:
4
+ test:
5
+ runs-on: ubuntu-latest
6
+ steps:
7
+ - uses: actions/checkout@v4
8
+ - uses: actions/setup-node@v4
9
+ with:
10
+ node-version: 20
11
+ - run: npm install --no-audit --no-fund
12
+ - run: npm run gen:check
13
+ - run: npm test
@@ -0,0 +1,18 @@
1
+ # cabinbook
2
+
3
+ Holiday-cabin booking: the booking API and the web booking form.
4
+
5
+ ## Getting started
6
+
7
+ yarn install
8
+ cd api && yarn start
9
+
10
+ The API listens on http://localhost:3000.
11
+
12
+ ## Running tests
13
+
14
+ yarn test
15
+
16
+ ## Max stay
17
+
18
+ Bookings are limited to 14 nights; the limit lives in `api/config.js`.
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "cabinbook",
3
+ "private": true,
4
+ "workspaces": [
5
+ "packages/*"
6
+ ],
7
+ "scripts": {
8
+ "gen": "node scripts/gen-schema.mjs",
9
+ "gen:check": "node scripts/gen-schema.mjs --check",
10
+ "test": "npm test --workspaces",
11
+ "dev:api": "npm run dev -w @cabinbook/api"
12
+ }
13
+ }