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,116 @@
1
+ # Keep the rows a predicate answers true for, and nothing else about them changes.
2
+ #
3
+ # In: a list of stations with a status, a battery level, and a last-seen timestamp.
4
+ # Out: the subset that is reporting and healthy, elements untouched, in input order.
5
+ #
6
+ # filter.jq is select as a contract. The program answers one question about one element, and
7
+ # the frame appends the element it was given rather than anything the program returned -- so
8
+ # a filter cannot edit a row on the way past, however the program is written. Order is the
9
+ # input's, and the output is always a subset.
10
+ #
11
+ # The rule that catches everybody: jq's truthiness is not applied here. A program that
12
+ # answers 0, "" or null is a mistake surfaced rather than an element quietly dropped, and
13
+ # the step fails saying so:
14
+ #
15
+ # element 0: the program answered 4, and a filter answers true or false; jq's truthiness
16
+ # is not applied, so a program meaning 'has readings' writes '.count > 0'
17
+ #
18
+ # So a predicate is written as a comparison, always. The three below are the shapes most
19
+ # predicates take: a set membership with IN, a numeric threshold, and a presence check that
20
+ # distinguishes absent from null.
21
+ #
22
+ # The count step afterwards is the other half of filtering: a filter that reports nothing
23
+ # leaves "how many did it drop" to somebody reading two artifacts side by side.
24
+ #
25
+ # To change it: -p min_battery=90 tightens the threshold; -p statuses='["active","standby"]'
26
+ # widens the set the membership test reads.
27
+ #
28
+ # dg run --local examples/recipes/filter-by-predicate.yaml
29
+ # dg run --local examples/recipes/filter-by-predicate.yaml -p min_battery=90
30
+
31
+ format: dirigent/v1
32
+ kind: pipeline
33
+ code: filter-by-predicate
34
+ name: Filter with a predicate
35
+ description: Keep the elements a true-or-false program accepts, unmodified and in order, and report how many were dropped.
36
+
37
+ tags: [recipes, transform, filter]
38
+
39
+ requires:
40
+ blocks:
41
+ - value.const
42
+ - transform.jq
43
+ - filter.jq
44
+
45
+ params:
46
+ type: object
47
+ properties:
48
+ min_battery:
49
+ type: integer
50
+ minimum: 0
51
+ default: 20
52
+ description: The battery percentage below which a station is not considered healthy.
53
+ statuses:
54
+ type: array
55
+ default: [active]
56
+ items:
57
+ type: string
58
+ description: The statuses that count as reporting.
59
+
60
+ steps:
61
+ stations:
62
+ block: value.const
63
+ config:
64
+ value:
65
+ - { id: st-1, status: active, battery: 96, last_seen: "2026-01-01T06:00:00Z" }
66
+ - { id: st-2, status: retired, battery: 100, last_seen: "2025-11-02T06:00:00Z" }
67
+ - { id: st-3, status: active, battery: 11, last_seen: "2026-01-01T05:00:00Z" }
68
+ - { id: st-4, status: standby, battery: 80, last_seen: "2026-01-01T04:00:00Z" }
69
+ # No battery reading at all: absent is not the same as flat.
70
+ - { id: st-5, status: active, last_seen: "2026-01-01T06:00:00Z" }
71
+
72
+ attach:
73
+ block: transform.jq
74
+ depends_on: [stations]
75
+ config:
76
+ # The predicate's thresholds are data, so they travel with each element; a filter
77
+ # program sees one element and nothing else.
78
+ input:
79
+ rows: ${steps.stations.output.value}
80
+ min_battery: ${params.min_battery}
81
+ statuses: ${params.statuses}
82
+ program: |
83
+ . as {$rows, $min_battery, $statuses}
84
+ | [$rows[] | . + {_min_battery: $min_battery, _statuses: $statuses}]
85
+
86
+ healthy:
87
+ block: filter.jq
88
+ depends_on: [attach]
89
+ config:
90
+ input: ${steps.attach.output.value}
91
+ # Every clause is a comparison: IN answers a boolean, the threshold answers a boolean,
92
+ # and the presence check answers a boolean. Nothing here relies on truthiness.
93
+ #
94
+ # The element is named first because IN evaluates its argument against whatever was
95
+ # piped into it -- a bare .status | IN(._statuses[]) would look for _statuses on the
96
+ # status string and fail the element.
97
+ program: |
98
+ . as $row
99
+ | ($row.status | IN($row._statuses[]))
100
+ and ($row.battery != null)
101
+ and ($row.battery >= $row._min_battery)
102
+
103
+ report:
104
+ block: transform.jq
105
+ depends_on: [attach, healthy]
106
+ config:
107
+ input:
108
+ seen: ${steps.attach.output.value}
109
+ kept: ${steps.healthy.output.value}
110
+ program: |
111
+ {seen: (.seen | length),
112
+ kept: (.kept | length),
113
+ dropped: [(.seen | map(.id)) - (.kept | map(.id)) | .[]],
114
+ # The bookkeeping fields the predicate needed are removed here rather than in the
115
+ # filter, which is not allowed to modify what it keeps.
116
+ rows: [.kept[] | del(._min_battery, ._statuses)]}
@@ -0,0 +1,109 @@
1
+ # The three verbs in the order a pipeline usually needs them: keep, convert, summarise.
2
+ #
3
+ # In: raw meter readings, some of them from a decommissioned meter or flagged suspect.
4
+ # Out: {kept, converted, summary} -- and a summary computed by reduce over the converted
5
+ # rows.
6
+ #
7
+ # Each step is the verb whose promise matches what it is doing, and the promise is what the
8
+ # document tells a reader without their having to read the program:
9
+ #
10
+ # filter.jq the output is a subset of the input and the elements are untouched. A
11
+ # reader knows no value was edited here without reading the predicate.
12
+ # map.jq the output has one element per input element, in order. A reader knows the
13
+ # batch did not change size here without reading the program.
14
+ # transform.jq anything else. Summarising changes the shape from a list to an object, so
15
+ # it is the only one of the three that can do it.
16
+ #
17
+ # Written as one transform.jq the result would be identical and the document would say
18
+ # nothing. The cost of the split is two more steps; the payoff is that each step's failure
19
+ # names the element it happened on, and that a run's step list reads as what was done.
20
+ #
21
+ # reduce is the summariser here rather than three passes of add, because the accumulator
22
+ # needs to see each row once to keep a count, a total and a maximum together. add over three
23
+ # mapped lists is fine and reads well; reduce is what to reach for once the accumulator
24
+ # holds more than one number.
25
+ #
26
+ # To change it: -p suspect_is_kept=true keeps the flagged rows, which moves both the count
27
+ # and the mean and is the fastest way to see how much a quality rule is worth.
28
+ #
29
+ # dg run --local examples/recipes/filter-then-map-then-reduce.yaml
30
+ # dg run --local examples/recipes/filter-then-map-then-reduce.yaml -p suspect_is_kept=true
31
+
32
+ format: dirigent/v1
33
+ kind: pipeline
34
+ code: filter-then-map-then-reduce
35
+ name: Filter, then map, then reduce
36
+ description: Keep the usable readings, convert each one, and fold the result into a summary with reduce.
37
+
38
+ tags: [recipes, transform, filter, map]
39
+
40
+ requires:
41
+ blocks:
42
+ - value.const
43
+ - transform.jq
44
+ - map.jq
45
+ - filter.jq
46
+
47
+ params:
48
+ type: object
49
+ properties:
50
+ suspect_is_kept:
51
+ type: boolean
52
+ default: false
53
+ description: Keep the readings flagged suspect instead of dropping them.
54
+
55
+ steps:
56
+ readings:
57
+ block: value.const
58
+ config:
59
+ value:
60
+ - { meter: m-1, at: "2026-01-01T06:00:00Z", kwh: 12.5, state: live, suspect: false }
61
+ - { meter: m-2, at: "2026-01-01T06:00:00Z", kwh: 0.0, state: decommissioned, suspect: false }
62
+ - { meter: m-3, at: "2026-01-01T06:00:00Z", kwh: 9.25, state: live, suspect: true }
63
+ - { meter: m-4, at: "2026-01-01T06:00:00Z", kwh: 31.0, state: live, suspect: false }
64
+ - { meter: m-5, at: "2026-01-01T06:00:00Z", kwh: 4.75, state: live, suspect: false }
65
+
66
+ attach:
67
+ block: transform.jq
68
+ depends_on: [readings]
69
+ config:
70
+ input:
71
+ rows: ${steps.readings.output.value}
72
+ keep_suspect: ${params.suspect_is_kept}
73
+ program: |
74
+ . as {$rows, $keep_suspect}
75
+ | [$rows[] | . + {_keep_suspect: $keep_suspect}]
76
+
77
+ kept:
78
+ block: filter.jq
79
+ depends_on: [attach]
80
+ config:
81
+ input: ${steps.attach.output.value}
82
+ program: |
83
+ .state == "live" and (._keep_suspect or (.suspect | not))
84
+
85
+ converted:
86
+ block: map.jq
87
+ depends_on: [kept]
88
+ config:
89
+ input: ${steps.kept.output.value}
90
+ # A megajoule is 3.6 kWh, so this is the unit conversion a sink asked for; the map is
91
+ # where it belongs because it touches one row at a time and changes no row count.
92
+ program: |
93
+ {meter, at, megajoules: (.kwh * 3.6 | . * 100 | round / 100)}
94
+
95
+ summary:
96
+ block: transform.jq
97
+ depends_on: [converted]
98
+ config:
99
+ input: ${steps.converted.output.value}
100
+ program: |
101
+ . as $rows
102
+ | reduce $rows[] as $row
103
+ ({meters: 0, megajoules: 0, peak: null};
104
+ {meters: (.meters + 1),
105
+ megajoules: (.megajoules + $row.megajoules),
106
+ peak: (if .peak == null or $row.megajoules > .peak.megajoules
107
+ then $row else .peak end)})
108
+ | . + {mean_megajoules: (if .meters == 0 then null
109
+ else (.megajoules / .meters * 100 | round / 100) end)}
@@ -0,0 +1,142 @@
1
+ # Fetch, check what came back, reshape it, check what you made, and post it on.
2
+ #
3
+ # This is the shape most real pipelines have, and the reason it is written out in full here is
4
+ # the second gate. One gate, at the fetch, is the common half-measure: it catches the day the
5
+ # source changes and catches nothing else. The second gate catches the other failure, which is
6
+ # the pipeline's own -- a jq program edited under time pressure, a field renamed in one place
7
+ # and not the other -- and it catches it before the bad row leaves the building.
8
+ #
9
+ # In: a station and a day, as parameters.
10
+ # Out: one checked row, posted to a public echo service, and a receipt saying what it saw.
11
+ #
12
+ # Five hops, and what each one hands on:
13
+ #
14
+ # fetch http.request. A GET whose query carries the two parameters, answered by Postman
15
+ # Echo, a public request-and-response service, so this runs against something real
16
+ # with nothing to stand up first. Output: status, headers, body, body_bytes.
17
+ # checked validate.schema against `echo-reading`: the source's shape, as this document
18
+ # understands it. The block passes the value through unchanged when it holds, so
19
+ # the next step reads the gate's output rather than reaching back past it -- which
20
+ # is what makes the gate impossible to skip by accident.
21
+ # reading transform.jq. The reshaping: the query the echo service reflected back becomes
22
+ # the row a receiver wants, and the temperature becomes a number, because a query
23
+ # string carries text and a downstream average cannot add text.
24
+ # verified validate.schema again, against `station-reading` this time: the shape this
25
+ # document promises, with additionalProperties false so a field the program leaks
26
+ # is refused rather than shipped.
27
+ # publish http.request POST. The body is the value the gate passed, serialised by the
28
+ # block: nothing here builds JSON out of strings.
29
+ #
30
+ # BOTH SCHEMAS ARE NAMED, NOT CARRIED. `schema:` takes a code and never a shape, and a document
31
+ # that carries its own top-level `schemas:` section is refused by a server, so the two live in
32
+ # examples/schemas/ and `requires.schemas` below says the instance must hold them. That is the
33
+ # preflight: an apply against an instance missing either one is refused up front, naming what
34
+ # is missing, instead of failing on the first run.
35
+ #
36
+ # dg schema create examples/schemas/echo-reading.json
37
+ # dg schema create examples/schemas/station-reading.json
38
+ # dg apply examples/recipes/http-fetch-validate-post.yaml
39
+ # dg run http-fetch-validate-post --watch
40
+ #
41
+ # Locally there is no instance holding them, so hand them over on the command line:
42
+ #
43
+ # dg run --local examples/recipes/http-fetch-validate-post.yaml \
44
+ # --schema examples/schemas/echo-reading.json \
45
+ # --schema examples/schemas/station-reading.json
46
+ #
47
+ # TO MAKE IT YOURS: point `fetch` at your source, rewrite the two schemas for its shape and
48
+ # yours, and point `publish` at your receiver. The five hops do not move; a real pipeline is
49
+ # this with a longer jq program and a connection holding the credential.
50
+
51
+ format: dirigent/v1
52
+ kind: pipeline
53
+ code: http-fetch-validate-post
54
+ name: Fetch, validate, transform, validate, post
55
+ description: |
56
+ The full shape: a GET from a public endpoint, a schema gate on what came back, a jq
57
+ reshaping, a second gate on what was made, and a POST of the result.
58
+
59
+ The second gate is the point. The first one catches the source changing; the second catches
60
+ this document's own program going wrong, before the bad row is delivered.
61
+
62
+ tags: [recipes, http, transform, validate, starter]
63
+
64
+ requires:
65
+ blocks:
66
+ - http.request
67
+ - transform.jq
68
+ - validate.schema
69
+ # The preflight that refuses this document on an instance holding neither shape.
70
+ schemas:
71
+ - echo-reading
72
+ - station-reading
73
+
74
+ params:
75
+ type: object
76
+ properties:
77
+ station:
78
+ type: string
79
+ minLength: 1
80
+ default: st-1
81
+ description: The station the reading is attributed to.
82
+ day:
83
+ type: string
84
+ format: date
85
+ default: "2026-01-01"
86
+ description: The day the reading reports for.
87
+ celsius:
88
+ type: string
89
+ default: "12.5"
90
+ description: The temperature, as the text a query string carries; the transform makes it a number.
91
+
92
+ steps:
93
+ fetch:
94
+ block: http.request
95
+ config:
96
+ url: https://postman-echo.com/get
97
+ method: GET
98
+ query:
99
+ station: ${params.station}
100
+ day: ${params.day}
101
+ celsius: ${params.celsius}
102
+ timeout: 30s
103
+
104
+ checked:
105
+ block: validate.schema
106
+ depends_on: [fetch]
107
+ config:
108
+ input: ${steps.fetch.output.body}
109
+ # A code naming a schema the instance holds, never a shape written here.
110
+ schema: echo-reading
111
+
112
+ reading:
113
+ block: transform.jq
114
+ depends_on: [checked]
115
+ config:
116
+ input: ${steps.checked.output.value}
117
+ program: |
118
+ .args
119
+ | {station: .station,
120
+ day: .day,
121
+ # tonumber, because a query string carried this as text and a receiver that
122
+ # averages it cannot add text.
123
+ celsius: (.celsius | tonumber)}
124
+
125
+ verified:
126
+ block: validate.schema
127
+ depends_on: [reading]
128
+ config:
129
+ input: ${steps.reading.output.value}
130
+ schema: station-reading
131
+
132
+ publish:
133
+ block: http.request
134
+ depends_on: [verified]
135
+ config:
136
+ url: https://postman-echo.com/post
137
+ method: POST
138
+ # The value the gate passed through, serialised by the block.
139
+ body: ${steps.verified.output.value}
140
+ headers:
141
+ x-reading-day: ${params.day}
142
+ timeout: 30s
@@ -0,0 +1,100 @@
1
+ # Redirects are not followed unless a step says so, and both answers are useful.
2
+ #
3
+ # In: a redirect status and a target, as parameters.
4
+ # Out: the same call made twice -- once returning the 3xx itself, once following it to the
5
+ # end -- so the difference is one output rather than an argument.
6
+ #
7
+ # follow_redirects defaults to false, and the default is the interesting choice. A redirect
8
+ # is information: it says the resource moved, or that a load balancer wants a different
9
+ # host, or that an unauthenticated call is being sent to a login page. Following it
10
+ # silently turns "your URL is wrong" into a 200 that a pipeline treats as data, and the
11
+ # classic version of that is a 302 to an HTML sign-in page parsed as an empty result.
12
+ #
13
+ # So the block returns the 3xx as the answer, with its Location header, and a step that
14
+ # genuinely wants the destination asks for it:
15
+ #
16
+ # follow_redirects: false status is 302 and the body is the redirect's own. Location
17
+ # says where it points, which is a thing a pipeline can report
18
+ # or refuse.
19
+ # follow_redirects: true the client follows, and status is the destination's. Nothing
20
+ # says a redirect happened, so this is the setting for an API
21
+ # that is known to redirect as part of its normal working.
22
+ #
23
+ # A 3xx is not a 2xx, so the un-followed call needs the status listed as success or the
24
+ # step fails -- which is success_status doing exactly what http-success-status-list.yaml
25
+ # describes, on the one status that most deserves it.
26
+ #
27
+ # To change it: -p status=301 makes it a permanent move, which is the one worth reporting
28
+ # rather than following, since the URL in the document is now wrong.
29
+ #
30
+ # dg run --local examples/recipes/http-follow-redirects.yaml
31
+ # dg run --local examples/recipes/http-follow-redirects.yaml -p status=301
32
+
33
+ format: dirigent/v1
34
+ kind: pipeline
35
+ code: http-follow-redirects
36
+ name: Following a redirect, or not
37
+ description: Make the same redirecting call twice, with and without follow_redirects, and compare the status and the Location header.
38
+
39
+ tags: [recipes, http, transform]
40
+
41
+ requires:
42
+ blocks:
43
+ - http.request
44
+ - transform.jq
45
+
46
+ params:
47
+ type: object
48
+ properties:
49
+ status:
50
+ type: integer
51
+ default: 302
52
+ description: The redirect status the endpoint answers with.
53
+ target:
54
+ type: string
55
+ default: https://postman-echo.com/get
56
+ description: Where the redirect points.
57
+
58
+ steps:
59
+ unfollowed:
60
+ block: http.request
61
+ config:
62
+ url: https://postman-echo.com/redirect-to
63
+ method: GET
64
+ query:
65
+ url: ${params.target}
66
+ status_code: ${params.status}
67
+ # The default, written out because this document is about it.
68
+ follow_redirects: false
69
+ # A 3xx is not a 2xx, so the status the call is asking for has to be listed.
70
+ success_status: [301, 302, 307, 308]
71
+ timeout: 20s
72
+
73
+ followed:
74
+ block: http.request
75
+ config:
76
+ url: https://postman-echo.com/redirect-to
77
+ method: GET
78
+ query:
79
+ url: ${params.target}
80
+ status_code: ${params.status}
81
+ follow_redirects: true
82
+ timeout: 20s
83
+
84
+ compare:
85
+ block: transform.jq
86
+ depends_on: [unfollowed, followed]
87
+ config:
88
+ input:
89
+ unfollowed_status: ${steps.unfollowed.output.status}
90
+ unfollowed_headers: ${steps.unfollowed.output.headers}
91
+ followed_status: ${steps.followed.output.status}
92
+ followed_body: ${steps.followed.output.body}
93
+ program: |
94
+ {unfollowed: {status: .unfollowed_status,
95
+ # Where it pointed: the thing a pipeline can report or refuse.
96
+ location: .unfollowed_headers.location},
97
+ followed: {status: .followed_status,
98
+ # The destination answered, and nothing in the response says a redirect
99
+ # happened on the way.
100
+ url: .followed_body.url}}
@@ -0,0 +1,96 @@
1
+ # A GET with query parameters, and what to do with the answer.
2
+ #
3
+ # In: a station code and a day, as parameters.
4
+ # Out: the echo service's view of the request -- the query it received -- reshaped into a
5
+ # small record, plus the status and how long the call took.
6
+ #
7
+ # https://postman-echo.com/get answers with the request it was given, so a recipe about
8
+ # building a request can show what actually arrived on the wire without standing anything
9
+ # up. Against a real API the only lines that change are url or connection, and the path.
10
+ #
11
+ # The query is a map, not a string, and that is the whole point of the field: the block
12
+ # encodes each value once, correctly, so a station code with a space or an ampersand in it
13
+ # cannot break out of its parameter. A path with ?a=1&b=2 hand-written into it is where
14
+ # injection and double-encoding both come from.
15
+ #
16
+ # Values may be strings, numbers or booleans -- paging=false goes over the wire as the text
17
+ # `false`, because that is what a query string is -- and a reference resolves before the
18
+ # encoding happens, so ${params.day} is a value and never a fragment of URL.
19
+ #
20
+ # The answer is one field. body carries the parsed document when the service answered JSON
21
+ # and the text when it answered anything else, and body_bytes says how big it was.
22
+ # max_response bounds what the step will hold: a body past it is refused rather than read
23
+ # half way. A body worth keeping is handed to a storage.write step, which is what
24
+ # http-save-body-to-storage.yaml does with this same call.
25
+ #
26
+ # duration_ms is on every response, so a pipeline can report what a call cost without
27
+ # timing it itself.
28
+ #
29
+ # To change it: -p station=st-9, or point url at your own endpoint and keep the rest.
30
+ #
31
+ # dg run --local examples/recipes/http-get-with-query.yaml
32
+ # dg run --local examples/recipes/http-get-with-query.yaml -p station=st-9 -p day=2026-02-01
33
+
34
+ format: dirigent/v1
35
+ kind: pipeline
36
+ code: http-get-with-query
37
+ name: A GET with query parameters
38
+ description: Build a GET whose query is a map rather than a hand-written string, and reshape the JSON body it answers with.
39
+
40
+ tags: [recipes, http, transform]
41
+
42
+ requires:
43
+ blocks:
44
+ - http.request
45
+ - transform.jq
46
+
47
+ params:
48
+ type: object
49
+ properties:
50
+ station:
51
+ type: string
52
+ default: st-1
53
+ description: The station the request asks about.
54
+ day:
55
+ type: string
56
+ format: date
57
+ default: "2026-01-01"
58
+ description: The day the request asks about.
59
+ page_size:
60
+ type: integer
61
+ minimum: 1
62
+ default: 50
63
+ description: A numeric query value, sent as its digits.
64
+
65
+ steps:
66
+ fetch:
67
+ block: http.request
68
+ config:
69
+ # An absolute url, for the case where no connection is configured. A pipeline that
70
+ # runs against an instance names a connection instead, so moving from staging to
71
+ # production edits the connection and no document.
72
+ url: https://postman-echo.com/get
73
+ method: GET
74
+ query:
75
+ station: ${params.station}
76
+ day: ${params.day}
77
+ page_size: ${params.page_size}
78
+ # A boolean goes on the wire as the word, because a query string has no types.
79
+ paging: false
80
+ timeout: 20s
81
+
82
+ received:
83
+ block: transform.jq
84
+ depends_on: [fetch]
85
+ config:
86
+ input:
87
+ status: ${steps.fetch.output.status}
88
+ duration_ms: ${steps.fetch.output.duration_ms}
89
+ # The parsed body. The echo service puts what it received under .args.
90
+ body: ${steps.fetch.output.body}
91
+ program: |
92
+ {status, duration_ms,
93
+ url: .body.url,
94
+ # Every query value arrives back as text, because that is what a query string is.
95
+ query: .body.args,
96
+ page_size_is_text: (.body.args.page_size | type)}
@@ -0,0 +1,110 @@
1
+ # Credentials live on a connection, not in the document. Here is what that looks like.
2
+ #
3
+ # In: nothing but parameters.
4
+ # Out: {authenticated: true} from an endpoint that refuses an unauthenticated caller with a
5
+ # 401, proving the connection's credentials were applied.
6
+ #
7
+ # A document is portable precisely because it names a credential rather than carrying one.
8
+ # On a server the connection is created once and the document says only its code:
9
+ #
10
+ # dg connection create http postman-echo \
11
+ # --set base_url=https://postman-echo.com \
12
+ # --set basic_username=postman --set basic_password=password
13
+ #
14
+ # basic_password is a SecretStr: sealed on the way in, encrypted at rest, and redacted in
15
+ # every API response -- an applied connection reports which fields are set, never what they
16
+ # are set to.
17
+ #
18
+ # This document carries its connection inline instead, which is the exception rather than
19
+ # the pattern, and it is labelled below with why: the credentials are Postman's own public
20
+ # demo pair, so the recipe runs standalone with nothing to hand it. Real credentials never
21
+ # go in a file that lives in git. The instance will not store a document that carries a
22
+ # connection, either, which is the format making that hard to get wrong.
23
+ #
24
+ # What the connection contributes, once named:
25
+ #
26
+ # base_url the step gives a path, and the connection decides which host it is
27
+ # resolved against. Staging to production is a connection edit and no
28
+ # document change.
29
+ # basic_username the Authorization header, built by the block. Nothing in the document
30
+ # basic_password ever spells out `Basic ...`, and no secret reaches a header map.
31
+ # timeout the default deadline for every call on this connection, which a step
32
+ # may override for one call.
33
+ #
34
+ # headers is for what the receiver wants that is not a credential: a routing key, an API
35
+ # version, a correlation id. A token in there is a token in the document, which is the
36
+ # mistake this whole surface exists to prevent.
37
+ #
38
+ # To change it: -p path=/get calls an endpoint that takes no credentials at all and echoes
39
+ # the headers it saw, including the Authorization the connection added.
40
+ #
41
+ # dg run --local examples/recipes/http-headers-and-auth-connection.yaml
42
+ # dg run --local examples/recipes/http-headers-and-auth-connection.yaml -p path=/get
43
+
44
+ format: dirigent/v1
45
+ kind: pipeline
46
+ code: http-headers-and-auth-connection
47
+ name: Auth on a connection, headers in the step
48
+ description: Call a basic-auth endpoint through a connection that holds the credentials, with the step contributing only non-secret headers.
49
+
50
+ tags: [recipes, http, transform]
51
+
52
+ requires:
53
+ blocks:
54
+ - http.request
55
+ - transform.jq
56
+
57
+ # Carried inline so this recipe runs standalone. The pair is Postman's own public demo
58
+ # credential, documented at https://postman-echo.com; a real one is created on the instance
59
+ # and named by code, and an instance refuses to store a document that carries a connection
60
+ # at all.
61
+ connections:
62
+ echo-basic:
63
+ kind: http
64
+ config:
65
+ base_url: https://postman-echo.com
66
+ basic_username: postman
67
+ basic_password: password
68
+ timeout: 30s
69
+ # What dg connection check calls to answer green or red.
70
+ health_path: /get
71
+
72
+ params:
73
+ type: object
74
+ properties:
75
+ path:
76
+ type: string
77
+ default: /basic-auth
78
+ description: The path resolved against the connection's base URL.
79
+ correlation_id:
80
+ type: string
81
+ default: recipes-0001
82
+ description: A non-secret header the receiver wants, sent by the step.
83
+
84
+ steps:
85
+ call:
86
+ block: http.request
87
+ config:
88
+ # A code, resolved by the instance. The host, the credentials and the default timeout
89
+ # all come from it.
90
+ connection: echo-basic
91
+ path: ${params.path}
92
+ method: GET
93
+ headers:
94
+ # Not a credential: a routing and tracing header, which is what this map is for.
95
+ x-correlation-id: ${params.correlation_id}
96
+ accept: application/json
97
+
98
+ result:
99
+ block: transform.jq
100
+ depends_on: [call]
101
+ config:
102
+ input:
103
+ status: ${steps.call.output.status}
104
+ body: ${steps.call.output.body}
105
+ program: |
106
+ # /basic-auth answers {"authenticated": true}; /get echoes the request instead, and
107
+ # its .headers shows the Authorization header the connection contributed.
108
+ {status,
109
+ authenticated: (.body.authenticated // null),
110
+ saw_authorization: ((.body.headers.authorization // null) != null)}