dirigent-examples 0.15.0__py3-none-any.whl

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 (216) hide show
  1. dirigent_examples/__init__.py +22 -0
  2. dirigent_examples/py.typed +0 -0
  3. dirigent_examples/shelves/README.md +299 -0
  4. dirigent_examples/shelves/composition/README.md +18 -0
  5. dirigent_examples/shelves/composition/chained-instances.yaml +97 -0
  6. dirigent_examples/shelves/composition/composition-child.yaml +64 -0
  7. dirigent_examples/shelves/composition/composition-parent.yaml +89 -0
  8. dirigent_examples/shelves/connections.yaml +52 -0
  9. dirigent_examples/shelves/demo/README.md +19 -0
  10. dirigent_examples/shelves/demo/markdown-showcase.yaml +117 -0
  11. dirigent_examples/shelves/demo/params-showcase.yaml +92 -0
  12. dirigent_examples/shelves/demo/requires.yaml +65 -0
  13. dirigent_examples/shelves/demo/weekly-import-malawi.yaml +54 -0
  14. dirigent_examples/shelves/demo/weekly-import-nepal.yaml +75 -0
  15. dirigent_examples/shelves/docker/README.md +29 -0
  16. dirigent_examples/shelves/docker/docker-build-push.yaml +110 -0
  17. dirigent_examples/shelves/docker/docker-build-run.yaml +106 -0
  18. dirigent_examples/shelves/docker/docker-compose-database.yaml +124 -0
  19. dirigent_examples/shelves/docker/docker-compose-failing-up.yaml +83 -0
  20. dirigent_examples/shelves/docker/docker-compose-file.yaml +117 -0
  21. dirigent_examples/shelves/docker/docker-compose-profiles-env.yaml +133 -0
  22. dirigent_examples/shelves/docker/docker-compose-stack.yaml +70 -0
  23. dirigent_examples/shelves/docker/docker-hello.yaml +53 -0
  24. dirigent_examples/shelves/docker/docker-remote-daemon.yaml +92 -0
  25. dirigent_examples/shelves/docker/docker-run-failing-teardown.yaml +94 -0
  26. dirigent_examples/shelves/docker/docker-ticker.yaml +49 -0
  27. dirigent_examples/shelves/execute/README.md +16 -0
  28. dirigent_examples/shelves/execute/long-log.yaml +89 -0
  29. dirigent_examples/shelves/failure/README.md +20 -0
  30. dirigent_examples/shelves/failure/error-handler.yaml +89 -0
  31. dirigent_examples/shelves/failure/optional-step.yaml +82 -0
  32. dirigent_examples/shelves/failure/retries.yaml +75 -0
  33. dirigent_examples/shelves/failure/retry-budget.yaml +82 -0
  34. dirigent_examples/shelves/failure/step-timeout.yaml +96 -0
  35. dirigent_examples/shelves/git/README.md +32 -0
  36. dirigent_examples/shelves/git/git-checkout-build.yaml +125 -0
  37. dirigent_examples/shelves/git/git-checkout-compose.yaml +138 -0
  38. dirigent_examples/shelves/git/git-checkout-public.yaml +84 -0
  39. dirigent_examples/shelves/graph/README.md +22 -0
  40. dirigent_examples/shelves/graph/deep-chain.yaml +119 -0
  41. dirigent_examples/shelves/graph/fan-in.yaml +80 -0
  42. dirigent_examples/shelves/graph/fan-out.yaml +66 -0
  43. dirigent_examples/shelves/graph/linear.yaml +66 -0
  44. dirigent_examples/shelves/graph/parallel-branches.yaml +57 -0
  45. dirigent_examples/shelves/graph/parallel-sleep.yaml +55 -0
  46. dirigent_examples/shelves/graph/skip-diamond.yaml +105 -0
  47. dirigent_examples/shelves/graph/wide-fan.yaml +147 -0
  48. dirigent_examples/shelves/hello-world.yaml +30 -0
  49. dirigent_examples/shelves/open-data/README.md +67 -0
  50. dirigent_examples/shelves/open-data/feeds-composition.yaml +120 -0
  51. dirigent_examples/shelves/open-data/gdacs-disaster-updates.yaml +230 -0
  52. dirigent_examples/shelves/open-data/github-releases-relay.yaml +225 -0
  53. dirigent_examples/shelves/open-data/hdx-dataset-watch.yaml +211 -0
  54. dirigent_examples/shelves/open-data/kobo-submissions-to-csv.yaml +149 -0
  55. dirigent_examples/shelves/open-data/nominatim-geocode-facilities.yaml +169 -0
  56. dirigent_examples/shelves/open-data/odk-central-submissions.yaml +146 -0
  57. dirigent_examples/shelves/open-data/open-meteo-weekly-report.yaml +142 -0
  58. dirigent_examples/shelves/open-data/overpass-health-facilities.yaml +154 -0
  59. dirigent_examples/shelves/open-data/usgs-earthquakes-alert.yaml +208 -0
  60. dirigent_examples/shelves/open-data/who-gho-indicators-to-parquet.yaml +163 -0
  61. dirigent_examples/shelves/open-data/wikidata-country-reference.yaml +146 -0
  62. dirigent_examples/shelves/open-data/world-bank-population-trend.yaml +166 -0
  63. dirigent_examples/shelves/patterns/README.md +144 -0
  64. dirigent_examples/shelves/patterns/concurrency-queue.yaml +89 -0
  65. dirigent_examples/shelves/patterns/concurrency-replace.yaml +92 -0
  66. dirigent_examples/shelves/patterns/concurrency-skip.yaml +97 -0
  67. dirigent_examples/shelves/patterns/connections-referenced-vs-carried.yaml +140 -0
  68. dirigent_examples/shelves/patterns/deadline-on-a-sensor.yaml +106 -0
  69. dirigent_examples/shelves/patterns/fan-out-continue.yaml +88 -0
  70. dirigent_examples/shelves/patterns/fan-out-fail-fast.yaml +80 -0
  71. dirigent_examples/shelves/patterns/fan-out-from-params.yaml +84 -0
  72. dirigent_examples/shelves/patterns/fan-out-item-wise.yaml +111 -0
  73. dirigent_examples/shelves/patterns/fan-out-literal-list.yaml +81 -0
  74. dirigent_examples/shelves/patterns/fan-out-nested-objects.yaml +107 -0
  75. dirigent_examples/shelves/patterns/fan-out-then-join.yaml +86 -0
  76. dirigent_examples/shelves/patterns/log-levels.yaml +119 -0
  77. dirigent_examples/shelves/patterns/outputs-inline-vs-storage.yaml +140 -0
  78. dirigent_examples/shelves/patterns/params-every-type.yaml +259 -0
  79. dirigent_examples/shelves/patterns/params-validation-refuses.yaml +131 -0
  80. dirigent_examples/shelves/patterns/pipeline-run-child.yaml +96 -0
  81. dirigent_examples/shelves/patterns/pipeline-run-fire-and-forget.yaml +99 -0
  82. dirigent_examples/shelves/patterns/pipeline-run-strict.yaml +103 -0
  83. dirigent_examples/shelves/patterns/pipeline-run-wait.yaml +107 -0
  84. dirigent_examples/shelves/patterns/pipeline-run-with-params.yaml +124 -0
  85. dirigent_examples/shelves/patterns/poll-cadence.yaml +102 -0
  86. dirigent_examples/shelves/patterns/priority-layered.yaml +120 -0
  87. dirigent_examples/shelves/patterns/references-cheat-sheet.yaml +186 -0
  88. dirigent_examples/shelves/patterns/retry-budget-exhausted.yaml +92 -0
  89. dirigent_examples/shelves/patterns/retry-exponential-backoff.yaml +88 -0
  90. dirigent_examples/shelves/patterns/retry-only-transient.yaml +108 -0
  91. dirigent_examples/shelves/patterns/retry-with-jitter.yaml +102 -0
  92. dirigent_examples/shelves/patterns/rule-all-done.yaml +83 -0
  93. dirigent_examples/shelves/patterns/rule-all-success.yaml +79 -0
  94. dirigent_examples/shelves/patterns/rule-always.yaml +92 -0
  95. dirigent_examples/shelves/patterns/rule-one-failed.yaml +86 -0
  96. dirigent_examples/shelves/patterns/schedule-at-once.yaml +110 -0
  97. dirigent_examples/shelves/patterns/schedule-cron-timezone.yaml +121 -0
  98. dirigent_examples/shelves/patterns/schedule-interval.yaml +109 -0
  99. dirigent_examples/shelves/patterns/schedule-window-half-open.yaml +105 -0
  100. dirigent_examples/shelves/patterns/sensor-http-ready.yaml +121 -0
  101. dirigent_examples/shelves/patterns/sensor-storage-exists.yaml +134 -0
  102. dirigent_examples/shelves/patterns/step-names-and-keys.yaml +99 -0
  103. dirigent_examples/shelves/patterns/timeout-fails-the-step.yaml +94 -0
  104. dirigent_examples/shelves/patterns/timeout-skips-the-step.yaml +102 -0
  105. dirigent_examples/shelves/patterns/webhook-mapping-nested-payload.yaml +125 -0
  106. dirigent_examples/shelves/patterns/webhook-signed.yaml +144 -0
  107. dirigent_examples/shelves/preview/s3-parquet-to-ingestion.yaml +92 -0
  108. dirigent_examples/shelves/python/README.md +31 -0
  109. dirigent_examples/shelves/python/apply_and_run.py +52 -0
  110. dirigent_examples/shelves/python/ci_gate.py +76 -0
  111. dirigent_examples/shelves/python/connections.py +61 -0
  112. dirigent_examples/shelves/python/error_handling.py +84 -0
  113. dirigent_examples/shelves/python/follow_logs.py +39 -0
  114. dirigent_examples/shelves/python/list_and_filter.py +52 -0
  115. dirigent_examples/shelves/queues/README.md +59 -0
  116. dirigent_examples/shelves/queues/kafka-consume-then-transform.yaml +105 -0
  117. dirigent_examples/shelves/queues/kafka-produce-then-consume.yaml +124 -0
  118. dirigent_examples/shelves/queues/rabbitmq-consume-ack-on-success.yaml +102 -0
  119. dirigent_examples/shelves/queues/report-to-kafka.yaml +105 -0
  120. dirigent_examples/shelves/queues/report-to-rabbitmq.yaml +105 -0
  121. dirigent_examples/shelves/recipes/README.md +130 -0
  122. dirigent_examples/shelves/recipes/csv-header-rules.yaml +147 -0
  123. dirigent_examples/shelves/recipes/csv-to-ndjson.yaml +107 -0
  124. dirigent_examples/shelves/recipes/etl-csv-clean-validate-parquet.yaml +207 -0
  125. dirigent_examples/shelves/recipes/filter-by-predicate.yaml +116 -0
  126. dirigent_examples/shelves/recipes/filter-then-map-then-reduce.yaml +109 -0
  127. dirigent_examples/shelves/recipes/http-fetch-validate-post.yaml +142 -0
  128. dirigent_examples/shelves/recipes/http-follow-redirects.yaml +100 -0
  129. dirigent_examples/shelves/recipes/http-get-with-query.yaml +96 -0
  130. dirigent_examples/shelves/recipes/http-headers-and-auth-connection.yaml +110 -0
  131. dirigent_examples/shelves/recipes/http-post-file-from-storage.yaml +117 -0
  132. dirigent_examples/shelves/recipes/http-post-json-echo.yaml +105 -0
  133. dirigent_examples/shelves/recipes/http-post-report.yaml +183 -0
  134. dirigent_examples/shelves/recipes/http-save-body-to-storage.yaml +106 -0
  135. dirigent_examples/shelves/recipes/http-success-status-list.yaml +80 -0
  136. dirigent_examples/shelves/recipes/http-timeout-override.yaml +104 -0
  137. dirigent_examples/shelves/recipes/jq-dedupe-by-key.yaml +78 -0
  138. dirigent_examples/shelves/recipes/jq-defaults-and-nulls.yaml +91 -0
  139. dirigent_examples/shelves/recipes/jq-group-by-and-sum.yaml +74 -0
  140. dirigent_examples/shelves/recipes/jq-join-two-lists.yaml +77 -0
  141. dirigent_examples/shelves/recipes/jq-long-to-wide.yaml +76 -0
  142. dirigent_examples/shelves/recipes/jq-nested-to-flat.yaml +89 -0
  143. dirigent_examples/shelves/recipes/jq-pivot-wide-to-long.yaml +65 -0
  144. dirigent_examples/shelves/recipes/jq-running-totals.yaml +82 -0
  145. dirigent_examples/shelves/recipes/jq-string-cleaning.yaml +88 -0
  146. dirigent_examples/shelves/recipes/jq-top-n.yaml +85 -0
  147. dirigent_examples/shelves/recipes/jq-validate-in-jq-vs-schema.yaml +124 -0
  148. dirigent_examples/shelves/recipes/jq-window-dates.yaml +82 -0
  149. dirigent_examples/shelves/recipes/json-to-csv-flattening.yaml +141 -0
  150. dirigent_examples/shelves/recipes/large-output-to-storage.yaml +134 -0
  151. dirigent_examples/shelves/recipes/map-enrich-with-lookup.yaml +96 -0
  152. dirigent_examples/shelves/recipes/ndjson-to-parquet.yaml +130 -0
  153. dirigent_examples/shelves/recipes/pagination-by-fan-out.yaml +115 -0
  154. dirigent_examples/shelves/recipes/parquet-round-trip-types.yaml +163 -0
  155. dirigent_examples/shelves/recipes/reconcile-two-sources.yaml +159 -0
  156. dirigent_examples/shelves/recipes/report-built-in.yaml +72 -0
  157. dirigent_examples/shelves/recipes/report-daily-digest.yaml +186 -0
  158. dirigent_examples/shelves/recipes/report-to-file.yaml +131 -0
  159. dirigent_examples/shelves/recipes/report-to-webhook.yaml +136 -0
  160. dirigent_examples/shelves/recipes/schema-carried.yaml +112 -0
  161. dirigent_examples/shelves/recipes/schema-formats.yaml +107 -0
  162. dirigent_examples/shelves/recipes/schema-referenced.yaml +86 -0
  163. dirigent_examples/shelves/recipes/schema-refuses-then-rule.yaml +127 -0
  164. dirigent_examples/shelves/recipes/storage-copy-dated-archive.yaml +114 -0
  165. dirigent_examples/shelves/recipes/storage-exists-gate.yaml +127 -0
  166. dirigent_examples/shelves/recipes/storage-manifest-of-a-fan-out.yaml +104 -0
  167. dirigent_examples/shelves/recipes/storage-write-then-read.yaml +119 -0
  168. dirigent_examples/shelves/recipes/webhook-post-hmac.yaml +132 -0
  169. dirigent_examples/shelves/recipes/webhook-post-summary.yaml +142 -0
  170. dirigent_examples/shelves/s3/README.md +34 -0
  171. dirigent_examples/shelves/s3/report-to-s3.yaml +93 -0
  172. dirigent_examples/shelves/s3/s3-copy-and-verify.yaml +105 -0
  173. dirigent_examples/shelves/s3/s3-csv-report.yaml +87 -0
  174. dirigent_examples/shelves/s3/s3-parquet-report.yaml +77 -0
  175. dirigent_examples/shelves/s3/s3-round-trip.yaml +125 -0
  176. dirigent_examples/shelves/schemas/README.md +36 -0
  177. dirigent_examples/shelves/schemas/echo-reading.json +18 -0
  178. dirigent_examples/shelves/schemas/ou-record.json +13 -0
  179. dirigent_examples/shelves/schemas/station-reading.json +13 -0
  180. dirigent_examples/shelves/sensors/README.md +16 -0
  181. dirigent_examples/shelves/sensors/sensor-gate.yaml +65 -0
  182. dirigent_examples/shelves/sensors/time-window.yaml +61 -0
  183. dirigent_examples/shelves/sql/README.md +52 -0
  184. dirigent_examples/shelves/sql/duckdb-parquet-to-report.yaml +146 -0
  185. dirigent_examples/shelves/sql/sql-postgres-readonly.yaml +111 -0
  186. dirigent_examples/shelves/sql/sql-query-to-storage.yaml +85 -0
  187. dirigent_examples/shelves/sql/sql-sqlite-roundtrip.yaml +114 -0
  188. dirigent_examples/shelves/sql/warehouse.sql +42 -0
  189. dirigent_examples/shelves/transform/README.md +36 -0
  190. dirigent_examples/shelves/transform/csv-report.yaml +55 -0
  191. dirigent_examples/shelves/transform/jq-filter-and-map.yaml +70 -0
  192. dirigent_examples/shelves/transform/jq-group-and-aggregate.yaml +70 -0
  193. dirigent_examples/shelves/transform/jq-join-two-sources.yaml +98 -0
  194. dirigent_examples/shelves/transform/jq-reshape.yaml +91 -0
  195. dirigent_examples/shelves/transform/jq-stream-through-storage.yaml +112 -0
  196. dirigent_examples/shelves/transform/ndjson-round-trip.yaml +56 -0
  197. dirigent_examples/shelves/transform/parquet-round-trip.yaml +68 -0
  198. dirigent_examples/shelves/transform/std-convert-fan-out.yaml +142 -0
  199. dirigent_examples/shelves/transform/xml-feed-to-ndjson.yaml +116 -0
  200. dirigent_examples/shelves/transform/yaml-config-to-json.yaml +104 -0
  201. dirigent_examples/shelves/triggers/README.md +45 -0
  202. dirigent_examples/shelves/triggers/at-one-time.yaml +78 -0
  203. dirigent_examples/shelves/triggers/cron-nightly.yaml +79 -0
  204. dirigent_examples/shelves/triggers/cron-windowed.yaml +86 -0
  205. dirigent_examples/shelves/triggers/document-nightly.yaml +80 -0
  206. dirigent_examples/shelves/triggers/interval-rolling.yaml +88 -0
  207. dirigent_examples/shelves/triggers/managed-and-manual.yaml +109 -0
  208. dirigent_examples/shelves/triggers/webhook-trigger.yaml +75 -0
  209. dirigent_examples/shelves/validate/README.md +31 -0
  210. dirigent_examples/shelves/validate/expects-a-shape.yaml +56 -0
  211. dirigent_examples/shelves/validate/the-shape-is-wrong.yaml +46 -0
  212. dirigent_examples-0.15.0.dist-info/METADATA +21 -0
  213. dirigent_examples-0.15.0.dist-info/RECORD +216 -0
  214. dirigent_examples-0.15.0.dist-info/WHEEL +4 -0
  215. dirigent_examples-0.15.0.dist-info/entry_points.txt +3 -0
  216. dirigent_examples-0.15.0.dist-info/licenses/LICENSE +18 -0
