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,59 @@
1
+ # Queue examples
2
+
3
+ A queue sensor starts a run from a message instead of a clock or a webhook. Each poke reads
4
+ what has arrived since the last one and succeeds the moment there is enough, so the batch
5
+ becomes ordinary step output and the rest of the DAG never learns where it came from. A publish
6
+ goes the other way: a step puts records on a topic and succeeds once the broker has them.
7
+ [docs/queues.md](../../docs/queues.md) is the family's home.
8
+
9
+ Every block here is **ordinary**: each reaches only the brokers its connection names, so no id
10
+ has to be allowlisted to run one.
11
+
12
+ ## Bring the brokers up first
13
+
14
+ No document here can run without a broker. `infra/compose.queues.yaml` is a
15
+ single-node Redpanda (which speaks the Kafka protocol) and a RabbitMQ, both unprotected,
16
+ for a developer's machine:
17
+
18
+ ```bash
19
+ make queues-up # docker compose -f infra/compose.queues.yaml up -d --wait
20
+ ```
21
+
22
+ Then create the topic or the queue, put something on it, and create the connection each
23
+ document names. Each document's header spells out its own three commands, and the produce
24
+ document needs nothing put on the topic first: it publishes what it reads back.
25
+
26
+ Every document **names** its connection under `requires.connections` rather than carrying one,
27
+ which is the shape to copy: bootstrap servers, an AMQP URL and a sealed password belong to an
28
+ environment, not to a pipeline. On an instance they are created once:
29
+
30
+ ```bash
31
+ dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
32
+ dg connection create rabbitmq shop-queue \
33
+ --set url=amqp://dirigent@127.0.0.1:5672/ --set password=dirigent
34
+ ```
35
+
36
+ On the compose stack the brokers are the `infra/compose.brokers.yaml` overlay instead
37
+ (`make docker-run-queues`), and the same two connections name `redpanda:9092` and
38
+ `amqp://dirigent@rabbitmq:5672/`, which is what a container resolves.
39
+
40
+ ## Pipelines
41
+
42
+ | File | What it teaches |
43
+ | --- | --- |
44
+ | [kafka-consume-then-transform.yaml](kafka-consume-then-transform.yaml) | Waiting on a topic: `min_messages` and `max_messages`, why the cursor rather than a consumer group holds the offsets, and the batch read by a downstream transform. |
45
+ | [kafka-produce-then-consume.yaml](kafka-produce-then-consume.yaml) | Publishing: the two shapes an element of `records` may take, where a message key comes from, what `acks: all` and an idempotent producer do and do not promise, and why this one consumes from `earliest`. |
46
+ | [report-to-kafka.yaml](report-to-kafka.yaml) | Publishing a rendered page: `report.render` hands its text on, and `kafka.produce` puts it on the topic as one keyed record. |
47
+ | [rabbitmq-consume-ack-on-success.yaml](rabbitmq-consume-ack-on-success.yaml) | When a message is acknowledged: why `ack: on_success` is the default, what a poke that parks does with the messages it took, and what `always` gives up in exchange. |
48
+ | [report-to-rabbitmq.yaml](report-to-rabbitmq.yaml) | Publishing one message: the default exchange, where a routing key is a queue name, and what `persistent` buys. |
49
+
50
+ ## The cursor, in one paragraph
51
+
52
+ A sensor's poke keeps its place in a **cursor**, a JSON map the poke returns with a `NotYet`
53
+ and the next poke receives as `ctx.cursor`. `kafka.consume` keeps the offsets it has read to in
54
+ it; `rabbitmq.consume` keeps only a count and the last delivery tag, because the broker holds
55
+ the state there. It replaces rather than merges, it is stored by the transaction that parks the
56
+ attempt -- which makes advancing it at-least-once, so a poke may read the same ground twice --
57
+ and it lives only as long as the waiting attempt, because a poke that succeeds ends the step.
58
+ Anything a later step needs is in the output, which is why `kafka.consume` reports the offsets
59
+ it ended at there.
@@ -0,0 +1,105 @@
1
+ # A run that starts from what is on a Kafka topic, and reshapes the batch it took.
2
+ #
3
+ # NEEDS A BROKER, and a topic to read:
4
+ # docker compose -f infra/compose.queues.yaml up -d --wait
5
+ # docker exec -it dirigent-queues-redpanda-1 rpk topic create orders
6
+ # dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
7
+ # dg run examples/queues/kafka-consume-then-transform.yaml
8
+ # Then produce to the topic in another shell, whenever -- start: latest means the run takes
9
+ # what arrives after it began rather than what was already there, and the first poke writes
10
+ # that place down, so a message landing between two pokes is read by the next one:
11
+ # docker exec -it dirigent-queues-redpanda-1 \
12
+ # rpk topic produce orders --format '%v\n' # then type a JSON line and press ctrl-d
13
+ # On the compose stack the broker is the infra/compose.brokers.yaml overlay and the connection
14
+ # names it as `redpanda:9092`; docs/queues.md is the family's home.
15
+ #
16
+ # Two hops, and what each one hands on:
17
+ # wait kafka.consume, parked until at least one message is on the topic. Output:
18
+ # messages, count, and the offsets it ended at.
19
+ # summarise a jq transform over that batch, which is the only thing this example does with
20
+ # the messages -- the point is that they are ordinary step output from here on.
21
+ #
22
+ # THE CONNECTION IS NAMED, NOT CARRIED. requires.connections says the instance must already
23
+ # hold `orders-topic`, and refuses the document at apply if it does not. That is the shape to
24
+ # copy: bootstrap servers, a security setting and a sealed password belong to an environment,
25
+ # not to a pipeline, so pointing this at staging instead of production is an edit to one
26
+ # connection and to no document.
27
+ #
28
+ # WHY min_messages: 1. A poke succeeds as soon as there is one message, which is what "a run
29
+ # per batch, as soon as there is a batch" means. Raise it and the sensor waits for a fuller
30
+ # batch -- and a poke that finds fewer than that parks, keeping the offsets it read, so the
31
+ # next poke carries on rather than starting over. max_messages: 50 is the other half: it
32
+ # bounds what one run carries, so a topic that has been quiet for a week does not hand a
33
+ # single step ten thousand messages.
34
+ #
35
+ # NO group_id, DELIBERATELY. Without one the sensor tracks offsets itself, in the attempt's
36
+ # cursor, and commits nothing back to the cluster. Nothing on the broker has to be arranged
37
+ # for this to work, and a second pipeline over the same topic sees every message rather than
38
+ # racing this one for it. Set a group_id when the offsets should outlive the run -- then the
39
+ # commit happens in the poke that succeeded, and never in one that parked.
40
+ #
41
+ # READING THE SAME GROUND TWICE. Advancing the cursor is at-least-once: it is written by the
42
+ # transaction that parks the attempt, so a worker that dies before that commit leaves the
43
+ # older offsets behind and the next poke reads those messages again. A step that must not act
44
+ # twice on a message makes itself idempotent; this one only reshapes, so it does not care.
45
+ #
46
+ # TO MAKE IT YOURS: point the connection at your own cluster, change the topic, and replace
47
+ # the summarise step with whatever actually consumes the batch.
48
+
49
+ format: dirigent/v1
50
+ kind: pipeline
51
+ code: kafka-consume-then-transform
52
+ name: Consume a Kafka topic, then transform the batch
53
+ description: Wait for messages on a Kafka topic and reshape the batch the sensor handed on.
54
+
55
+ tags: [queues, kafka, sensor, transform, starter]
56
+
57
+ requires:
58
+ blocks:
59
+ - kafka.consume
60
+ - transform.jq
61
+ connections:
62
+ - orders-topic
63
+
64
+ steps:
65
+ wait:
66
+ block: kafka.consume
67
+ # A sensor's poll is a promise about how often the world is checked, and this one is
68
+ # checked every ten seconds. Between pokes the attempt is a row with a due time, not a
69
+ # worker: waiting on a quiet topic for the whole deadline costs nothing.
70
+ poll: 10s
71
+ deadline: 1h
72
+ # A wait that ran out is not a failure here: no messages arrived inside the hour, so the
73
+ # run is skipped and the next trigger tries again.
74
+ on_timeout: skip
75
+ config:
76
+ connection: orders-topic
77
+ topic: orders
78
+ min_messages: 1
79
+ max_messages: 50
80
+ # How long one poke waits on the broker before answering with what it has. It bounds the
81
+ # poke, never the wait.
82
+ poll_timeout: 5s
83
+ # latest begins at whatever arrives next, which is what a pipeline started by a message
84
+ # wants: earliest would replay the whole topic on the first poke of every run.
85
+ start: latest
86
+ # A value is JSON here because whatever produces to this topic writes JSON. text or
87
+ # base64 are the other two, and base64 is the default for a key because a key is often
88
+ # not text at all.
89
+ value_format: json
90
+
91
+ summarise:
92
+ block: transform.jq
93
+ depends_on: [wait]
94
+ config:
95
+ # The batch is ordinary step output from here on: a list of objects, each with the
96
+ # topic, partition, offset, key, value, timestamp and headers the broker recorded.
97
+ input: "${steps.wait.output.messages}"
98
+ program: >-
99
+ {
100
+ count: length,
101
+ partitions: (map(.partition) | unique),
102
+ first_offset: (map(.offset) | min),
103
+ last_offset: (map(.offset) | max),
104
+ values: map(.value)
105
+ }
@@ -0,0 +1,124 @@
1
+ # A run that puts records on a Kafka topic and then reads its own batch back off it.
2
+ #
3
+ # NEEDS A BROKER, and a topic to write to:
4
+ # docker compose -f infra/compose.queues.yaml up -d --wait
5
+ # docker exec -it dirigent-queues-redpanda-1 rpk topic create orders
6
+ # dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
7
+ # dg run examples/queues/kafka-produce-then-consume.yaml
8
+ # Nothing else has to produce for this one: it publishes what it later reads, so the whole
9
+ # round trip is in the document.
10
+ #
11
+ # ON THE COMPOSE STACK the broker is the infra/compose.brokers.yaml overlay
12
+ # (`make docker-run-queues`), where the connection names it `redpanda:9092` rather than
13
+ # `127.0.0.1:9092` -- a container resolves the service, not the host. Create the topic on that
14
+ # stack with `docker compose ... exec redpanda rpk topic create orders`.
15
+ # docs/queues.md is the family's home.
16
+ #
17
+ # Three hops, and what each one hands on:
18
+ # publish kafka.produce, three records onto the topic. Output: produced, the topic, the
19
+ # last offset per partition, duration_ms and sent_bytes.
20
+ # wait kafka.consume, reading the topic from the beginning, so it takes what the step
21
+ # before it just wrote.
22
+ # summarise a jq transform over that batch, so the messages end as ordinary step output.
23
+ #
24
+ # THE CONNECTION IS NAMED, NOT CARRIED. requires.connections says the instance must already
25
+ # hold `orders-topic`, and the document is refused at apply if it does not. Bootstrap servers,
26
+ # a security setting and a sealed password belong to an environment, not to a pipeline.
27
+ #
28
+ # TWO SHAPES OF RECORD. An element of `records` is read as an envelope when it is an object
29
+ # carrying `value` and nothing besides `key`, `value` and `headers`; every other element is
30
+ # itself the value. So a list of documents from an earlier step publishes unchanged, and a
31
+ # record that needs a key or a header says so in an envelope. An object that means to be a
32
+ # value and would read as an envelope is written `{"value": {...}}`.
33
+ #
34
+ # WHERE THE KEY COMES FROM. `key: region` takes each record's key from that field of its
35
+ # value, which is what puts every record of one region on one partition and keeps them in
36
+ # order relative to each other. A record whose envelope names its own `key` keeps that one, and
37
+ # a record with no such field fails the step rather than being published unkeyed.
38
+ #
39
+ # ACKS AND WHAT IDEMPOTENCE ACTUALLY BUYS. `acks: all` is the default and the only setting the
40
+ # idempotent producer runs under: the broker recognises a record the client itself retried and
41
+ # writes it once, so a connection that stumbles inside this step does not double-publish. A
42
+ # RETRY OF THE STEP is a different matter -- it opens a new producer session, and all three
43
+ # records go again. This example does not retry the publish, which is the simplest way to mean
44
+ # it; a pipeline that must not publish twice either does the same or lets a compacted topic or
45
+ # the reader settle the duplicates.
46
+ #
47
+ # WHY start: earliest HERE. The consume sensor's usual setting is `latest`, because a pipeline
48
+ # started by a message wants what arrives next. This one reads a batch its own earlier step
49
+ # wrote, which is already on the topic by the time the sensor pokes, so it starts at the oldest
50
+ # record instead. On a topic something else is also writing to, that would replay the lot.
51
+ #
52
+ # TO MAKE IT YOURS: point the connection at your own cluster, change the topic, and feed
53
+ # `records` from an earlier step instead of writing them inline -- an export in storage reaches
54
+ # the publish through a storage.read, which is the one door a value comes in by.
55
+
56
+ format: dirigent/v1
57
+ kind: pipeline
58
+ code: kafka-produce-then-consume
59
+ name: Publish to a Kafka topic, then consume the batch
60
+ description: Publish records to a Kafka topic and read the same batch back off it.
61
+
62
+ tags: [queues, kafka, sensor, transform]
63
+
64
+ requires:
65
+ blocks:
66
+ - kafka.produce
67
+ - kafka.consume
68
+ - transform.jq
69
+ connections:
70
+ - orders-topic
71
+
72
+ steps:
73
+ publish:
74
+ block: kafka.produce
75
+ config:
76
+ connection: orders-topic
77
+ topic: orders
78
+ # Each record's key is this field of its value, so one region's records share a
79
+ # partition and keep their order.
80
+ key: region
81
+ records:
82
+ # A bare element is the value.
83
+ - {order: 1, region: north, total: 240}
84
+ - {order: 2, region: south, total: 95}
85
+ # An envelope, because this record carries headers as well as a value.
86
+ - value: {order: 3, region: north, total: 610}
87
+ headers:
88
+ source: till-4
89
+ # The default, and the only setting the idempotent producer runs under.
90
+ acks: all
91
+ # The whole publish, from the first send to the last acknowledgement.
92
+ timeout: 30s
93
+
94
+ wait:
95
+ block: kafka.consume
96
+ depends_on: [publish]
97
+ poll: 5s
98
+ deadline: 5m
99
+ config:
100
+ connection: orders-topic
101
+ topic: orders
102
+ # The batch the step before this one wrote is already on the topic, so this poke reads
103
+ # the whole topic rather than waiting for what arrives next.
104
+ start: earliest
105
+ min_messages: 3
106
+ max_messages: 50
107
+ poll_timeout: 5s
108
+ # The keys were written as text, so they are read back as text; base64 is the default,
109
+ # because a key is often not text at all.
110
+ key_format: text
111
+ value_format: json
112
+
113
+ summarise:
114
+ block: transform.jq
115
+ depends_on: [wait]
116
+ config:
117
+ input: "${steps.wait.output.messages}"
118
+ program: >-
119
+ {
120
+ count: length,
121
+ keys: (map(.key) | unique),
122
+ total: (map(.value.total) | add),
123
+ headers: (map(.headers) | map(select(length > 0)))
124
+ }
@@ -0,0 +1,102 @@
1
+ # A run that starts from what is on a RabbitMQ queue, and why the default ack mode is the one
2
+ # that hands a short batch back.
3
+ #
4
+ # NEEDS A BROKER. Bring one up and put something on the queue first:
5
+ # docker compose -f infra/compose.queues.yaml up -d --wait
6
+ # open http://127.0.0.1:15672 # dirigent / dirigent; declare a queue named `shop-orders`
7
+ # # and publish a JSON body to it from the management UI
8
+ # dg connection create rabbitmq shop-queue \
9
+ # --set url=amqp://dirigent@127.0.0.1:5672/ --set password=dirigent
10
+ # dg run examples/queues/rabbitmq-consume-ack-on-success.yaml
11
+ # On the compose stack the broker is the infra/compose.brokers.yaml overlay and the connection
12
+ # names it as `amqp://dirigent@rabbitmq:5672/`; docs/queues.md is the family's home.
13
+ #
14
+ # Two hops, and what each one hands on:
15
+ # wait rabbitmq.consume, parked until at least one message is on the queue. Output:
16
+ # messages and count.
17
+ # summarise a jq transform over that batch, which is the only thing this example does with
18
+ # the messages.
19
+ #
20
+ # THE CONNECTION IS NAMED, NOT CARRIED. requires.connections says the instance must already
21
+ # hold `shop-queue`. The password is never written into the url: a plain field is neither
22
+ # encrypted at rest nor redacted in an API response, so the secret goes in the connection's
23
+ # sealed password field, which is both, and is merged into the url when a connection is opened
24
+ # and nowhere else. A url written amqp://user:secret@host is refused, the same way the sql
25
+ # kind refuses one.
26
+ #
27
+ # WHY ack: on_success IS THE DEFAULT. RabbitMQ, unlike Kafka, keeps its own place: a message is
28
+ # held unacknowledged until it is acked or nacked. So the question is not where to read but
29
+ # when to acknowledge, and the safe answer is "only in the poke that succeeded".
30
+ #
31
+ # Work through what a poke does with min_messages: 3 and two messages on the queue. It takes
32
+ # both, finds the batch too small, and nacks both back onto the queue with requeue -- so they
33
+ # are still there for the next poke, or for another consumer, and nothing was lost to a batch
34
+ # that was never acted on. Then a third message arrives, the next poke takes all three, hands
35
+ # them to the transform, and acknowledges them.
36
+ #
37
+ # ack: always is the other choice, and it is a real one with a real cost: every message is
38
+ # acknowledged the moment it is taken, park or no park, so a batch that parks is gone. It
39
+ # suits a queue nothing else reads and a step that would rather drop a partial batch than see
40
+ # a message twice.
41
+ #
42
+ # ON_SUCCESS IS STILL AT-LEAST-ONCE. Acknowledging happens inside the poke, and the attempt's
43
+ # outcome commits after it: a worker that dies in between leaves the messages acked and the
44
+ # step unfinished, and a worker that dies before the ack leaves them on the queue to be taken
45
+ # again. Neither ordering gives exactly-once -- that would need the broker and this instance's
46
+ # database in one transaction -- so a step that must not act twice makes itself idempotent.
47
+ #
48
+ # THE QUEUE MUST ALREADY EXIST. This block declares nothing: a queue a pipeline invented is a
49
+ # typo that reads as a working pipeline, so a name nobody created is rejected, with the name in
50
+ # the message, rather than waiting forever on a queue that will never have anything in it.
51
+ #
52
+ # TO MAKE IT YOURS: point the connection at your own broker, change the queue, and replace the
53
+ # summarise step with whatever actually consumes the batch.
54
+
55
+ format: dirigent/v1
56
+ kind: pipeline
57
+ code: rabbitmq-consume-ack-on-success
58
+ name: Consume a RabbitMQ queue, acknowledging only on success
59
+ description: Wait for messages on a RabbitMQ queue and reshape the batch, handing a short batch back to the queue.
60
+
61
+ tags: [queues, rabbitmq, sensor, transform, starter]
62
+
63
+ requires:
64
+ blocks:
65
+ - rabbitmq.consume
66
+ - transform.jq
67
+ connections:
68
+ - shop-queue
69
+
70
+ steps:
71
+ wait:
72
+ block: rabbitmq.consume
73
+ poll: 10s
74
+ deadline: 1h
75
+ # A wait that ran out is not a failure: nothing arrived inside the hour, so the run is
76
+ # skipped and the next trigger tries again.
77
+ on_timeout: skip
78
+ config:
79
+ connection: shop-queue
80
+ queue: shop-orders
81
+ min_messages: 1
82
+ max_messages: 50
83
+ # How long one poke waits on the broker before answering with what it has.
84
+ poll_timeout: 5s
85
+ # Written out although it is the default, because it is the decision this example is
86
+ # about. Change it to always and a batch below min_messages is acknowledged and dropped.
87
+ ack: on_success
88
+ value_format: json
89
+
90
+ summarise:
91
+ block: transform.jq
92
+ depends_on: [wait]
93
+ config:
94
+ # Each message carries the routing key, the exchange, the delivery tag, the decoded
95
+ # body, the headers, and the publisher's timestamp when it stamped one.
96
+ input: "${steps.wait.output.messages}"
97
+ program: >-
98
+ {
99
+ count: length,
100
+ routing_keys: (map(.routing_key) | unique),
101
+ bodies: map(.body)
102
+ }
@@ -0,0 +1,105 @@
1
+ # A rendered page published to a Kafka topic, for whatever reads the topic to deliver.
2
+ #
3
+ # NEEDS A BROKER, and a topic to write to:
4
+ # docker compose -f infra/compose.queues.yaml up -d --wait
5
+ # docker exec -it dirigent-queues-redpanda-1 rpk topic create orders
6
+ # dg connection create kafka orders-topic --set bootstrap_servers='["127.0.0.1:9092"]'
7
+ # dg run examples/queues/report-to-kafka.yaml
8
+ # On the compose stack the broker is the infra/compose.brokers.yaml overlay and the
9
+ # connection names it `redpanda:9092`; docs/queues.md is the family's home.
10
+ #
11
+ # Two hops, and what each one hands on:
12
+ # page report.render, the markdown. Output: text, content_type, text_bytes.
13
+ # publish kafka.produce, one record carrying the page. Output: produced, the topic, the
14
+ # last offset per partition, duration_ms and sent_bytes.
15
+ #
16
+ # THE RENDER IS A STEP, THE SENDING IS ANOTHER. report.render has no destination of its own:
17
+ # it renders text and hands it on, and the step below it decides where the text goes. The
18
+ # same template writes a file in examples/recipes/report-to-file.yaml, an object in
19
+ # examples/s3/report-to-s3.yaml, and a queue message in report-to-rabbitmq.yaml.
20
+ #
21
+ # WHY THE RECORD CARRIES A CODE. A consumer reading this topic gets one JSON record whose
22
+ # `text` is the whole page, so `code` is what tells it which report arrived without parsing
23
+ # the markdown. It is the record's key too, so every day's page for one report lands on one
24
+ # partition and the days stay in order relative to each other.
25
+ #
26
+ # A PUBLISH IS NOT IDEMPOTENT ACROSS A RETRY: a retried step opens a new producer session and
27
+ # publishes the record again. This document does not retry it, which is the simplest way to
28
+ # mean that.
29
+ #
30
+ # TO MAKE IT YOURS: change the template, change the topic, and give the record whatever
31
+ # envelope the consumer on the other side expects.
32
+
33
+ format: dirigent/v1
34
+ kind: pipeline
35
+ code: report-to-kafka
36
+ name: Publish a rendered report to a Kafka topic
37
+ description: Render a markdown page from a Jinja template and publish it as one record on a Kafka topic.
38
+
39
+ tags: [queues, report, kafka, transform]
40
+
41
+ requires:
42
+ blocks:
43
+ - transform.jq
44
+ - report.render
45
+ - kafka.produce
46
+ connections:
47
+ - orders-topic
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 report is about.
57
+
58
+ steps:
59
+ summary:
60
+ block: transform.jq
61
+ config:
62
+ input:
63
+ day: ${params.day}
64
+ orders:
65
+ - {order: 1, region: north, total: 240}
66
+ - {order: 2, region: south, total: 95}
67
+ - {order: 3, region: north, total: 610}
68
+ program: |
69
+ {day: .day,
70
+ orders: (.orders | length),
71
+ total: (.orders | map(.total) | add),
72
+ by_region: (.orders | group_by(.region)
73
+ | map({region: .[0].region, total: map(.total) | add}))}
74
+
75
+ page:
76
+ block: report.render
77
+ depends_on: [summary]
78
+ config:
79
+ values:
80
+ summary: ${steps.summary.output.value}
81
+ content_type: text/markdown
82
+ template: |
83
+ # Orders for {{ summary.day }}
84
+
85
+ {{ summary.orders }} orders, {{ summary.total }} in total.
86
+
87
+ {% for row in summary.by_region %}
88
+ - {{ row.region }}: {{ row.total }}
89
+ {% endfor %}
90
+
91
+ publish:
92
+ block: kafka.produce
93
+ depends_on: [page]
94
+ config:
95
+ connection: orders-topic
96
+ topic: orders
97
+ # The record's key, so one report's days share a partition and keep their order.
98
+ key: code
99
+ records:
100
+ - code: daily-orders
101
+ day: ${params.day}
102
+ text: ${steps.page.output.text}
103
+ content_type: ${steps.page.output.content_type}
104
+ acks: all
105
+ timeout: 30s
@@ -0,0 +1,105 @@
1
+ # A rendered page published to a RabbitMQ queue, for whatever consumes the queue to deliver.
2
+ #
3
+ # NEEDS A BROKER, and a queue to publish to:
4
+ # docker compose -f infra/compose.queues.yaml up -d --wait
5
+ # open http://127.0.0.1:15672 # dirigent / dirigent; declare a queue named `shop-reports`
6
+ # dg connection create rabbitmq shop-queue \
7
+ # --set url=amqp://dirigent@127.0.0.1:5672/ --set password=dirigent
8
+ # dg run examples/queues/report-to-rabbitmq.yaml
9
+ # On the compose stack the broker is the infra/compose.brokers.yaml overlay and the
10
+ # connection names it `amqp://dirigent@rabbitmq:5672/`; docs/queues.md is the family's home.
11
+ #
12
+ # Two hops, and what each one hands on:
13
+ # page report.render, the markdown. Output: text, content_type, text_bytes.
14
+ # publish rabbitmq.publish, one message carrying that text. Output: published, which is
15
+ # one, and message_bytes.
16
+ #
17
+ # THE DEFAULT EXCHANGE IS THE ONE WITH NO NAME, and on it a routing key is a queue name. So
18
+ # `exchange: ""` with `routing_key: shop-reports` puts the message on that queue and nowhere
19
+ # else. The queue has to exist: the publish is mandatory, so a routing key nothing takes is
20
+ # refused by the broker and fails the step, rather than dropped without a word. Naming an
21
+ # exchange instead routes by whatever bindings the broker holds, and an exchange the broker
22
+ # does not have is rejected by name the same way.
23
+ #
24
+ # WHAT THE MESSAGE CARRIES. A string message is sent as UTF-8 text and anything else as
25
+ # canonical JSON, and the content type follows -- `text/plain` or `application/json` unless
26
+ # the step names one. This one sends the page itself and says it is markdown, so a consumer
27
+ # reads the body rather than unwrapping an envelope first.
28
+ #
29
+ # PERSISTENT: TRUE is the default, and it is what makes a durable queue keep the message
30
+ # across a broker restart. A publish is not idempotent either way: a retry of this step puts
31
+ # a second copy on the queue, so a pipeline that must not do that either does not retry it or
32
+ # gives the consumer something to settle duplicates by.
33
+ #
34
+ # TO MAKE IT YOURS: change the template, change the queue, and point the connection at your
35
+ # own broker.
36
+
37
+ format: dirigent/v1
38
+ kind: pipeline
39
+ code: report-to-rabbitmq
40
+ name: Publish a rendered report to a RabbitMQ queue
41
+ description: Render a markdown page from a Jinja template and publish it as one message on a RabbitMQ queue.
42
+
43
+ tags: [queues, report, rabbitmq, transform]
44
+
45
+ requires:
46
+ blocks:
47
+ - transform.jq
48
+ - report.render
49
+ - rabbitmq.publish
50
+ connections:
51
+ - shop-queue
52
+
53
+ params:
54
+ type: object
55
+ properties:
56
+ day:
57
+ type: string
58
+ format: date
59
+ default: "2026-01-01"
60
+ description: The day the report is about.
61
+
62
+ steps:
63
+ summary:
64
+ block: transform.jq
65
+ config:
66
+ input:
67
+ day: ${params.day}
68
+ orders:
69
+ - {order: 1, status: shipped}
70
+ - {order: 2, status: held}
71
+ - {order: 3, status: shipped}
72
+ program: |
73
+ {day: .day,
74
+ orders: (.orders | length),
75
+ held: [.orders[] | select(.status == "held") | .order]}
76
+
77
+ page:
78
+ block: report.render
79
+ depends_on: [summary]
80
+ config:
81
+ values:
82
+ summary: ${steps.summary.output.value}
83
+ content_type: text/markdown
84
+ template: |
85
+ # Shop report for {{ summary.day }}
86
+
87
+ {{ summary.orders }} orders, {{ summary.held | length }} held.
88
+
89
+ {% for order in summary.held %}
90
+ - order {{ order }} is held
91
+ {% endfor %}
92
+
93
+ publish:
94
+ block: rabbitmq.publish
95
+ depends_on: [page]
96
+ config:
97
+ connection: shop-queue
98
+ # The default exchange, where the routing key is the queue's own name.
99
+ exchange: ""
100
+ routing_key: shop-reports
101
+ message: ${steps.page.output.text}
102
+ content_type: ${steps.page.output.content_type}
103
+ # The default, written out because it is what a durable queue needs to survive a restart.
104
+ persistent: true
105
+ timeout: 30s