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,112 @@
1
+ # A shape written into the document itself, so the pipeline runs anywhere with nothing to
2
+ # hand it.
3
+ #
4
+ # In: a payload shaped like an API read -- an envelope around a list of org units.
5
+ # Out: the same payload, passed through the gate unchanged, and a count taken from the
6
+ # gate's output rather than from the step before it.
7
+ #
8
+ # A validate.schema gate never writes a shape inline in its config: its schema field is a
9
+ # code, and the code resolves either to a schema the instance holds or to one this document
10
+ # carries in its top-level schemas: section. This recipe is the carried half; its pair,
11
+ # schema-referenced.yaml, is the same check written the other way.
12
+ #
13
+ # Carried is the right choice when the shape belongs to this pipeline and nothing else, and
14
+ # when the document has to run somewhere that holds nothing -- a --local run, a colleague's
15
+ # checkout, a CI lane. The cost is that two documents carrying the same shape are two copies
16
+ # that drift.
17
+ #
18
+ # What the shape says, and why each keyword is there:
19
+ #
20
+ # required presence, which is a different question from type. A field named in
21
+ # properties but not required may be absent; one that is present is
22
+ # still held to its type.
23
+ # additionalProperties: false
24
+ # closes the door. JSON Schema is open by default, so an unexpected
25
+ # field passes silently unless the schema says otherwise. Closed is
26
+ # right for a payload this pipeline fully owns and wrong for one an
27
+ # upstream is free to extend.
28
+ # minItems an empty list is a shape that fits every item schema, so "there is
29
+ # at least one" has to be said out loud.
30
+ # pattern, minimum the value-level narrowing that turns a type check into a real gate.
31
+ #
32
+ # The gate is also a waypoint. Steps below it read ${steps.gate.output.value}, which is the
33
+ # input unchanged, and that reference is the document's own proof that nothing bypassed the
34
+ # check.
35
+ #
36
+ # To change it: -p level=0 makes the payload violate `minimum: 1`, and that run fails at the
37
+ # gate with the failing instance path and keyword named.
38
+ #
39
+ # The schema is carried in the document, so no server stores this one: it runs with
40
+ # `dg run --local`, and on an instance the same shape is created once with `dg schema create`.
41
+ #
42
+ # dg run --local examples/recipes/schema-carried.yaml
43
+ # dg run --local examples/recipes/schema-carried.yaml -p level=0
44
+
45
+ format: dirigent/v1
46
+ kind: pipeline
47
+ code: schema-carried
48
+ name: A schema carried in the document
49
+ description: Validate a payload against a shape the document carries, and read the gate's output downstream as proof of the check.
50
+
51
+ tags: [recipes, transform, validate]
52
+
53
+ requires:
54
+ blocks:
55
+ - value.const
56
+ - transform.jq
57
+ - validate.schema
58
+
59
+ schemas:
60
+ recipe-org-units:
61
+ title: Organisation units read
62
+ description: The envelope an org-unit read answers with, and the fields a loader depends on.
63
+ type: object
64
+ required: [organisationUnits]
65
+ additionalProperties: false
66
+ properties:
67
+ organisationUnits:
68
+ type: array
69
+ minItems: 1
70
+ items:
71
+ type: object
72
+ required: [id, displayName, level]
73
+ additionalProperties: false
74
+ properties:
75
+ id: { type: string, pattern: "^[A-Za-z][A-Za-z0-9]{10}$" }
76
+ displayName: { type: string, minLength: 1 }
77
+ level: { type: integer, minimum: 1 }
78
+
79
+ params:
80
+ type: object
81
+ properties:
82
+ level:
83
+ type: integer
84
+ default: 2
85
+ description: The level the second org unit reports; 0 makes the payload fail the gate.
86
+
87
+ steps:
88
+ payload:
89
+ block: value.const
90
+ config:
91
+ value:
92
+ organisationUnits:
93
+ - { id: ImspTQPwCqd, displayName: Sierra Leone, level: 1 }
94
+ # The level is a parameter so the gate can be made to refuse without editing the
95
+ # document: value.const emits what it is given, references included.
96
+ - { id: O6uvpzGd5pu, displayName: Bo, level: "${params.level}" }
97
+
98
+ gate:
99
+ block: validate.schema
100
+ depends_on: [payload]
101
+ config:
102
+ input: ${steps.payload.output.value}
103
+ schema: recipe-org-units
104
+
105
+ count:
106
+ block: transform.jq
107
+ depends_on: [gate]
108
+ config:
109
+ input: ${steps.gate.output.value}
110
+ program: |
111
+ {units: (.organisationUnits | length),
112
+ levels: [.organisationUnits[].level] | unique}
@@ -0,0 +1,107 @@
1
+ # format: the keyword that says what kind of string a string is, and here actually asserts.
2
+ #
3
+ # In: one record of strings -- a date, a timestamp, an email, a uri, a uuid, a hex digest,
4
+ # and one string carrying a format no pack on this instance contributes.
5
+ # Out: the record, through a gate that checked every one of them.
6
+ #
7
+ # In JSON Schema, format is an annotation by default: a validator only asserts it when it is
8
+ # handed a format checker, and plenty are not. Dirigent's engine always hands its validator
9
+ # one, so a format written here is enforced rather than decorative. That is the difference
10
+ # worth knowing before writing a schema for this engine and reusing it somewhere else.
11
+ #
12
+ # The standard formats are all available -- date, date-time, time, email, uri, hostname,
13
+ # ipv4, ipv6, regex, and the rest -- and dirigent registers the ones its own data speaks:
14
+ # ulid, uuid4, uuid7, md5, sha1, sha256, sha512, base64.
15
+ #
16
+ # A format only ever narrows a string. A value of the wrong type is caught by `type`, and
17
+ # format speaks only once the value is already a string, so `{type: integer, format: date}`
18
+ # asserts nothing about anything.
19
+ #
20
+ # The last field is the interesting one. acme-site-id is a format a plugin pack
21
+ # contributes; on an instance with that pack installed it asserts, and on one without it the
22
+ # keyword is a passing annotation and the value is simply unchecked. That is what keeps a
23
+ # schema portable: the same file asserts more on an instance that knows more, and refuses
24
+ # nothing it cannot check. This recipe runs green either way, and on an instance with the
25
+ # pack a site id of the wrong shape there would fail it.
26
+ #
27
+ # To change it: -p email=not-an-address, -p when=last%20Tuesday, or -p digest=abc all fail
28
+ # the gate, each naming the field and the format it did not satisfy.
29
+ #
30
+ # The schema is carried in the document, so no server stores this one: it runs with
31
+ # `dg run --local`, and on an instance the same shape is created once with `dg schema create`.
32
+ #
33
+ # dg run --local examples/recipes/schema-formats.yaml
34
+ # dg run --local examples/recipes/schema-formats.yaml -p email=not-an-address
35
+
36
+ format: dirigent/v1
37
+ kind: pipeline
38
+ code: schema-formats
39
+ name: Formats that actually assert
40
+ description: Hold a record of strings to date, date-time, email, uri, uuid4 and digest formats, and show what a pack-contributed format does.
41
+
42
+ tags: [recipes, validate]
43
+
44
+ requires:
45
+ blocks:
46
+ - value.const
47
+ - validate.schema
48
+
49
+ schemas:
50
+ recipe-formatted-record:
51
+ type: object
52
+ required: [day, when, email, endpoint, request_id, digest, site]
53
+ additionalProperties: false
54
+ properties:
55
+ day: { type: string, format: date }
56
+ when: { type: string, format: date-time }
57
+ email: { type: string, format: email }
58
+ endpoint: { type: string, format: uri }
59
+ request_id: { type: string, format: uuid4 }
60
+ digest: { type: string, format: sha256 }
61
+ # Contributed by a plugin pack. Where that pack is installed this asserts; where it
62
+ # is not, the value passes unchecked and the schema stays portable.
63
+ site: { type: string, format: acme-site-id }
64
+
65
+ params:
66
+ type: object
67
+ properties:
68
+ day:
69
+ type: string
70
+ default: "2026-01-01"
71
+ description: Held to format date; "last Tuesday" is refused.
72
+ when:
73
+ type: string
74
+ default: "2026-01-01T06:00:00Z"
75
+ description: Held to format date-time, which wants a zone as well as a time.
76
+ email:
77
+ type: string
78
+ default: ops@example.org
79
+ description: Held to format email.
80
+ endpoint:
81
+ type: string
82
+ default: https://postman-echo.com/get
83
+ description: Held to format uri.
84
+ digest:
85
+ type: string
86
+ default: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
87
+ description: Held to format sha256, which is exactly 64 hex characters.
88
+
89
+ steps:
90
+ record:
91
+ block: value.const
92
+ config:
93
+ value:
94
+ day: "${params.day}"
95
+ when: "${params.when}"
96
+ email: "${params.email}"
97
+ endpoint: "${params.endpoint}"
98
+ request_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
99
+ digest: "${params.digest}"
100
+ site: NO-OSLO-01
101
+
102
+ gate:
103
+ block: validate.schema
104
+ depends_on: [record]
105
+ config:
106
+ input: ${steps.record.output.value}
107
+ schema: recipe-formatted-record
@@ -0,0 +1,86 @@
1
+ # The same gate, written the other way: the shape lives on the instance and the document
2
+ # names it.
3
+ #
4
+ # In: one org-unit record.
5
+ # Out: that record, passed through a gate that names ou-record -- a schema the instance
6
+ # holds, not one this file carries.
7
+ #
8
+ # A schema is a resource in its own right, applied like a connection rather than as part of
9
+ # a pipeline:
10
+ #
11
+ # dg schema create examples/schemas/ou-record.json
12
+ # dg schema list
13
+ #
14
+ # and its identity comes out of its own keywords: $id becomes the code every reference uses,
15
+ # title its name, description its body. So the code in the gate below is the $id in that
16
+ # file and nothing else.
17
+ #
18
+ # Referenced is the right choice as soon as a second pipeline holds the same picture of the
19
+ # same payload. One shape, one place to change it, and every pipeline that names it is
20
+ # refused the moment they stop agreeing. It costs a dependency: requires.schemas is where
21
+ # the document says so, and an instance that does not hold ou-record refuses this document
22
+ # at apply, naming what to create first, rather than failing at three in the morning.
23
+ #
24
+ # A --local run starts on an empty throwaway instance, so the schema is handed to it the way
25
+ # connections are:
26
+ #
27
+ # dg run --local --schema examples/schemas/ou-record.json \
28
+ # examples/recipes/schema-referenced.yaml
29
+ #
30
+ # Without --schema that run is refused before anything executes, which is the preflight
31
+ # doing its job.
32
+ #
33
+ # To change it: -p level=zero sends a string where the schema wants an integer, and that run
34
+ # fails at the gate naming the instance path and the keyword.
35
+ #
36
+ # dg run --local --schema examples/schemas/ou-record.json examples/recipes/schema-referenced.yaml
37
+ # dg run --local --schema examples/schemas/ou-record.json examples/recipes/schema-referenced.yaml -p level=zero
38
+
39
+ format: dirigent/v1
40
+ kind: pipeline
41
+ code: schema-referenced
42
+ name: A schema the instance holds
43
+ description: Validate a record against ou-record, a schema applied to the instance on its own, and declare the dependency in requires.
44
+
45
+ tags: [recipes, transform, validate]
46
+
47
+ requires:
48
+ blocks:
49
+ - value.const
50
+ - transform.jq
51
+ - validate.schema
52
+ # The preflight that refuses this document on an instance holding no such schema.
53
+ schemas:
54
+ - ou-record
55
+
56
+ params:
57
+ type: object
58
+ properties:
59
+ level:
60
+ default: 2
61
+ description: The record's level; anything but an integer is refused at the gate.
62
+
63
+ steps:
64
+ record:
65
+ block: value.const
66
+ config:
67
+ value:
68
+ id: O6uvpzGd5pu
69
+ name: Bo
70
+ level: "${params.level}"
71
+
72
+ gate:
73
+ block: validate.schema
74
+ depends_on: [record]
75
+ config:
76
+ input: ${steps.record.output.value}
77
+ # A code, never a shape: this one names the schema stored under $id ou-record.
78
+ schema: ou-record
79
+
80
+ describe:
81
+ block: transform.jq
82
+ depends_on: [gate]
83
+ config:
84
+ input: ${steps.gate.output.value}
85
+ program: |
86
+ {checked_id: .id, at_level: .level}
@@ -0,0 +1,127 @@
1
+ # A gate that refuses, and the two branches below it: the one that runs and the one that
2
+ # does not.
3
+ #
4
+ # THIS RUN FAILS ON PURPOSE. It ends `failed`, with the gate failed, the load skipped, and
5
+ # the alert succeeded. That is the outcome the recipe is here to show; nothing is broken.
6
+ #
7
+ # In: a payload whose second record carries a level of 0, which the carried schema refuses.
8
+ # Out: a failed run whose alert step has a summary of what went wrong, and a load step that
9
+ # never started.
10
+ #
11
+ # The edge into a step carries a rule, and the rule is what decides whether that step runs
12
+ # once its dependencies have settled:
13
+ #
14
+ # all_success the default. Every dependency succeeded. The load below never starts.
15
+ # one_failed at least one dependency failed. This is an error handler, and it is drawn
16
+ # in the DAG rather than hidden inside a block's config.
17
+ # all_done every dependency finished, whatever it finished as. This is the cleanup
18
+ # edge: a step that must run whether the work passed or failed.
19
+ #
20
+ # A handler cannot read the output of the step that failed, because a failed step has no
21
+ # stored output; there is nothing to reference. So the alert reads what it can -- the run's
22
+ # own identity, and the payload as it was before the gate -- and says which pipeline and
23
+ # which run to look at. Reaching for ${steps.gate.output.value} in a one_failed handler is
24
+ # refused when the document is applied, which is the format catching the mistake early.
25
+ #
26
+ # The run's status is still `failed`, and that is deliberate: handling a failure is not the
27
+ # same as not having had one. A step that is allowed to fail without failing the run says so
28
+ # with `continue_on_failure`, which is a different recipe and a different intent.
29
+ #
30
+ # To change it: -p level=2 makes the payload fit the schema, and the same document runs
31
+ # green with the load taken and the alert skipped -- the pair worth running back to back.
32
+ #
33
+ # The schema is carried in the document, so no server stores this one: it runs with
34
+ # `dg run --local`, and on an instance the same shape is created once with `dg schema create`.
35
+ #
36
+ # dg run --local examples/recipes/schema-refuses-then-rule.yaml # fails, by design
37
+ # dg run --local examples/recipes/schema-refuses-then-rule.yaml -p level=2 # succeeds
38
+
39
+ format: dirigent/v1
40
+ kind: pipeline
41
+ code: schema-refuses-then-rule
42
+ name: A refused gate and what runs after it
43
+ description: A validation that fails on purpose, an error branch on rule one_failed, a load that is skipped, and a cleanup on all_done.
44
+
45
+ tags: [recipes, transform, validate, failure]
46
+
47
+ requires:
48
+ blocks:
49
+ - value.const
50
+ - transform.jq
51
+ - validate.schema
52
+
53
+ schemas:
54
+ recipe-levelled-units:
55
+ type: object
56
+ required: [organisationUnits]
57
+ properties:
58
+ organisationUnits:
59
+ type: array
60
+ minItems: 1
61
+ items:
62
+ type: object
63
+ required: [id, level]
64
+ properties:
65
+ id: { type: string, minLength: 1 }
66
+ level: { type: integer, minimum: 1 }
67
+
68
+ params:
69
+ type: object
70
+ properties:
71
+ level:
72
+ type: integer
73
+ default: 0
74
+ description: The second unit's level. The default of 0 is what the schema refuses.
75
+
76
+ steps:
77
+ payload:
78
+ block: value.const
79
+ config:
80
+ value:
81
+ organisationUnits:
82
+ - { id: ImspTQPwCqd, level: 1 }
83
+ - { id: O6uvpzGd5pu, level: "${params.level}" }
84
+
85
+ gate:
86
+ block: validate.schema
87
+ depends_on: [payload]
88
+ config:
89
+ input: ${steps.payload.output.value}
90
+ schema: recipe-levelled-units
91
+
92
+ load:
93
+ block: transform.jq
94
+ depends_on: [gate]
95
+ # The default rule, written out because this document is about rules. The gate failed,
96
+ # so this step is skipped and nothing it would have done happened.
97
+ rule: all_success
98
+ config:
99
+ input: ${steps.gate.output.value}
100
+ program: |
101
+ {loaded: (.organisationUnits | length)}
102
+
103
+ alert:
104
+ block: transform.jq
105
+ depends_on: [gate]
106
+ rule: one_failed
107
+ config:
108
+ # A failed step has no stored output, so the handler reads the run's own identity and
109
+ # the payload as it stood before the gate.
110
+ input:
111
+ run: ${run.id}
112
+ payload: ${steps.payload.output.value}
113
+ program: |
114
+ {alert: "org unit levels failed validation",
115
+ run: .run,
116
+ units: (.payload.organisationUnits | length),
117
+ suspect: [.payload.organisationUnits[] | select(.level < 1) | .id]}
118
+
119
+ close_out:
120
+ block: transform.jq
121
+ depends_on: [load, alert]
122
+ # Satisfied by a skip as much as by a success, so the run always finishes here.
123
+ rule: all_done
124
+ config:
125
+ input: ${run.id}
126
+ program: |
127
+ {closed_run: .}
@@ -0,0 +1,114 @@
1
+ # Keep a dated copy of what a run produced, with the layout decided in the document.
2
+ #
3
+ # In: a payload built inline and handed to storage.write.
4
+ # Out: that file, plus a copy at a dated archive key, and a manifest naming both with the
5
+ # byte count the copy reported.
6
+ #
7
+ # The first two hops are the pair every value makes on its way out: transform.jq produces a
8
+ # value, storage.write puts it at a URI, and the copy below works on the URI the write
9
+ # reported rather than on anything the run is still holding.
10
+ #
11
+ # storage.copy moves bytes from one URI to another and knows nothing else. It is not an S3
12
+ # block or a file block: the scheme in the URI decides the backend, so file:// to file://,
13
+ # file:// to s3:// and s3:// to s3:// are all the same step, and adding an object store to
14
+ # an instance changes no document.
15
+ #
16
+ # The archive key is where the thinking goes, because a key layout is a decision nobody
17
+ # revisits once a year of files is written under it. This one is prefix/YYYY/MM/DD/name:
18
+ #
19
+ # Date parts are separate segments, so a listing can be taken per year or per month
20
+ # without parsing names, and a lifecycle rule can expire a prefix.
21
+ # The date is the run's parameter and never the wall clock, so a rerun for last Tuesday
22
+ # writes last Tuesday's key rather than today's.
23
+ # The name is stable inside the dated prefix, so the newest file is always the one under
24
+ # the newest key rather than the one with the highest suffix.
25
+ #
26
+ # The copy reports bytes_copied, which is the cheapest possible integrity check: a manifest
27
+ # carrying it lets a later run notice a zero-byte archive without opening it.
28
+ #
29
+ # To change it: -p archive_prefix=... points the same copy at another root, and -p
30
+ # day=2026-02-03 writes into 2026/02/03 instead.
31
+ #
32
+ # dg run --local examples/recipes/storage-copy-dated-archive.yaml
33
+ # dg run --local examples/recipes/storage-copy-dated-archive.yaml -p day=2026-02-03
34
+
35
+ format: dirigent/v1
36
+ kind: pipeline
37
+ code: storage-copy-dated-archive
38
+ name: A dated archive copy
39
+ description: Write a file, copy it to a date-partitioned archive key, and record both URIs and the bytes copied in a manifest.
40
+
41
+ tags: [recipes, storage, transform]
42
+
43
+ requires:
44
+ blocks:
45
+ - transform.jq
46
+ - storage.write
47
+ - storage.copy
48
+
49
+ params:
50
+ type: object
51
+ properties:
52
+ day:
53
+ type: string
54
+ format: date
55
+ default: "2026-01-01"
56
+ description: The day being archived; the archive key is built from it, not from now.
57
+ archive_prefix:
58
+ type: string
59
+ default: archive/readings
60
+ description: The root the dated key hangs under.
61
+
62
+ steps:
63
+ build:
64
+ block: transform.jq
65
+ config:
66
+ input:
67
+ day: ${params.day}
68
+ program: |
69
+ {day: .day,
70
+ readings: [range(5) | {station: "st-\(.)", celsius: (. * 2) - 3}]}
71
+
72
+ store:
73
+ block: storage.write
74
+ depends_on: [build]
75
+ config:
76
+ target: ${run.scratch}/current/readings.json
77
+ value: ${steps.build.output.value}
78
+
79
+ key:
80
+ block: transform.jq
81
+ depends_on: [build]
82
+ config:
83
+ input:
84
+ day: ${params.day}
85
+ prefix: ${params.archive_prefix}
86
+ program: |
87
+ . as {$day, $prefix}
88
+ | ($day | split("-")) as [$year, $month, $date]
89
+ | {key: "\($prefix)/\($year)/\($month)/\($date)/readings.json",
90
+ year: $year, month: $month, date: $date}
91
+
92
+ archive:
93
+ block: storage.copy
94
+ depends_on: [store, key]
95
+ config:
96
+ source: ${steps.store.output.uri}
97
+ # The key comes from the step that built it, so the layout is written once.
98
+ target: ${run.scratch}/${steps.key.output.value.key}
99
+
100
+ manifest:
101
+ block: transform.jq
102
+ depends_on: [archive]
103
+ config:
104
+ input:
105
+ source: ${steps.archive.output.source}
106
+ target: ${steps.archive.output.target}
107
+ bytes: ${steps.archive.output.bytes_copied}
108
+ day: ${params.day}
109
+ program: |
110
+ {day, source, target,
111
+ archived_bytes: .bytes,
112
+ # A zero-byte archive is the failure a manifest is for: it is visible here without
113
+ # anybody opening the object.
114
+ empty: (.bytes == 0)}
@@ -0,0 +1,127 @@
1
+ # Wait for an object before reading it, and set the floor that stops a half-written file
2
+ # being read.
3
+ #
4
+ # In: a file this pipeline writes itself, so the sensor has something to find.
5
+ # Out: the file's URI, size and modification time from the sensor, and a summary read from
6
+ # it afterwards.
7
+ #
8
+ # storage.exists is a sensor, not an operator. It observes and changes nothing, each poke is
9
+ # one cheap head request, and "not yet" is the expected answer rather than a failure. While
10
+ # it waits, the run holds no worker: it parks and is poked again on the schedule the step
11
+ # sets.
12
+ #
13
+ # The three knobs are step-level engine semantics, uniform across every sensor and never
14
+ # inside a block's config:
15
+ #
16
+ # poll how often to look. Seconds here because the file is already there; minutes
17
+ # is the honest setting for a drop somebody else produces.
18
+ # deadline how long to keep looking before giving up. A sensor without one waits 24h.
19
+ # on_timeout what a deadline means. `skip` says nobody dropped a file inside the window,
20
+ # which is a quiet day rather than a broken pipeline, and the steps below skip
21
+ # with it. `fail` says the drop was promised, and its absence is an incident.
22
+ #
23
+ # min_size is the one that saves a real pipeline. A producer uploading a large object makes
24
+ # the key visible before the bytes are all there, so a sensor without a floor hands a
25
+ # truncated file to the reader below it. The floor is set to what the smallest legitimate
26
+ # file could be, never to what a typical one is.
27
+ #
28
+ # The uri may glob in its final segments, so `${run.scratch}/drops/*.json` waits for
29
+ # whichever file arrives; the sensor's output names the one it actually found, which is why
30
+ # the reader below reads ${steps.dropped.output.uri} and not the pattern.
31
+ #
32
+ # The sensor observes, it does not read: it reports a URI, a size and a time, and nothing
33
+ # about what is inside. Turning that into a value is storage.read's job, one hop later, and
34
+ # that hop is where max_size decides how much of the drop this run will hold.
35
+ #
36
+ # To change it: point uri at a drop somebody else writes and raise the deadline; with
37
+ # on_timeout: skip that run ends green having done nothing, which is what a quiet day
38
+ # should look like.
39
+ #
40
+ # dg run --local examples/recipes/storage-exists-gate.yaml
41
+ # dg run --local examples/recipes/storage-exists-gate.yaml -p min_size=1mb # skips: no file is that big
42
+
43
+ format: dirigent/v1
44
+ kind: pipeline
45
+ code: storage-exists-gate
46
+ name: Wait for an object, then read it
47
+ description: Park on a storage.exists sensor with a size floor and a deadline, then read whatever file the sensor reports it found.
48
+
49
+ tags: [recipes, sensor, storage, transform]
50
+
51
+ requires:
52
+ blocks:
53
+ - transform.jq
54
+ - storage.write
55
+ - storage.exists
56
+ - storage.read
57
+
58
+ params:
59
+ type: object
60
+ properties:
61
+ day:
62
+ type: string
63
+ format: date
64
+ default: "2026-01-01"
65
+ description: The day the drop is named for.
66
+ min_size:
67
+ type: string
68
+ default: 32b
69
+ description: The floor that stops a half-written object being read. 1mb makes this run skip.
70
+
71
+ steps:
72
+ produce:
73
+ block: transform.jq
74
+ config:
75
+ input:
76
+ day: ${params.day}
77
+ program: |
78
+ {day: .day, readings: [range(8) | {station: "st-\(.)", celsius: (. - 3)}]}
79
+
80
+ drop:
81
+ block: storage.write
82
+ depends_on: [produce]
83
+ config:
84
+ target: ${run.scratch}/drops/${params.day}.json
85
+ value: ${steps.produce.output.value}
86
+
87
+ dropped:
88
+ block: storage.exists
89
+ depends_on: [drop]
90
+ poll: 2s
91
+ deadline: 30s
92
+ # A missing drop is a quiet day: the run ends green with this and everything below it
93
+ # skipped, rather than raising an incident nobody can act on.
94
+ on_timeout: skip
95
+ config:
96
+ # A glob, so whichever file lands that day is found; the sensor reports which.
97
+ uri: ${run.scratch}/drops/*.json
98
+ min_size: ${params.min_size}
99
+
100
+ read:
101
+ block: storage.read
102
+ depends_on: [dropped]
103
+ config:
104
+ # The file the sensor found, not the pattern it was given.
105
+ source: ${steps.dropped.output.uri}
106
+ max_size: 1mb
107
+
108
+ summary:
109
+ block: transform.jq
110
+ depends_on: [read]
111
+ config:
112
+ input: ${steps.read.output.value}
113
+ program: |
114
+ {day, rows: (.readings | length),
115
+ warmest: (.readings | max_by(.celsius) | .station)}
116
+
117
+ receipt:
118
+ block: transform.jq
119
+ depends_on: [dropped, summary]
120
+ config:
121
+ input:
122
+ uri: ${steps.dropped.output.uri}
123
+ size: ${steps.dropped.output.size}
124
+ modified_at: ${steps.dropped.output.modified_at}
125
+ summary: ${steps.summary.output.value}
126
+ program: |
127
+ {found: .uri, size_bytes: .size, modified_at, summary}