@@ -0,0 +1,104 @@
1
+ # One file per item, then one manifest listing all of them.
2
+ #
3
+ # In: a list of regions, as a parameter.
4
+ # Out: one JSON file per region in the run's scratch space, and a manifest naming every file
5
+ # with its byte count.
6
+ #
7
+ # A fan-out is `for_each` on a step: the run gets one item per element, each with its own
8
+ # status, error, retry budget and output. Two things about it shape this recipe:
9
+ #
10
+ # for_each is expanded when the run is created, so it reads params.*, run.*, and another
11
+ # fan-out's grid as ${steps.<step>.items} -- never a step's output. That is why the regions
12
+ # are a parameter here and not the result of an upstream read: the item grid exists from the
13
+ # moment the run is visible.
14
+ # A fan-out step's output is the list of its items' outputs, in item order. So the step
15
+ # after the fan reads the whole batch at once, and it reads ${steps.<step>.output} --
16
+ # the list -- rather than ${steps.<step>.output.uri}, which is a field of one item's
17
+ # output and not of the list.
18
+ #
19
+ # The fanned step is the storage.write itself, which is the shortest form when one step is
20
+ # doing the work and the writing. Where they are two steps, the second maps over the first's
21
+ # grid with for_each: ${steps.<step>.items} and reads its match -- that is
22
+ # examples/patterns/fan-out-item-wise.yaml.
23
+ #
24
+ # ${item} is the element, and it goes into the target as well as into the value, which is
25
+ # what keeps two items from writing the same key. Items run concurrently, so a shared target
26
+ # would be a race with a winner nobody chose.
27
+ #
28
+ # The manifest is the point. A fan-out that writes twelve files and reports nothing leaves
29
+ # "did region nine actually land" to somebody listing a bucket by hand; a manifest step
30
+ # turns the run itself into the answer, and it costs one jq program over outputs the engine
31
+ # already collected.
32
+ #
33
+ # items: continue is the other half of the same idea -- under it one bad region does not
34
+ # fail the step, the failures are recorded against their own items, and the run reports
35
+ # completed_with_errors. The manifest then lists what did land, which is exactly what a
36
+ # partial run needs to say.
37
+ #
38
+ # To change it: -p regions='["east","west"]' fans two ways instead of four.
39
+ #
40
+ # dg run --local examples/recipes/storage-manifest-of-a-fan-out.yaml
41
+ # dg run --local examples/recipes/storage-manifest-of-a-fan-out.yaml -p regions='["east"]'
42
+
43
+ format: dirigent/v1
44
+ kind: pipeline
45
+ code: storage-manifest-of-a-fan-out
46
+ name: A manifest of a fan-out
47
+ description: Write one file per item with for_each, then read the fan's outputs back as one list and record every file in a manifest.
48
+
49
+ tags: [recipes, storage, transform, graph]
50
+
51
+ requires:
52
+ blocks:
53
+ - storage.write
54
+ - transform.jq
55
+
56
+ params:
57
+ type: object
58
+ properties:
59
+ regions:
60
+ type: array
61
+ default: [east, west, north, south]
62
+ items:
63
+ type: string
64
+ description: One item, and one file, per element.
65
+ day:
66
+ type: string
67
+ format: date
68
+ default: "2026-01-01"
69
+ description: The day the files are named for.
70
+
71
+ steps:
72
+ per_region:
73
+ block: storage.write
74
+ for_each: ${params.regions}
75
+ # items: continue would let one bad region be recorded against its own item instead of
76
+ # failing the step; the default, fail_fast, is right when a missing region is an outage.
77
+ items: fail_fast
78
+ config:
79
+ # The item is in the key, so two items never write the same file.
80
+ target: ${run.scratch}/regions/${params.day}/${item}.json
81
+ value:
82
+ region: ${item}
83
+ day: ${params.day}
84
+ rows:
85
+ - { station: "${item}-st-0", celsius: -4 }
86
+ - { station: "${item}-st-1", celsius: -1 }
87
+ - { station: "${item}-st-2", celsius: 2 }
88
+
89
+ manifest:
90
+ block: transform.jq
91
+ depends_on: [per_region]
92
+ config:
93
+ # The fan-out's output is the list of its items' outputs, so this reads .output and
94
+ # not .output.uri.
95
+ input:
96
+ items: ${steps.per_region.output}
97
+ day: ${params.day}
98
+ program: |
99
+ . as {$items, $day}
100
+ | {day: $day,
101
+ files: ($items | length),
102
+ total_bytes: ([$items[].bytes_written] | add),
103
+ entries: [$items[] | {uri, bytes: .bytes_written}],
104
+ empty: [$items[] | select(.bytes_written == 0) | .uri]}
@@ -0,0 +1,119 @@
1
+ # Hand work from one step to the next through storage, which is how a big payload travels.
2
+ #
3
+ # In: a list built inline.
4
+ # Out: two files in the run's scratch space and a summary read back out of the second one.
5
+ #
6
+ # A value flows through step outputs. storage.write is the only way one goes out and
7
+ # storage.read the only way one comes back in, so a chain that wants to hand work along
8
+ # through storage is written as that pair, twice:
9
+ #
10
+ # storage.write target, and exactly one of text or value. Output: uri, bytes_written,
11
+ # content_type. A value is written as canonical JSON -- sorted keys, no
12
+ # spaces -- so the same value writes the same bytes every run.
13
+ # storage.read source, and max_size: how much of the object this step will hold, 1mb
14
+ # unless it says otherwise. An object above the cap is refused rather than
15
+ # truncated, because half a document is not a smaller one, it is a wrong
16
+ # one. The object comes back as `value` when it is JSON and as `text`
17
+ # otherwise, and a type it cannot decode it refuses instead of guessing.
18
+ #
19
+ # ${run.scratch} is the run's own prefix in whatever storage the instance is configured
20
+ # with. On a --local run that is a directory under a temporary tree that is deleted when the
21
+ # run ends; on a server it is the artifact root, and with an s3 connection configured for
22
+ # the scheme it is a bucket. The document says none of that: it names a scheme-agnostic
23
+ # prefix, and where the bytes land is an instance setting.
24
+ #
25
+ # Every read below is spelled ${steps.<step>.output.uri} rather than the same path typed
26
+ # twice: one place decides where a file went, and a rename is a one-line change.
27
+ #
28
+ # To change it: point the targets at s3://<bucket>/... and nothing else in the document
29
+ # moves.
30
+ #
31
+ # dg run --local examples/recipes/storage-write-then-read.yaml
32
+ # dg run --local examples/recipes/storage-write-then-read.yaml -p day=2026-02-01
33
+
34
+ format: dirigent/v1
35
+ kind: pipeline
36
+ code: storage-write-then-read
37
+ name: Write to storage, then read it back
38
+ description: Put a transform's result in the run's scratch space with storage.write, read it into the next step with storage.read, and do it again with what that step produced.
39
+
40
+ tags: [recipes, storage, transform, starter]
41
+
42
+ requires:
43
+ blocks:
44
+ - transform.jq
45
+ - storage.write
46
+ - storage.read
47
+
48
+ params:
49
+ type: object
50
+ properties:
51
+ day:
52
+ type: string
53
+ format: date
54
+ default: "2026-01-01"
55
+ description: The day the written files are named for.
56
+ stations:
57
+ type: integer
58
+ minimum: 1
59
+ default: 50
60
+ description: How many rows are generated.
61
+
62
+ steps:
63
+ generate:
64
+ block: transform.jq
65
+ config:
66
+ input:
67
+ count: ${params.stations}
68
+ day: ${params.day}
69
+ program: |
70
+ . as {$count, $day}
71
+ | [range($count)
72
+ | {station: "st-\(.)", day: $day,
73
+ celsius: (. % 17) - 5,
74
+ active: (. % 5 != 0)}]
75
+
76
+ save:
77
+ block: storage.write
78
+ depends_on: [generate]
79
+ config:
80
+ target: ${run.scratch}/readings-${params.day}.json
81
+ value: ${steps.generate.output.value}
82
+
83
+ back:
84
+ block: storage.read
85
+ depends_on: [save]
86
+ config:
87
+ # Where the write said the object landed, not the same path typed twice.
88
+ source: ${steps.save.output.uri}
89
+ max_size: 8mb
90
+
91
+ cold:
92
+ block: transform.jq
93
+ depends_on: [back]
94
+ config:
95
+ # The rows as they came off storage, not as they were held in memory upstream.
96
+ input: ${steps.back.output.value}
97
+ program: |
98
+ [.[] | select(.celsius < 0)]
99
+
100
+ save_cold:
101
+ block: storage.write
102
+ depends_on: [cold]
103
+ config:
104
+ target: ${run.scratch}/cold-${params.day}.json
105
+ value: ${steps.cold.output.value}
106
+
107
+ back_cold:
108
+ block: storage.read
109
+ depends_on: [save_cold]
110
+ config:
111
+ source: ${steps.save_cold.output.uri}
112
+
113
+ summary:
114
+ block: transform.jq
115
+ depends_on: [back_cold]
116
+ config:
117
+ input: ${steps.back_cold.output.value}
118
+ program: |
119
+ {cold_rows: length, coldest: (min_by(.celsius) | .station)}
@@ -0,0 +1,132 @@
1
+ # A signed webhook: what gets signed, with what, and what the receiver checks.
2
+ #
3
+ # In: a small event built inline.
4
+ # Out: the echo service's view of the delivery, including the X-Dirigent-Signature header,
5
+ # and the step's own `signed: true`.
6
+ #
7
+ # webhook.post serialises the body itself and signs the bytes that go on the wire. That
8
+ # order is the entire security property: if the body were handed to an HTTP client that
9
+ # re-encoded it -- a different key order, different spacing -- the signature would cover
10
+ # bytes the receiver never sees, and every delivery would fail verification for a reason
11
+ # nobody could find.
12
+ #
13
+ # The signature is HMAC-SHA256 over the raw body, hex, in X-Dirigent-Signature. A receiver
14
+ # verifies by computing the same HMAC over the body exactly as it arrived -- before parsing,
15
+ # never after re-serialising -- and comparing in constant time. Dirigent's own inbound
16
+ # webhook trigger does precisely that, so an instance can hand work to another instance with
17
+ # nothing shared but the secret.
18
+ #
19
+ # sign_with names a connection, not a secret. The secret is that connection's hmac_secret, a
20
+ # SecretStr sealed at rest and redacted in every response, and a connection without one
21
+ # fails the step saying so rather than sending an unsigned POST that looks signed.
22
+ #
23
+ # The connection is carried inline here, labelled, so the recipe runs standalone -- the
24
+ # secret below is a demo string in a public file and protects nothing. On an instance it is
25
+ # created once and the document names its code.
26
+ #
27
+ # A signature is not a substitute for TLS, and it is not a replay defence. It says the body
28
+ # came from someone holding the secret; it says nothing about when. A receiver that cares
29
+ # about replay checks a timestamp or an id it has seen before, which is why the body carries
30
+ # both.
31
+ #
32
+ # The same body is delivered twice, once with sign_with and once without, through the same
33
+ # connection: signing is the step's decision, and the output shows what the second delivery
34
+ # is missing.
35
+ #
36
+ # To change it: point the connection's base_url at a receiver you own and give it the same
37
+ # hmac_secret; nothing in the steps changes.
38
+ #
39
+ # dg run --local examples/recipes/webhook-post-hmac.yaml
40
+ # dg run --local examples/recipes/webhook-post-hmac.yaml -p day=2026-02-01
41
+
42
+ format: dirigent/v1
43
+ kind: pipeline
44
+ code: webhook-post-hmac
45
+ name: A signed webhook delivery
46
+ description: POST an event with an HMAC-SHA256 signature over the exact bytes sent, using a secret that lives on a connection.
47
+
48
+ tags: [recipes, transform, webhook]
49
+
50
+ requires:
51
+ blocks:
52
+ - transform.jq
53
+ - webhook.post
54
+
55
+ # Carried so the recipe runs standalone. hmac_secret is a SecretStr; this one is a demo
56
+ # string in a public file and protects nothing.
57
+ connections:
58
+ echo-webhook:
59
+ kind: http
60
+ config:
61
+ base_url: https://postman-echo.com
62
+ hmac_secret: a shared secret between two dirigent instances
63
+ timeout: 30s
64
+ health_path: /get
65
+
66
+ params:
67
+ type: object
68
+ properties:
69
+ day:
70
+ type: string
71
+ format: date
72
+ default: "2026-01-01"
73
+ description: The day the event reports on.
74
+
75
+ steps:
76
+ event:
77
+ block: transform.jq
78
+ config:
79
+ input:
80
+ day: ${params.day}
81
+ program: |
82
+ {kind: "import.finished",
83
+ day: .day,
84
+ # A receiver that cares about replay needs an identity and a time; a signature
85
+ # gives it neither.
86
+ event_id: "import-\(.day)",
87
+ at: "\(.day)T06:15:00Z",
88
+ rows: 1440}
89
+
90
+ deliver:
91
+ block: webhook.post
92
+ depends_on: [event]
93
+ config:
94
+ connection: echo-webhook
95
+ path: /post
96
+ body: ${steps.event.output.value}
97
+ # The connection whose hmac_secret signs the bytes. Unset means an unsigned POST.
98
+ sign_with: echo-webhook
99
+ headers:
100
+ x-event-kind: import.finished
101
+
102
+ deliver_unsigned:
103
+ block: webhook.post
104
+ depends_on: [event]
105
+ config:
106
+ connection: echo-webhook
107
+ path: /post
108
+ body: ${steps.event.output.value}
109
+ # No sign_with, and the same connection: signing is the step's decision, not the
110
+ # connection's.
111
+ headers:
112
+ x-event-kind: import.finished
113
+
114
+ receipt:
115
+ block: transform.jq
116
+ depends_on: [deliver, deliver_unsigned]
117
+ config:
118
+ input:
119
+ status: ${steps.deliver.output.status}
120
+ signed: ${steps.deliver.output.signed}
121
+ echoed: ${steps.deliver.output.json_body}
122
+ unsigned_signed: ${steps.deliver_unsigned.output.signed}
123
+ unsigned_echoed: ${steps.deliver_unsigned.output.json_body}
124
+ program: |
125
+ {status, signed,
126
+ # The echo service shows what arrived: the signature header, and the body it
127
+ # parsed out of the exact bytes that were signed.
128
+ signature: (.echoed.headers["x-dirigent-signature"] // null),
129
+ delivered_event: .echoed.json.event_id,
130
+ content_type: .echoed.headers["content-type"],
131
+ unsigned: {signed: .unsigned_signed,
132
+ signature: (.unsigned_echoed.headers["x-dirigent-signature"] // null)}}
@@ -0,0 +1,142 @@
1
+ # Tell somebody what a run did, in a body they can act on rather than a sentence.
2
+ #
3
+ # In: a batch of load results, some of them failures.
4
+ # Out: a notification POSTed to a receiver, and its answer -- with the summary built as
5
+ # structure and the human sentence derived from it, not the other way round.
6
+ #
7
+ # A notification body is an interface, so this recipe builds one rather than formatting a
8
+ # message:
9
+ #
10
+ # Counts before prose. rows, loaded, rejected and a boolean the receiver can route on.
11
+ # The identifiers to act with: the run id, the pipeline's day, and the rejected keys --
12
+ # not "3 records failed", which nobody can look up.
13
+ # A `text` field derived from the numbers, for a chat client that renders one line. It is
14
+ # built last, from the same object, so it can never disagree with the counts beside it.
15
+ # A severity the receiver can route on, decided here where the meaning of the numbers is
16
+ # known, rather than by a rule written in the receiver's config.
17
+ #
18
+ # webhook.post is the block for this rather than http.request, because a notification is a
19
+ # delivery: it takes sign_with when the receiver verifies signatures, it caps the response
20
+ # it reads at 1mb because nobody wants a receiver's HTML error page in a step output, and
21
+ # success_status is how a receiver that answers 202 is accommodated.
22
+ #
23
+ # The delivery is a step in the DAG like any other, so a rule decides when it runs. This one
24
+ # is `all_done`, so the notification is sent whether the batch succeeded or not -- a
25
+ # notifier that only fires on success is one that goes quiet exactly when it matters.
26
+ #
27
+ # To change it: -p failures=0 sends the green version, and severity, text and routing all
28
+ # follow from the numbers without another branch in the document.
29
+ #
30
+ # dg run --local examples/recipes/webhook-post-summary.yaml
31
+ # dg run --local examples/recipes/webhook-post-summary.yaml -p failures=0
32
+
33
+ format: dirigent/v1
34
+ kind: pipeline
35
+ code: webhook-post-summary
36
+ name: A run summary as a webhook
37
+ description: Build a notification body of counts and identifiers, derive its human line from those counts, and deliver it on all_done.
38
+
39
+ tags: [recipes, transform, webhook]
40
+
41
+ requires:
42
+ blocks:
43
+ - transform.jq
44
+ - webhook.post
45
+
46
+ connections:
47
+ echo-webhook:
48
+ kind: http
49
+ config:
50
+ base_url: https://postman-echo.com
51
+ timeout: 30s
52
+ health_path: /get
53
+
54
+ params:
55
+ type: object
56
+ properties:
57
+ day:
58
+ type: string
59
+ format: date
60
+ default: "2026-01-01"
61
+ description: The day the load reports on.
62
+ rows:
63
+ type: integer
64
+ minimum: 1
65
+ default: 12
66
+ description: How many records the load handled.
67
+ failures:
68
+ type: integer
69
+ minimum: 0
70
+ default: 3
71
+ description: How many of them were rejected; 0 sends the green version.
72
+
73
+ steps:
74
+ results:
75
+ block: transform.jq
76
+ config:
77
+ input:
78
+ rows: ${params.rows}
79
+ failures: ${params.failures}
80
+ day: ${params.day}
81
+ program: |
82
+ . as {$rows, $failures, $day}
83
+ | [range($rows)
84
+ | {key: "\($day)/rec-\(.)",
85
+ status: (if . < $failures then "rejected" else "loaded" end),
86
+ reason: (if . < $failures then "period is outside the data set" else null end)}]
87
+
88
+ summary:
89
+ block: transform.jq
90
+ depends_on: [results]
91
+ config:
92
+ input:
93
+ rows: ${steps.results.output.value}
94
+ day: ${params.day}
95
+ run: ${run.id}
96
+ program: |
97
+ . as {$rows, $day, $run}
98
+ | {kind: "load.finished",
99
+ day: $day,
100
+ run: $run,
101
+ rows: ($rows | length),
102
+ loaded: ([$rows[] | select(.status == "loaded")] | length),
103
+ rejected: ([$rows[] | select(.status == "rejected")] | length),
104
+ # The keys, so the receiver can look them up; not a count of them, which nobody
105
+ # can act on.
106
+ rejected_keys: [$rows[] | select(.status == "rejected") | .key]}
107
+ | . + {clean: (.rejected == 0),
108
+ severity: (if .rejected == 0 then "info"
109
+ elif .rejected < (.rows / 2) then "warning"
110
+ else "error" end)}
111
+ # The sentence is derived from the counts, last, so it cannot contradict them.
112
+ | . + {text: "\(.day): \(.loaded)/\(.rows) loaded, \(.rejected) rejected"}
113
+
114
+ notify:
115
+ block: webhook.post
116
+ depends_on: [summary]
117
+ # Sent whether the work above passed or failed: a notifier that only fires on success
118
+ # goes quiet exactly when somebody needs it.
119
+ rule: all_done
120
+ config:
121
+ connection: echo-webhook
122
+ path: /post
123
+ body: ${steps.summary.output.value}
124
+ headers:
125
+ # Routing the receiver can dispatch on without parsing the body.
126
+ x-severity: ${steps.summary.output.value.severity}
127
+ # A receiver that queues rather than processes answers 202; both count as delivered.
128
+ success_status: [200, 202, 204]
129
+
130
+ delivered:
131
+ block: transform.jq
132
+ depends_on: [notify]
133
+ config:
134
+ input:
135
+ status: ${steps.notify.output.status}
136
+ signed: ${steps.notify.output.signed}
137
+ echoed: ${steps.notify.output.json_body}
138
+ program: |
139
+ {status, signed,
140
+ severity: .echoed.headers["x-severity"],
141
+ text: .echoed.json.text,
142
+ rejected_keys: .echoed.json.rejected_keys}
@@ -0,0 +1,34 @@
1
+ # S3 examples
2
+
3
+ Object storage without an S3 block anywhere: `s3://` is a registered URI scheme, so the
4
+ ordinary storage blocks address it the way they address anything else,
5
+ and every document here declares `requires: {storage: [s3]}` so an instance without a
6
+ backend refuses it before storing it.
7
+
8
+ All five name one connection, `artifacts`, and it is the code the
9
+ [compose stack](../../docs/operations.md) already bootstraps for its own artifact bucket. So
10
+ on the stack there is nothing to set up: apply and run, naming the stack's bucket.
11
+
12
+ ```bash
13
+ dg run s3-round-trip -p day=2026-01-01 -p bucket=dirigent --watch
14
+ ```
15
+
16
+ Under `dg dev` there is no S3 server and no connection, and
17
+ [s3-round-trip.yaml](s3-round-trip.yaml) documents both: the rustfs container to start, the
18
+ `artifacts` connection to create, and the `DIRIGENT_STORAGE_CONNECTIONS` line that makes it
19
+ serve the scheme.
20
+
21
+ ```bash
22
+ dg apply examples/s3/s3-round-trip.yaml
23
+ dg run s3-round-trip -p day=2026-01-01 --watch
24
+ ```
25
+
26
+ ## Pipelines
27
+
28
+ | File | What it teaches |
29
+ | --- | --- |
30
+ | [s3-round-trip.yaml](s3-round-trip.yaml) | The whole seam: an artifact out to a bucket and back, moved by the ordinary storage blocks. |
31
+ | [s3-copy-and-verify.yaml](s3-copy-and-verify.yaml) | Promote-then-consume: a copy between prefixes, and `storage.exists` standing between the copy and the reader. |
32
+ | [s3-csv-report.yaml](s3-csv-report.yaml) | A report delivered to a bucket: the csv-report chain with the converter's `target` one word away from scratch. |
33
+ | [report-to-s3.yaml](report-to-s3.yaml) | A rendered markdown page delivered to a bucket: `report.render` hands its text on and `storage.write` puts it in the object, content type and all. |
34
+ | [s3-parquet-report.yaml](s3-parquet-report.yaml) | A typed parquet dataset written straight to a bucket, for an analysis to open (needs `dirigent-parquet`). |
@@ -0,0 +1,93 @@
1
+ # The same rendered page as examples/recipes/report-to-file.yaml, delivered to a bucket.
2
+ #
3
+ # NEEDS AN S3-COMPATIBLE ENDPOINT, set up exactly as s3-round-trip.yaml documents: the same
4
+ # rustfs container, the same artifacts connection, the same DIRIGENT_STORAGE_CONNECTIONS.
5
+ #
6
+ # Three hops, and what each one hands on:
7
+ # summary transform.jq, the numbers the page is about.
8
+ # page report.render, the markdown. Output: text, content_type, text_bytes.
9
+ # deliver storage.write, the text to an s3:// URI. Output: uri, bytes_written,
10
+ # content_type -- and on S3 the content type is stored with the object, so a
11
+ # browser opening the link is told what it is.
12
+ #
13
+ # THE RENDER DOES NOT KNOW WHERE THE PAGE GOES. It renders text and hands it on; only the
14
+ # step below it names a destination, and changing s3:// to file:// is the whole difference
15
+ # between this document and the recipe it was copied from. That is what schemes are for.
16
+ #
17
+ # dg apply examples/s3/report-to-s3.yaml
18
+ # dg run report-to-s3 -p day=2026-01-01 --watch
19
+ #
20
+ # The page is at s3://<bucket>/reports/<day>.md when the run settles.
21
+
22
+ format: dirigent/v1
23
+ kind: pipeline
24
+ code: report-to-s3
25
+ name: A rendered report delivered to a bucket
26
+ description: Render a markdown page from a Jinja template and write it to an object in S3.
27
+
28
+ tags: [s3, report, storage, transform]
29
+
30
+ requires:
31
+ blocks:
32
+ - transform.jq
33
+ - report.render
34
+ - storage.write
35
+ connections:
36
+ - artifacts
37
+ storage:
38
+ - s3
39
+
40
+ params:
41
+ type: object
42
+ required: [day]
43
+ additionalProperties: false
44
+ properties:
45
+ day:
46
+ type: string
47
+ format: date
48
+ description: The day the report is about, and the object it is named as.
49
+ bucket:
50
+ type: string
51
+ default: dirigent-archive
52
+ description: The bucket the page is delivered into.
53
+
54
+ steps:
55
+ summary:
56
+ block: transform.jq
57
+ config:
58
+ input:
59
+ day: ${params.day}
60
+ readings:
61
+ - {station: st-1, region: east, celsius: 4.5}
62
+ - {station: st-3, region: west, celsius: 1.2}
63
+ - {station: st-4, region: north, celsius: -3.4}
64
+ program: |
65
+ {day: .day,
66
+ stations: (.readings | length),
67
+ coldest: (.readings | min_by(.celsius) | .station),
68
+ rows: .readings}
69
+
70
+ page:
71
+ block: report.render
72
+ depends_on: [summary]
73
+ config:
74
+ values:
75
+ summary: ${steps.summary.output.value}
76
+ content_type: text/markdown
77
+ template: |
78
+ # Readings for {{ summary.day }}
79
+
80
+ {{ summary.stations }} stations reported; the coldest was {{ summary.coldest }}.
81
+
82
+ {% for row in summary.rows %}
83
+ - **{{ row.station }}** ({{ row.region }}): {{ row.celsius }}C
84
+ {% endfor %}
85
+
86
+ deliver:
87
+ block: storage.write
88
+ depends_on: [page]
89
+ config:
90
+ target: s3://${params.bucket}/reports/${params.day}.md
91
+ text: ${steps.page.output.text}
92
+ # Stored on the object, so whoever fetches it is told it is markdown.
93
+ content_type: ${steps.page.output.content_type}