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,117 @@
1
+ # POST a file that lives in storage: read it, then send what the read brought back.
2
+ #
3
+ # In: a table of readings generated inline and written to the run's scratch space.
4
+ # Out: the echo service's view of the upload -- the size it received and the content type it
5
+ # was told -- beside the size the file actually is.
6
+ #
7
+ # A file is bytes at a URI, and a request sends a value, so the hop between them is a
8
+ # storage.read step. Four hops:
9
+ #
10
+ # build transform.jq, the payload a real pipeline would have fetched.
11
+ # file storage.write, the value to a URI. Output: uri, bytes_written, content_type.
12
+ # load storage.read, the same URI. The object is application/json, so it comes back
13
+ # as `value`; max_size is how much of it this step will hold, and an object
14
+ # past it is refused rather than truncated.
15
+ # upload http.request. body is that value, serialised and sent as JSON by the block, so
16
+ # nothing here builds a request out of strings.
17
+ #
18
+ # HOW BIG A FILE THIS SENDS. Everything above passes through the worker, so the file has to
19
+ # fit in it twice over: once in the read, once in the request. That is the trade for being
20
+ # able to look at what is sent. Bytes nobody has to look at move between URIs with
21
+ # storage.copy instead, bounded by the storage rather than by the worker, and a body coming
22
+ # back the other way is bounded by max_response.
23
+ #
24
+ # The file is addressed as ${steps.file.output.uri} rather than typed twice, so one step
25
+ # decides where it went and a rename is a one-line change. A missing object is a rejected
26
+ # read, not a retried one: calling again cannot make a file appear that was never written.
27
+ #
28
+ # To change it: point the write at s3://<bucket>/... and the read follows it, because the
29
+ # scheme is the only thing that changes.
30
+ #
31
+ # dg run --local examples/recipes/http-post-file-from-storage.yaml
32
+ # dg run --local examples/recipes/http-post-file-from-storage.yaml -p stations=500
33
+
34
+ format: dirigent/v1
35
+ kind: pipeline
36
+ code: http-post-file-from-storage
37
+ name: POST a file read out of storage
38
+ description: Write a file into the run's scratch space, read it back with storage.read, and POST what the read brought back.
39
+
40
+ tags: [recipes, http, storage, transform]
41
+
42
+ requires:
43
+ blocks:
44
+ - transform.jq
45
+ - storage.write
46
+ - storage.read
47
+ - http.request
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 the uploaded file is named for.
57
+ stations:
58
+ type: integer
59
+ minimum: 1
60
+ default: 40
61
+ description: How many rows the file holds, which is what makes it big enough to matter.
62
+
63
+ steps:
64
+ build:
65
+ block: transform.jq
66
+ config:
67
+ input:
68
+ count: ${params.stations}
69
+ day: ${params.day}
70
+ program: |
71
+ . as {$count, $day}
72
+ | {day: $day,
73
+ readings: [range($count)
74
+ | {station: "st-\(.)", celsius: (. % 17) - 5}]}
75
+
76
+ file:
77
+ block: storage.write
78
+ depends_on: [build]
79
+ config:
80
+ target: ${run.scratch}/readings-${params.day}.json
81
+ value: ${steps.build.output.value}
82
+
83
+ load:
84
+ block: storage.read
85
+ depends_on: [file]
86
+ config:
87
+ source: ${steps.file.output.uri}
88
+ max_size: 8mb
89
+
90
+ upload:
91
+ block: http.request
92
+ depends_on: [load]
93
+ config:
94
+ url: https://postman-echo.com/post
95
+ method: POST
96
+ # A value, so the block serialises it and sends it as application/json. An endpoint
97
+ # that wants some other type is told with content_type.
98
+ body: ${steps.load.output.value}
99
+ timeout: 30s
100
+
101
+ receipt:
102
+ block: transform.jq
103
+ depends_on: [file, load, upload]
104
+ config:
105
+ input:
106
+ written: ${steps.file.output.bytes_written}
107
+ read: ${steps.load.output.bytes_read}
108
+ echoed: ${steps.upload.output.body}
109
+ program: |
110
+ {written, read,
111
+ # What the receiver was told the body was, and how much of it arrived.
112
+ content_type: .echoed.headers["content-type"],
113
+ received: (.echoed.headers["content-length"] | tonumber),
114
+ rows: (.echoed.json.readings | length),
115
+ # The file and the read agree on one number, and the receiver counted the same
116
+ # bytes back out of the request.
117
+ intact: (.written == .read and .read == (.echoed.headers["content-length"] | tonumber))}
@@ -0,0 +1,105 @@
1
+ # POST a JSON body built from upstream work, and read the receipt.
2
+ #
3
+ # In: a batch of readings built inline.
4
+ # Out: the echo service's view of the POST -- the body it parsed and the headers it saw --
5
+ # alongside the status and the payload's own row count.
6
+ #
7
+ # body is a value, not a string. The block serialises it, sets the content type, and sends
8
+ # it, so a payload assembled by a jq step upstream travels as structure the whole way and
9
+ # nothing hand-builds JSON with string interpolation. That is what keeps a quote in a
10
+ # station name from becoming a syntax error on somebody else's parser.
11
+ #
12
+ # https://postman-echo.com/post answers with what it received, so the .json field of the
13
+ # response is this pipeline's own body coming back -- which is why the last step can compare
14
+ # what was sent with what arrived and report them side by side.
15
+ #
16
+ # Two headers are worth knowing about. The block sets content-type itself, so a header map
17
+ # is for what the receiver wants on top: a routing key, an idempotency key, an API version.
18
+ # A secret does not go here -- it goes on a connection, which is the whole point of
19
+ # http-headers-and-auth-connection.yaml.
20
+ #
21
+ # A POST is not idempotent, and the block declares itself so: the engine will spend a retry
22
+ # budget on one, and it is on the author to know whether the receiver deduplicates. An
23
+ # idempotency key in the headers is what makes that safe, and it belongs in the payload's
24
+ # own identity rather than in a random value minted per attempt.
25
+ #
26
+ # To change it: -p batch=3 sends a bigger payload; the shape of the document does not move.
27
+ #
28
+ # dg run --local examples/recipes/http-post-json-echo.yaml
29
+ # dg run --local examples/recipes/http-post-json-echo.yaml -p batch=3
30
+
31
+ format: dirigent/v1
32
+ kind: pipeline
33
+ code: http-post-json-echo
34
+ name: POST a JSON body
35
+ description: Build a payload upstream, POST it as structure rather than as text, and read the receipt the endpoint answers with.
36
+
37
+ tags: [recipes, http, transform]
38
+
39
+ requires:
40
+ blocks:
41
+ - transform.jq
42
+ - http.request
43
+
44
+ params:
45
+ type: object
46
+ properties:
47
+ batch:
48
+ type: integer
49
+ minimum: 1
50
+ default: 5
51
+ description: How many readings the payload carries.
52
+ day:
53
+ type: string
54
+ format: date
55
+ default: "2026-01-01"
56
+ description: The day the payload reports.
57
+
58
+ steps:
59
+ payload:
60
+ block: transform.jq
61
+ config:
62
+ input:
63
+ batch: ${params.batch}
64
+ day: ${params.day}
65
+ program: |
66
+ . as {$batch, $day}
67
+ | {day: $day,
68
+ source: "dirigent recipes",
69
+ readings: [range($batch)
70
+ | {station: "st-\(.)", celsius: (. * 2) - 4, note: "it's fine"}]}
71
+
72
+ publish:
73
+ block: http.request
74
+ depends_on: [payload]
75
+ config:
76
+ url: https://postman-echo.com/post
77
+ method: POST
78
+ # A value, serialised by the block. Nothing here builds JSON out of strings.
79
+ body: ${steps.payload.output.value}
80
+ headers:
81
+ # What the receiver asked for, on top of the content type the block sets itself.
82
+ x-batch-day: ${params.day}
83
+ # Derived from what is being sent rather than minted per attempt, so a retry of the
84
+ # same payload presents the same key.
85
+ idempotency-key: readings-${params.day}
86
+ timeout: 30s
87
+
88
+ receipt:
89
+ block: transform.jq
90
+ depends_on: [payload, publish]
91
+ config:
92
+ input:
93
+ sent: ${steps.payload.output.value}
94
+ status: ${steps.publish.output.status}
95
+ duration_ms: ${steps.publish.output.duration_ms}
96
+ echoed: ${steps.publish.output.body}
97
+ program: |
98
+ {status, duration_ms,
99
+ sent_rows: (.sent.readings | length),
100
+ # The echo service returns the parsed body under .json, so this is the payload
101
+ # having made the round trip.
102
+ received_rows: (.echoed.json.readings | length),
103
+ identical: (.echoed.json == .sent),
104
+ content_type: .echoed.headers["content-type"],
105
+ batch_day_header: .echoed.headers["x-batch-day"]}
@@ -0,0 +1,183 @@
1
+ # POST a batch, summarise the receipt, and let the run write the page a person reads.
2
+ #
3
+ # In: a count of stations and a count of days, as parameters. The readings are generated,
4
+ # so the payload is genuinely large without forty rows being typed out.
5
+ # Out: forty readings POSTed, a small summary of what came back, and the run's own report
6
+ # document: a markdown page rendered when the run settles, stored with the run, and
7
+ # read back with `dg runs report ID --markdown`.
8
+ #
9
+ # The four hops:
10
+ #
11
+ # readings a jq program builds the batch. range() twice is a cross product, so
12
+ # stations x days rows come out of five lines rather than out of a literal, and
13
+ # the arithmetic is fixed rather than random: the same parameters always POST
14
+ # the same bytes, which is what makes a rerun comparable to the first run.
15
+ # publish the batch goes out as structure. body is a value the block serialises, so a
16
+ # quote in a station name is not somebody else's parse error. The URL is here
17
+ # rather than on a connection because Postman Echo is public and this recipe
18
+ # should run with nothing applied first.
19
+ # summary postman-echo answers with what it received: the parsed body under .json and
20
+ # the headers it saw under .headers. This hop keeps a page's worth of it -- a
21
+ # row count, min, max and mean, the first few rows, and the headers as a list
22
+ # of {name, value} pairs, because a template loops over a list without asking
23
+ # anything of the sandbox.
24
+ # report the section at the bottom. It renders no data of its own: everything in it is
25
+ # the run's facts and the outputs the three steps left behind.
26
+ #
27
+ # Where the template's facts come from:
28
+ #
29
+ # run.* the engine's account of the run -- id, status, trigger, the two moments and
30
+ # the duration between them. Half of them are legitimately null depending on
31
+ # how a run ended, and a null renders as nothing rather than failing the page.
32
+ # pipeline.* the pipeline version this run pinned, so the page names what actually ran.
33
+ # steps[] one entry per step, in the order a person watched them, carrying outcome,
34
+ # attempts, duration_ms, output and output_bytes. output is the step's *last*
35
+ # attempt's value, and it is there only when the step ran: a skipped or
36
+ # never-reached step has none, which is why the page guards on it before
37
+ # reading into it.
38
+ # rendered_at when this page was rendered, which is the moment the run settled.
39
+ #
40
+ # steps is a list, in the order a person watched the run. step is the same facts by name --
41
+ # `step.summary.output.value` is the summary hop's output -- which is how this page reads one
42
+ # hop without walking the list, and it is why a step a template reads should be named for what
43
+ # it holds.
44
+ #
45
+ # The three filters are the ones that make a machine's number readable: `duration` turns
46
+ # milliseconds into 1m30s, `iso` renders a moment, `bytes` a size. `round` is Jinja's own.
47
+ #
48
+ # To change it: -p stations=20 -p days=10 POSTs two hundred rows and the page reports two
49
+ # hundred; -p rows_shown=10 lengthens the table without touching the payload. Deleting the
50
+ # report: section stops the document being rendered at all, and `report: {}` swaps this page
51
+ # for the built-in one -- report-built-in.yaml is that document.
52
+ #
53
+ # dg run --local examples/recipes/http-post-report.yaml
54
+ # dg apply examples/recipes/http-post-report.yaml
55
+ # dg run http-post-report --watch
56
+ # dg runs report RUN_ID --markdown
57
+
58
+ format: dirigent/v1
59
+ kind: pipeline
60
+ code: http-post-report
61
+ name: POST a batch and report on it
62
+ description: POST a generated batch to an echo service, summarise the receipt, and render the run's own markdown report from the run's facts and its steps' outputs.
63
+
64
+ tags: [recipes, http, transform, report, starter]
65
+
66
+ requires:
67
+ blocks:
68
+ - transform.jq
69
+ - http.request
70
+
71
+ params:
72
+ type: object
73
+ properties:
74
+ stations:
75
+ type: integer
76
+ minimum: 1
77
+ default: 8
78
+ description: How many stations report.
79
+ days:
80
+ type: integer
81
+ minimum: 1
82
+ default: 5
83
+ description: How many days each station reports for.
84
+ rows_shown:
85
+ type: integer
86
+ minimum: 1
87
+ default: 5
88
+ description: How many rows the report's table carries.
89
+
90
+ steps:
91
+ readings:
92
+ block: transform.jq
93
+ config:
94
+ input:
95
+ stations: ${params.stations}
96
+ days: ${params.days}
97
+ program: |
98
+ . as {$stations, $days}
99
+ | {source: "dirigent recipes",
100
+ readings: [range($days) as $day
101
+ | range($stations) as $station
102
+ | {station: "st-\($station)",
103
+ day: ("2026-01-0" + ($day + 1 | tostring)),
104
+ # Fixed arithmetic, so the same parameters POST the same bytes.
105
+ celsius: ((($station * 7) + ($day * 3)) % 19) - 4}]}
106
+
107
+ publish:
108
+ block: http.request
109
+ depends_on: [readings]
110
+ config:
111
+ url: https://postman-echo.com/post
112
+ method: POST
113
+ # A value the block serialises. Nothing here builds JSON out of strings.
114
+ body: ${steps.readings.output.value}
115
+ headers:
116
+ # A header the report's own table has something to show. Headers are strings, so
117
+ # a count is interpolated into one rather than passed as a number.
118
+ x-report-batch: ${params.stations}-stations-${params.days}-days
119
+ timeout: 30s
120
+
121
+ summary:
122
+ block: transform.jq
123
+ depends_on: [publish]
124
+ config:
125
+ input:
126
+ shown: ${params.rows_shown}
127
+ status: ${steps.publish.output.status}
128
+ echoed: ${steps.publish.output.body}
129
+ program: |
130
+ . as {$shown, $status, $echoed}
131
+ | ($echoed.json.readings | map(.celsius)) as $values
132
+ | {status: $status,
133
+ rows: ($echoed.json.readings | length),
134
+ min: ($values | min),
135
+ max: ($values | max),
136
+ mean: (($values | add) / ($values | length)),
137
+ first_rows: ($echoed.json.readings[:$shown]),
138
+ # A list rather than a map: a template loops over a list with no filter at all.
139
+ headers: ($echoed.headers | to_entries | map({name: .key, value: (.value | tostring)}))}
140
+
141
+ report:
142
+ template: |
143
+ # {{ pipeline.name or pipeline.code }}: {{ run.status }}
144
+
145
+ - run: `{{ run.id }}`
146
+ - pipeline: `{{ pipeline.code }}` version {{ pipeline.version }}
147
+ - trigger: {{ run.trigger }}
148
+ - started: {{ run.started_at | iso }}
149
+ - finished: {{ run.finished_at | iso }}
150
+ - duration: {{ run.duration_ms | duration }}
151
+ - rendered: {{ rendered_at | iso }}
152
+
153
+ ## Steps
154
+
155
+ | step | outcome | attempts | took | output |
156
+ | --- | --- | --- | --- | --- |
157
+ {% for one in steps %}
158
+ | {{ one.step }} | {{ one.outcome }} | {{ one.attempts }} | {{ one.duration_ms | duration }} | {{ one.output_bytes | bytes }} |
159
+ {% endfor %}
160
+
161
+ ## What came back
162
+
163
+ {% set echoed = step.summary.output.value %}
164
+ {% if echoed %}
165
+ The echo service answered {{ echoed.status }} and parsed {{ echoed.rows }} readings back,
166
+ between {{ echoed.min }}C and {{ echoed.max }}C, averaging {{ echoed.mean | round(1) }}C.
167
+
168
+ | station | day | celsius |
169
+ | --- | --- | --- |
170
+ {% for row in echoed.first_rows %}
171
+ | {{ row.station }} | {{ row.day }} | {{ row.celsius }} |
172
+ {% endfor %}
173
+
174
+ ### Headers it saw
175
+
176
+ | header | value |
177
+ | --- | --- |
178
+ {% for header in echoed.headers %}
179
+ | {{ header.name }} | {{ header.value }} |
180
+ {% endfor %}
181
+ {% else %}
182
+ The summary step left no output, so this run never got as far as a receipt.
183
+ {% endif %}
@@ -0,0 +1,106 @@
1
+ # Call a service, put the answer in a file, and read the file back.
2
+ #
3
+ # In: a query the echo service reflects back, named by parameters.
4
+ # Out: the body as a file in the run's scratch space, and a small summary read back out of
5
+ # that file.
6
+ #
7
+ # An http.request step hands its answer on as `body`, and a body worth keeping travels to
8
+ # storage the way every other value does: through a storage.write step. Four hops, and what
9
+ # each one hands on:
10
+ #
11
+ # download http.request. Output: status, headers, body, body_bytes, duration_ms. body is
12
+ # the parsed document here, because the echo service answers JSON.
13
+ # save storage.write, the value to a URI. Output: uri, bytes_written, content_type.
14
+ # The bytes are staged beside the target and renamed on close, so the object
15
+ # becomes visible only once it is whole -- which is what makes a storage.exists
16
+ # sensor on the other side a gate rather than a race.
17
+ # back storage.read, the same URI. It is application/json, so the object comes back
18
+ # as `value` rather than as `text`.
19
+ # summary transform.jq, which is where a pipeline decides how much of a large body it
20
+ # actually needs: this one keeps four fields.
21
+ #
22
+ # HOW BIG A BODY THIS HOLDS. max_response bounds what the call reads into the worker, and a
23
+ # body past it is refused rather than read half way; max_size bounds what the read brings
24
+ # back. Both are whole values a step can look at. Bytes nobody has to look at never become a
25
+ # step output at all: storage.copy moves them between URIs, bounded by the storage rather
26
+ # than by the worker's memory.
27
+ #
28
+ # To change it: point save at s3://<bucket>/incoming/... and the same three steps become a
29
+ # download into an object store, because the scheme is the only thing that changes.
30
+ #
31
+ # dg run --local examples/recipes/http-save-body-to-storage.yaml
32
+ # dg run --local examples/recipes/http-save-body-to-storage.yaml -p day=2026-02-01
33
+
34
+ format: dirigent/v1
35
+ kind: pipeline
36
+ code: http-save-body-to-storage
37
+ name: A response body saved to storage
38
+ description: Call an endpoint, write the body it answered with to a storage URI, and read the file back for a summary.
39
+
40
+ tags: [recipes, http, storage, transform, starter]
41
+
42
+ requires:
43
+ blocks:
44
+ - http.request
45
+ - storage.write
46
+ - storage.read
47
+ - transform.jq
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 the downloaded file is named for.
57
+ station:
58
+ type: string
59
+ default: st-1
60
+ description: A query value, echoed back inside the downloaded body.
61
+
62
+ steps:
63
+ download:
64
+ block: http.request
65
+ config:
66
+ url: https://postman-echo.com/get
67
+ method: GET
68
+ query:
69
+ station: ${params.station}
70
+ day: ${params.day}
71
+ max_response: 8mb
72
+ timeout: 30s
73
+
74
+ save:
75
+ block: storage.write
76
+ depends_on: [download]
77
+ config:
78
+ # ${run.scratch} is the run's own prefix in whatever storage the instance is
79
+ # configured with: a temporary directory under --local, the artifact root on a
80
+ # server, a bucket where an s3 connection serves the scheme.
81
+ target: ${run.scratch}/incoming/${params.day}.json
82
+ value: ${steps.download.output.body}
83
+
84
+ back:
85
+ block: storage.read
86
+ depends_on: [save]
87
+ config:
88
+ # Where the write said the object landed, not the same path typed twice.
89
+ source: ${steps.save.output.uri}
90
+ max_size: 8mb
91
+
92
+ summary:
93
+ block: transform.jq
94
+ depends_on: [download, save, back]
95
+ config:
96
+ input:
97
+ status: ${steps.download.output.status}
98
+ uri: ${steps.save.output.uri}
99
+ bytes: ${steps.save.output.bytes_written}
100
+ body: ${steps.back.output.value}
101
+ program: |
102
+ {status, uri, file_bytes: .bytes,
103
+ summary: {url: .body.url,
104
+ station: .body.args.station,
105
+ day: .body.args.day,
106
+ host: .body.headers.host}}
@@ -0,0 +1,80 @@
1
+ # When a non-2xx is the right answer: success_status, and what it does not mean.
2
+ #
3
+ # In: a status code, as a parameter, defaulting to 418.
4
+ # Out: that status treated as success, with the run green and the body available to the
5
+ # steps below.
6
+ #
7
+ # By default any 2xx is success and everything else fails the step. success_status replaces
8
+ # that list for one step: writing [200, 418] says exactly those two are success, and 200
9
+ # alone no longer passes unless it is in the list. It is a replacement, not an addition,
10
+ # which is the field's one sharp edge.
11
+ #
12
+ # It is worth using where an API genuinely answers a non-2xx for an outcome that is not an
13
+ # error:
14
+ #
15
+ # 404 on a delete or a lookup, where "it is not there" is the state being asked about.
16
+ # 409 on a create, where "it already exists" is the state being asked about.
17
+ # 204 for an accepted write with nothing to return.
18
+ #
19
+ # It is not a way to silence a failure. A step that lists 500 as success has not made the
20
+ # server healthy; it has removed the run's only way of saying otherwise. The rule of thumb
21
+ # is that a listed status must be one the steps below can act on, and the branch below here
22
+ # does exactly that: it reads the status and reports what it means.
23
+ #
24
+ # https://postman-echo.com/status/<code> answers with the code it is given, which is what
25
+ # makes this runnable without arranging a real 409.
26
+ #
27
+ # To change it: -p status=404 with -p expected=404 is the "not there" case; -p status=500
28
+ # with the default expectations fails the run, which is the check that this recipe is not
29
+ # just swallowing everything.
30
+ #
31
+ # dg run --local examples/recipes/http-success-status-list.yaml
32
+ # dg run --local examples/recipes/http-success-status-list.yaml -p status=200
33
+ # dg run --local examples/recipes/http-success-status-list.yaml -p status=500 # fails, on purpose
34
+
35
+ format: dirigent/v1
36
+ kind: pipeline
37
+ code: http-success-status-list
38
+ name: Statuses that count as success
39
+ description: Accept a specific non-2xx as success for one call with success_status, and branch on what the status actually meant.
40
+
41
+ tags: [recipes, http, transform]
42
+
43
+ requires:
44
+ blocks:
45
+ - http.request
46
+ - transform.jq
47
+
48
+ params:
49
+ type: object
50
+ properties:
51
+ status:
52
+ type: integer
53
+ default: 418
54
+ description: The status the endpoint is asked to answer with.
55
+
56
+ steps:
57
+ call:
58
+ block: http.request
59
+ config:
60
+ url: https://postman-echo.com/status/${params.status}
61
+ method: GET
62
+ # Replaces the default, so 200 is listed explicitly: leaving it out would fail a call
63
+ # that succeeded ordinarily.
64
+ success_status: [200, 404, 418]
65
+ timeout: 20s
66
+
67
+ interpret:
68
+ block: transform.jq
69
+ depends_on: [call]
70
+ config:
71
+ input:
72
+ status: ${steps.call.output.status}
73
+ program: |
74
+ # A listed status has to be one the steps below can act on; that is what makes
75
+ # listing it different from ignoring a failure.
76
+ {status,
77
+ outcome: (if .status == 200 then "present"
78
+ elif .status == 404 then "absent, which is an answer"
79
+ else "answered, and unusual" end),
80
+ acted_on: (.status != 200)}