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,111 @@
1
+ # Two fan-outs over one grid: item three of the second reads item three of the first.
2
+ #
3
+ # A fanned step's output is the list of its items' outputs, so a step that depends on a
4
+ # fan-out sees the whole batch. That is right for a join and wrong for everything that is
5
+ # still per item: writing one file per district, calling one endpoint per region, sending one
6
+ # message per order. Those want the matching item, not the list.
7
+ #
8
+ # for_each: ${steps.<name>.items} is that. The step maps over the grid the named fan-out
9
+ # already has instead of a grid of its own, and the two are paired by position: item 3 here is
10
+ # item 3 there, ${item} is the same element in both, and ${steps.<name>.item.output} is the
11
+ # matching item's output.
12
+ #
13
+ # Three things the pairing buys, and one it costs:
14
+ #
15
+ # 1. Cardinality is still fixed when the run is created. The grid comes from a step that was
16
+ # itself expanded then, so nothing here waits on execution to know how wide it is.
17
+ # 2. The names must be a direct dependency: the adopted step goes in depends_on. Pairing
18
+ # across a step that is not waited for would read an item that has not run.
19
+ # 3. Adoption chains. A third step may map over the second's items and still reach the
20
+ # first's, because they are all one grid.
21
+ # 4. The cost: an item whose match did not succeed is SKIPPED, not failed and not run. Its
22
+ # input never existed, so there is nothing to attempt. The rest of the grid carries on.
23
+ #
24
+ # Hop by hop:
25
+ #
26
+ # shape transform.jq, once per region. Under items: continue a region that refuses is
27
+ # recorded against its own item and the others finish.
28
+ # write_one storage.write, mapping over shape's grid rather than a list of its own. One
29
+ # file per region, named for ${item}, holding ${steps.shape.item.output.value} --
30
+ # that region's own object, not the batch.
31
+ # manifest the join, with no for_each at all: one attempt over the whole list of files.
32
+ #
33
+ # EXPECT THIS RUN TO SUCCEED in about a second, with three files written and a manifest naming
34
+ # all three.
35
+ #
36
+ # To change it: -p refuse=west makes shape's west item fail, which skips write_one's west item
37
+ # rather than failing it, leaves the other two written, and ends the run
38
+ # completed_with_errors with a manifest of two files.
39
+ #
40
+ # dg run --local examples/patterns/fan-out-item-wise.yaml
41
+ # dg run --local examples/patterns/fan-out-item-wise.yaml -p refuse=west # completed_with_errors
42
+
43
+ format: dirigent/v1
44
+ kind: pipeline
45
+ code: fan-out-item-wise
46
+ name: One item to one item
47
+ description: |
48
+ `for_each: ${steps.<name>.items}` maps a step over another fan-out's grid, so the two are
49
+ paired by item position and `${steps.<name>.item.output}` is the matching item's output.
50
+
51
+ An item whose match did not succeed is skipped rather than failed, and the rest of the grid
52
+ carries on.
53
+
54
+ tags: [patterns, storage, transform, fan-out, references, starter]
55
+
56
+ requires:
57
+ blocks:
58
+ - storage.write
59
+ - transform.jq
60
+
61
+ params:
62
+ type: object
63
+ properties:
64
+ regions:
65
+ type: array
66
+ default: [east, west, north]
67
+ minItems: 1
68
+ maxItems: 16
69
+ items:
70
+ type: string
71
+ description: One item, one file, per element.
72
+ refuse:
73
+ type: string
74
+ default: ""
75
+ description: A region whose shape step fails, so its write is skipped rather than run.
76
+
77
+ steps:
78
+ shape:
79
+ block: transform.jq
80
+ for_each: "${params.regions}"
81
+ items: continue
82
+ config:
83
+ input:
84
+ region: "${item}"
85
+ refuse: "${params.refuse}"
86
+ # error() is how a jq program fails one item deliberately; without the refuse knob this
87
+ # program is just the object-building half.
88
+ program: |
89
+ if .region == .refuse then error("region \(.region) refused the export")
90
+ else {region: .region, rows: [{station: "\(.region)-st-0", celsius: -4}]}
91
+ end
92
+
93
+ write_one:
94
+ block: storage.write
95
+ depends_on: [shape]
96
+ # The whole point of the file: this step has no grid of its own, it takes shape's.
97
+ for_each: "${steps.shape.items}"
98
+ config:
99
+ # ${item} is shape's element, so two items never write the same key.
100
+ target: "${run.scratch}/regions/${item}.json"
101
+ # ...and this is that element's own output, not the list of every item's.
102
+ value: "${steps.shape.item.output.value}"
103
+
104
+ manifest:
105
+ block: transform.jq
106
+ depends_on: [write_one]
107
+ # No for_each, so this is the join: one attempt, and the batch arrives as a list.
108
+ config:
109
+ input: "${steps.write_one.output}"
110
+ program: |
111
+ {files: length, uris: [.[].uri], total_bytes: ([.[].bytes_written] | add)}
@@ -0,0 +1,81 @@
1
+ # for_each as a literal list: the smallest fan-out there is.
2
+ #
3
+ # for_each takes one of two things: a reference to a list, or a list written out in the
4
+ # document. This file is the second, which is the right form when the elements are part of the
5
+ # pipeline's shape rather than part of a run's input -- the three environments you deploy to,
6
+ # the four quarters, the fixed set of feeds. Nobody should have to pass those in to get an
7
+ # ordinary run.
8
+ #
9
+ # What a fan-out actually is: one step definition, N run items, one attempt each, all of them
10
+ # claimable at once. The item grid exists from the moment the run is visible, because
11
+ # for_each is expanded when the run is CREATED. That has a consequence worth knowing before
12
+ # you reach for it: for_each may read params, run, and another fan-out's grid as
13
+ # ${steps.<step>.items}, but never a step's output, because the cardinality has to be known
14
+ # before anything executes. A fan-out over a list fetched at run time is not expressible, and
15
+ # that is deliberate rather than missing.
16
+ #
17
+ # Hop by hop:
18
+ #
19
+ # greet transform.jq, once per name in the literal list. ${item} is the element, and
20
+ # here that is a plain string.
21
+ # collect runs ONCE, not once per item, because it has no for_each of its own. It reads
22
+ # ${steps.greet.output}, which is the list of the items' outputs in item order.
23
+ #
24
+ # EXPECT THIS RUN TO SUCCEED in about a second. Nothing here touches the network or runs code
25
+ # on the worker, so it needs no allowlist and no connection.
26
+ #
27
+ # To change it: fan-out-from-params.yaml is the same shape with the list supplied by the
28
+ # caller, fan-out-nested-objects.yaml is the same shape with objects as elements, and
29
+ # fan-out-item-wise.yaml is a second step mapping over this one's grid instead of joining it.
30
+ #
31
+ # dg run --local examples/patterns/fan-out-literal-list.yaml
32
+
33
+ format: dirigent/v1
34
+ kind: pipeline
35
+ code: fan-out-literal-list
36
+ name: A fan-out over a literal list
37
+ description: |
38
+ `for_each` written as a list in the document: one step definition, one run item per
39
+ element, all claimable at once.
40
+
41
+ A literal list is the right form when the elements are part of the pipeline's shape
42
+ rather than part of a run's input.
43
+
44
+ tags: [patterns, transform, fan-out, graph]
45
+
46
+ requires:
47
+ blocks:
48
+ - transform.jq
49
+
50
+ params:
51
+ type: object
52
+ properties:
53
+ greeting:
54
+ type: string
55
+ description: The word each item is greeted with; it is the same for every item.
56
+ default: hei
57
+
58
+ steps:
59
+ greet:
60
+ block: transform.jq
61
+ # Written out here rather than referenced, because these three are what this pipeline is
62
+ # about. A list that changes per run belongs in params instead.
63
+ for_each: [oslo, bergen, tromso]
64
+ config:
65
+ input:
66
+ # ${item} is this run item's element. It is only in scope inside a step that fans
67
+ # out; anywhere else it is an unknown reference and the attempt is rejected.
68
+ city: "${item}"
69
+ greeting: "${params.greeting}"
70
+ program: |
71
+ {city, line: "\(.greeting), \(.city)"}
72
+
73
+ collect:
74
+ block: transform.jq
75
+ depends_on: [greet]
76
+ # One attempt, not three. A step joins a fan-out simply by having no for_each of its own.
77
+ config:
78
+ # The list of the items' outputs, in item order.
79
+ input: "${steps.greet.output}"
80
+ program: |
81
+ {lines: [.[].value.line], cities: length}
@@ -0,0 +1,107 @@
1
+ # An item that is an object, and the fields of it a step reaches into.
2
+ #
3
+ # An element of a for_each list is any JSON value, and once it is an object the item stops
4
+ # being a label and becomes a small record. ${item.code} reaches a field, ${item.limits.max_ms}
5
+ # reaches a nested one, and ${item} on its own is still the whole object -- so one element can
6
+ # carry everything one item's work needs instead of forcing three parallel lists that have to
7
+ # stay in the same order.
8
+ #
9
+ # The reference rule that makes this work: a reference that is the ENTIRE value resolves to
10
+ # the typed value, so "${item.limits.max_ms}" is the integer 2000 downstream and not the text
11
+ # "2000". A reference inside a larger string interpolates as text instead, which is why the
12
+ # URL below reads as a URL and the threshold reads as a number.
13
+ #
14
+ # A missing field is not an empty string. ${item.nickname} on an element that has no nickname
15
+ # raises, and the engine settles that attempt as rejected rather than quietly proceeding with
16
+ # nothing -- so every element in the list must carry every field the step names. Give the
17
+ # optional ones a default in the list itself.
18
+ #
19
+ # Hop by hop:
20
+ #
21
+ # probe one item per feed. Each item posts its own code and its own latency budget, so
22
+ # the three items do genuinely different work from one step definition. Postman
23
+ # Echo answers with what it was sent, which is how each item's element comes back
24
+ # attached to that item's output.
25
+ # verdict a join over the batch, comparing each feed's measured round trip against the
26
+ # budget that travelled with it.
27
+ #
28
+ # EXPECT THIS RUN TO SUCCEED in about two seconds, with three feeds and a verdict each. The
29
+ # items run side by side, so the run costs one round trip rather than three.
30
+ #
31
+ # To change it: add a feed to the list, or lower a budget below the real round trip and watch
32
+ # that feed's verdict flip to slow. The budget lives on the element rather than in params
33
+ # because it belongs to one feed and not to the run.
34
+ #
35
+ # dg run --local examples/patterns/fan-out-nested-objects.yaml
36
+
37
+ format: dirigent/v1
38
+ kind: pipeline
39
+ code: fan-out-nested-objects
40
+ name: Items that are objects
41
+ description: |
42
+ A `for_each` element is any JSON value. Once it is an object, `${item.code}` reaches a
43
+ field and `${item.limits.max_ms}` a nested one, so one element carries everything one item's
44
+ work needs.
45
+
46
+ A whole reference resolves to the typed value; a missing field is a rejected attempt, not
47
+ an empty string.
48
+
49
+ tags: [patterns, http, transform, fan-out, references]
50
+
51
+ requires:
52
+ blocks:
53
+ - http.request
54
+ - transform.jq
55
+
56
+ params:
57
+ type: object
58
+ properties:
59
+ tolerance_ms:
60
+ type: integer
61
+ description: Slack added to every feed's own threshold before the verdict is taken.
62
+ default: 500
63
+ minimum: 0
64
+ maximum: 5000
65
+
66
+ steps:
67
+ probe:
68
+ block: http.request
69
+ # Three records, not three parallel lists. Nothing has to stay in step with anything.
70
+ for_each:
71
+ - {code: cases, path: post, limits: {max_ms: 2000}}
72
+ - {code: climate, path: post, limits: {max_ms: 2500}}
73
+ - {code: population, path: post, limits: {max_ms: 400}}
74
+ config:
75
+ # Interpolated into a larger string, so the field is rendered as text inside the URL.
76
+ url: "https://postman-echo.com/${item.path}"
77
+ method: POST
78
+ body:
79
+ feed: "${item.code}"
80
+ # The nested field, carried in the request so it comes back in the echo. That is how a
81
+ # per-item value reaches the join: the join sees outputs, not elements, so anything an
82
+ # element knows has to be inside an item's output to survive the fan-in. A whole
83
+ # reference stays typed, so this arrives as the integer it was written as.
84
+ max_ms: "${item.limits.max_ms}"
85
+
86
+ verdict:
87
+ block: transform.jq
88
+ depends_on: [probe]
89
+ config:
90
+ input:
91
+ measured: "${steps.probe.output}"
92
+ tolerance_ms: "${params.tolerance_ms}"
93
+ # json is the typed echo of the body that was posted, so max_ms is still a number here
94
+ # and needs no tonumber. A query string would have come back as text.
95
+ program: |
96
+ .tolerance_ms as $tolerance
97
+ | {
98
+ feeds: [
99
+ .measured[]
100
+ | {
101
+ feed: .body.json.feed,
102
+ duration_ms,
103
+ budget_ms: (.body.json.max_ms + $tolerance)
104
+ }
105
+ | .verdict = (if .duration_ms <= .budget_ms then "within budget" else "slow" end)
106
+ ]
107
+ }
@@ -0,0 +1,86 @@
1
+ # Fan out, then reduce: N items in, one aggregate out, and no keyword for either.
2
+ #
3
+ # The join is not a construct. A step joins a fan-out by depending on it and NOT declaring a
4
+ # for_each of its own -- that is the whole mechanism, and it is why nothing here says "join"
5
+ # or "reduce". One step definition, one attempt, and the batch arrives as a list.
6
+ #
7
+ # Three facts about what the join sees:
8
+ #
9
+ # 1. ${steps.measure.output} is a JSON array of the items' outputs, in item order.
10
+ # 2. It is a value like any other, so a jq program reduces it the way jq reduces anything.
11
+ # 3. Under items: continue a failed item is absent from that array, so the join must compute
12
+ # from what is there rather than from what was asked for. add/length below is right
13
+ # whatever survived; dividing by a hard-coded four would not be.
14
+ #
15
+ # Hop by hop:
16
+ #
17
+ # measure a literal list of station readings, fanned out. Each item scales one reading and
18
+ # emits a small object. This stands in for a fetch, and it needs no network, so the
19
+ # aggregation is what you watch rather than the HTTP.
20
+ # summary the join. One attempt, the whole array, count and min and max and mean out of it.
21
+ #
22
+ # EXPECT THIS RUN TO SUCCEED in about a second, with a summary over four stations.
23
+ #
24
+ # To change it: add a station to the literal list and nothing else needs editing -- the join is
25
+ # already written for whatever arrives. fan-in.yaml on the graph/ shelf is the same shape
26
+ # shipping the raw batch onward instead of reducing it.
27
+ #
28
+ # dg run --local examples/patterns/fan-out-then-join.yaml
29
+ # dg run --local examples/patterns/fan-out-then-join.yaml -p scale=2
30
+
31
+ format: dirigent/v1
32
+ kind: pipeline
33
+ code: fan-out-then-join
34
+ name: Fan out, then reduce
35
+ description: |
36
+ A step joins a fan-out by depending on it and having no `for_each` of its own: one attempt,
37
+ and the batch arrives as a JSON array of the items' outputs.
38
+
39
+ The aggregate is computed from what is in that array, never from the width that was asked
40
+ for.
41
+
42
+ tags: [patterns, transform, fan-out, graph]
43
+
44
+ requires:
45
+ blocks:
46
+ - transform.jq
47
+
48
+ params:
49
+ type: object
50
+ properties:
51
+ scale:
52
+ type: number
53
+ description: A factor every reading is multiplied by before the summary reduces them.
54
+ default: 1.0
55
+ minimum: 0.1
56
+ maximum: 10.0
57
+
58
+ steps:
59
+ measure:
60
+ block: transform.jq
61
+ # Objects rather than strings, so one item carries a whole reading. ${item.station} and
62
+ # ${item.celsius} reach into it; fan-out-nested-objects.yaml is that on its own.
63
+ for_each:
64
+ - {station: fornebu, celsius: 4.5}
65
+ - {station: blindern, celsius: 3.1}
66
+ - {station: gardermoen, celsius: -1.2}
67
+ - {station: tryvann, celsius: -4.8}
68
+ config:
69
+ input:
70
+ station: "${item.station}"
71
+ # A whole reference resolves to the typed value, so this is a number downstream and
72
+ # not the text of one. That is what lets the program below multiply it.
73
+ celsius: "${item.celsius}"
74
+ scale: "${params.scale}"
75
+ program: |
76
+ {station, celsius: (.celsius * .scale)}
77
+
78
+ summary:
79
+ block: transform.jq
80
+ depends_on: [measure]
81
+ # No for_each: this is the join, and one attempt sees all four items.
82
+ config:
83
+ input: "${steps.measure.output}"
84
+ program: |
85
+ [.[].value.celsius] |
86
+ {stations: length, min: min, max: max, mean: (add / length)}
@@ -0,0 +1,119 @@
1
+ # Turning one block family's logs up, without turning the whole run's logs up.
2
+ #
3
+ # A run keeps info and above by default. --log-level changes that FOR ONE RUN, and it is a
4
+ # property of the run rather than of the document -- which is the right place for it: how
5
+ # loudly a pipeline logs is an operational question asked while something is being
6
+ # investigated, not a fact about the pipeline that belongs in git and in a review.
7
+ #
8
+ # THE FLAG TAKES TWO FORMS, and it repeats:
9
+ #
10
+ # --log-level debug every block. The blunt instrument.
11
+ # --log-level PATTERN=LEVEL one family. The one worth learning.
12
+ #
13
+ # PATTERN is an fnmatch over the BLOCK ID, and the most specific match wins -- the longest
14
+ # pattern, with an exact id beating any wildcard. So:
15
+ #
16
+ # --log-level 'http.*=debug' both HTTP blocks, nothing else
17
+ # --log-level 'http.request=debug' one block, even alongside a looser http.* rule
18
+ # --log-level debug --log-level 'transform.*=warning'
19
+ # everything loud, except the jq steps
20
+ #
21
+ # WHY IT IS PER-FAMILY AND NOT PER-STEP: a level is about a block's own chattiness, and the
22
+ # same block is usually several steps. Turning http.* up is asking one implementation to
23
+ # explain itself, which is what an investigation actually wants.
24
+ #
25
+ # This document exists to give the flag something to bite on: three block families, several
26
+ # steps in each, all of them cheap. Run it once plain and once loud and compare the log lines
27
+ # -- and note that the RECORDS are identical either way. Only the logs change; a run's outcome
28
+ # never depends on how much it said about itself.
29
+ #
30
+ # Hop by hop:
31
+ #
32
+ # fetch_one, fetch_two two http.request steps, so an http.* pattern has more than one
33
+ # thing to affect.
34
+ # shape, tally two transform.jq steps, the family to turn DOWN when the HTTP is
35
+ # what is being investigated.
36
+ # settle a value.const, a third family, so a wildcard has a boundary.
37
+ #
38
+ # EXPECT THIS RUN TO SUCCEED in about two seconds, whichever levels are set.
39
+ #
40
+ # dg run --local examples/patterns/log-levels.yaml
41
+ # dg run --local examples/patterns/log-levels.yaml --log-level 'http.*=debug'
42
+ # dg run --local examples/patterns/log-levels.yaml --log-level debug --log-level 'transform.*=warning'
43
+ # dg run --local examples/patterns/log-levels.yaml --log-level 'http.*=debug' | dg format
44
+ #
45
+ # A schedule carries its own log levels too, which is how a nightly firing can be made
46
+ # permanently loud without every ad hoc run being loud with it.
47
+
48
+ format: dirigent/v1
49
+ kind: pipeline
50
+ code: log-levels
51
+ name: One family, turned up
52
+ description: |
53
+ `--log-level` is a **run** setting, not a document one: `debug` for everything, or
54
+ `PATTERN=LEVEL` for one block family, repeatable.
55
+
56
+ The pattern is an fnmatch over the block id and the most specific match wins. The records
57
+ a run emits are the same either way; only the log lines change.
58
+
59
+ tags: [patterns, http, transform, observability]
60
+
61
+ requires:
62
+ blocks:
63
+ - http.request
64
+ - transform.jq
65
+ - value.const
66
+
67
+ params:
68
+ type: object
69
+ properties:
70
+ dataset:
71
+ type: string
72
+ description: Echoed by both calls, so a debug line names which step made which request.
73
+ default: cases
74
+
75
+ steps:
76
+ fetch_one:
77
+ block: http.request
78
+ config:
79
+ url: https://postman-echo.com/get
80
+ query:
81
+ dataset: "${params.dataset}"
82
+ part: "1"
83
+
84
+ fetch_two:
85
+ block: http.request
86
+ # No edge to fetch_one, so the two run side by side and their log lines interleave --
87
+ # which is exactly when knowing that every line carries its step name starts to matter.
88
+ config:
89
+ url: https://postman-echo.com/get
90
+ query:
91
+ dataset: "${params.dataset}"
92
+ part: "2"
93
+
94
+ shape:
95
+ block: transform.jq
96
+ depends_on: [fetch_one, fetch_two]
97
+ config:
98
+ input:
99
+ one: "${steps.fetch_one.output.body.args}"
100
+ two: "${steps.fetch_two.output.body.args}"
101
+ program: |
102
+ {parts: [.one.part, .two.part], dataset: .one.dataset}
103
+
104
+ tally:
105
+ block: transform.jq
106
+ depends_on: [shape]
107
+ config:
108
+ input: "${steps.shape.output.value}"
109
+ program: |
110
+ {dataset, parts: (.parts | length)}
111
+
112
+ settle:
113
+ block: value.const
114
+ depends_on: [tally]
115
+ # A third family, so 'http.*' and 'transform.*' each have something they plainly do not
116
+ # cover.
117
+ config:
118
+ value:
119
+ note: the run's records do not change with its log level
@@ -0,0 +1,140 @@
1
+ # Where a value lives: in a step's output, in the run's scratch space, or in storage.
2
+ #
3
+ # A value travels in a step's output. That is the whole of it: a step reads
4
+ # ${steps.x.output.field} from a step it depends on, and no block reads or writes storage to
5
+ # move a value around. The two ends of a run are the exception, and they are blocks of their
6
+ # own -- storage.read is the only way a value comes in from storage, and storage.write the
7
+ # only way one goes out.
8
+ #
9
+ # THE ENGINE'S OWN DECISION is separate from both, and it is about size rather than about
10
+ # what a document said. Every successful attempt keeps its output whole, whatever its size,
11
+ # so a reference always resolves. Alongside it the engine files a copy: at or below
12
+ # inline_artifact_max (16384 bytes by default) the copy is inlined, and above it the
13
+ # canonical JSON is streamed to the run's scratch prefix and the attempt records the URI it
14
+ # went to. Nothing in this document chooses that, and nothing downstream has to know which
15
+ # happened.
16
+ #
17
+ # dg runs show <run-id> the attempt says where its output's copy went
18
+ # DIRIGENT_INLINE_ARTIFACT_MAX the instance setting that moves the line
19
+ #
20
+ # WHERE A URI MAY POINT. ${run.scratch} is this run's own directory under the instance's
21
+ # artifact root. file:// URIs are refused outside that root, so a stored pipeline can never
22
+ # be turned into an arbitrary-file read, and a run cannot write over another run's work. An
23
+ # s3:// URI goes to whichever connection the instance binds that scheme to.
24
+ #
25
+ # Hop by hop:
26
+ #
27
+ # fetch an http.request. The answer is a few hundred bytes, so its copy is inlined,
28
+ # and the next step reads .body straight out of the output.
29
+ # bulk jq builds four thousand rows from it. The serialised output is far over the
30
+ # default cap, so the engine spills that copy to the run's scratch.
31
+ # count reads ${steps.bulk.output.value} anyway. A spilled copy changes nothing for a
32
+ # reference: the attempt kept the value whole.
33
+ # keep storage.write, the one way out. The rows become an object with a URI.
34
+ # read_back storage.read, the one way back in. The object becomes a value again.
35
+ # compare the small answer and the round-tripped rows, side by side.
36
+ #
37
+ # EXPECT THIS RUN TO SUCCEED in about three seconds, with bulk's output spilled and every
38
+ # reference to it resolving regardless.
39
+ #
40
+ # dg run --local examples/patterns/outputs-inline-vs-storage.yaml
41
+
42
+ format: dirigent/v1
43
+ kind: pipeline
44
+ code: outputs-inline-vs-storage
45
+ name: A value, and the storage on either side of it
46
+ description: |
47
+ A value travels in a step's output. `storage.read` is the only way one comes in from
48
+ storage and `storage.write` the only way one goes out.
49
+
50
+ Where the engine files its copy of an output is a separate question, decided by size:
51
+ at or below `inline_artifact_max` (16KB by default) it is inlined on the attempt, and
52
+ above it spilled to the run's scratch space. A reference resolves either way.
53
+
54
+ tags: [patterns, http, storage, transform, outputs]
55
+
56
+ requires:
57
+ blocks:
58
+ - http.request
59
+ - transform.jq
60
+ - storage.write
61
+ - storage.read
62
+
63
+ params:
64
+ type: object
65
+ properties:
66
+ dataset:
67
+ type: string
68
+ description: Echoed back by the endpoint, so the answer has something recognisable in it.
69
+ default: cases
70
+ rows:
71
+ type: integer
72
+ description: How many rows the bulk step builds. Above about three hundred the output spills.
73
+ default: 4000
74
+ minimum: 1
75
+
76
+ steps:
77
+ fetch:
78
+ block: http.request
79
+ config:
80
+ url: https://postman-echo.com/get
81
+ query:
82
+ dataset: "${params.dataset}"
83
+
84
+ bulk:
85
+ block: transform.jq
86
+ depends_on: [fetch]
87
+ config:
88
+ input:
89
+ dataset: "${steps.fetch.output.body.args.dataset}"
90
+ rows: "${params.rows}"
91
+ program: |
92
+ . as {$dataset, $rows} | [range($rows) | {row: ., dataset: $dataset, seen_at: "postman-echo"}]
93
+
94
+ count:
95
+ block: transform.jq
96
+ depends_on: [bulk]
97
+ config:
98
+ # The reference is the same reference it would be for a four-field output. Whether the
99
+ # engine inlined the copy or spilled it is not something a document can see.
100
+ input: "${steps.bulk.output.value}"
101
+ program: |
102
+ {rows: length, first: .[0]}
103
+
104
+ keep:
105
+ block: storage.write
106
+ depends_on: [bulk]
107
+ config:
108
+ target: "${run.scratch}/rows/${params.dataset}.json"
109
+ value: "${steps.bulk.output.value}"
110
+
111
+ read_back:
112
+ block: storage.read
113
+ depends_on: [keep]
114
+ config:
115
+ source: "${steps.keep.output.uri}"
116
+ # This step's own guard on how much it will hold. A humane size, and an object bigger
117
+ # than it fails the step rather than exhausting the worker; bytes nobody has to look
118
+ # at move with storage.copy instead, which this does not bound.
119
+ max_size: 8mb
120
+
121
+ compare:
122
+ block: transform.jq
123
+ depends_on: [fetch, count, keep, read_back]
124
+ config:
125
+ input:
126
+ inline_dataset: "${steps.fetch.output.body.args.dataset}"
127
+ counted: "${steps.count.output.value.rows}"
128
+ written_uri: "${steps.keep.output.uri}"
129
+ written_bytes: "${steps.keep.output.bytes_written}"
130
+ read_bytes: "${steps.read_back.output.bytes_read}"
131
+ round_tripped: "${steps.read_back.output.value}"
132
+ program: |
133
+ {
134
+ inline_dataset,
135
+ counted,
136
+ written_uri,
137
+ written_bytes,
138
+ read_bytes,
139
+ same: (.counted == (.round_tripped | length))
140
+ }