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,144 @@
1
+ # A signed outbound delivery, and where the secret on each side of it lives.
2
+ #
3
+ # webhook.post is the sending half of the webhook story; webhook-mapping-nested-payload.yaml
4
+ # is the receiving half. sign_with names the connection whose hmac_secret signs the body, and
5
+ # the block presents the result as X-Dirigent-Signature -- computed exactly the way dirigent's
6
+ # own /hooks/{token} verifies one, so a dirigent instance can be the receiver on the other end
7
+ # with nothing in between.
8
+ #
9
+ # THREE THINGS ARE WORTH BEING PRECISE ABOUT:
10
+ #
11
+ # * The signature covers THE BYTES THAT GO ON THE WIRE. The block serialises the body
12
+ # itself and signs those bytes, because letting an HTTP client re-encode a body after
13
+ # signing produces a signature that verifies nowhere.
14
+ # * sign_with is separate from connection ON PURPOSE. A POST to an absolute URL can still be
15
+ # signed with a secret this instance holds, and signing is something the document says out
16
+ # loud rather than something that happens invisibly because a connection had a field set.
17
+ # * A secret is never in a document. It is on a connection, sealed at rest and redacted in
18
+ # every API response, and a document names the connection. What this file carries is a
19
+ # connection for a --local run to create, which is the one case where carrying one is
20
+ # right -- a server refuses a document that embeds a credential, because applying it would
21
+ # store the secret in every version of the pipeline.
22
+ #
23
+ # THE INBOUND DIRECTION IS AN INSTANCE SETTING, NOT A DOCUMENT ONE. A webhook a document
24
+ # declares carries only its code and its payload mapping; whether deliveries to it must be
25
+ # signed, and with what secret, is set where the token is minted:
26
+ #
27
+ # dg webhook create <pipeline> <code> --hmac-secret "$SECRET" --map day='$.date'
28
+ #
29
+ # An unsigned delivery to a webhook that requires a signature is answered 401 with "this
30
+ # webhook requires a X-Dirigent-Signature header", and a wrong one is answered 401 too, after
31
+ # a constant-time comparison.
32
+ #
33
+ # Hop by hop:
34
+ #
35
+ # build assembles the notification body. Signing a value a step computed is the ordinary
36
+ # case; the signature is over whatever the body serialises to.
37
+ # notify POSTs it signed. Postman Echo takes any body and answers with what it received,
38
+ # so its answer shows the header that was presented.
39
+ # unsigned the same POST with no sign_with, for comparison: output.signed is false.
40
+ # compare reads both back and puts the two signed flags side by side.
41
+ #
42
+ # EXPECT THIS RUN TO SUCCEED in about two seconds, with notify's signed true and unsigned's
43
+ # signed false.
44
+ #
45
+ # dg run --local examples/patterns/webhook-signed.yaml
46
+
47
+ format: dirigent/v1
48
+ kind: pipeline
49
+ code: webhook-signed
50
+ name: A signed webhook delivery
51
+ description: |
52
+ `webhook.post` with `sign_with` presents `X-Dirigent-Signature` over the exact bytes it
53
+ sends, computed the way dirigent's own inbound `/hooks/{token}` verifies one.
54
+
55
+ The secret lives on a connection, never in the document. Requiring a signature on an
56
+ **inbound** webhook is an instance setting made when the token is minted, not something
57
+ `dirigent/v1` can say.
58
+
59
+ tags: [patterns, transform, webhook, credential]
60
+
61
+ requires:
62
+ blocks:
63
+ - webhook.post
64
+ - transform.jq
65
+ - value.const
66
+
67
+ # Carried, because a --local run has no instance to hold a connection. Against a real
68
+ # instance this is a connection created once, and the document names it by code:
69
+ # dg connection create http ops-receiver --set base_url=https://postman-echo.com \
70
+ # --set hmac_secret=...
71
+ connections:
72
+ ops-receiver:
73
+ kind: http
74
+ config:
75
+ base_url: https://postman-echo.com
76
+ # A SecretStr: sealed on the way in, redacted on the way out, and never readable back
77
+ # from the API. This one is a demonstration value and protects nothing.
78
+ hmac_secret: a shared secret between two dirigent instances
79
+ timeout: 30s
80
+
81
+ params:
82
+ type: object
83
+ properties:
84
+ dataset:
85
+ type: string
86
+ description: What the notification says finished.
87
+ default: cases
88
+ rows:
89
+ type: integer
90
+ description: How many rows it says were loaded.
91
+ default: 4120
92
+ minimum: 0
93
+
94
+ steps:
95
+ build:
96
+ block: value.const
97
+ config:
98
+ value:
99
+ event: dataset.loaded
100
+ dataset: "${params.dataset}"
101
+ rows: "${params.rows}"
102
+
103
+ notify:
104
+ block: webhook.post
105
+ depends_on: [build]
106
+ config:
107
+ # Where it goes: the connection's base URL plus this path.
108
+ connection: ops-receiver
109
+ path: /post
110
+ # What signs it. The same connection here, but it need not be: a POST to one place can
111
+ # be signed with a secret agreed with somebody else.
112
+ sign_with: ops-receiver
113
+ body: "${steps.build.output.value}"
114
+ headers:
115
+ # Routing metadata the receiver wants. Headers are outside the signature, which
116
+ # covers the body's bytes only, so nothing security-bearing belongs up here.
117
+ x-event-kind: dataset.loaded
118
+
119
+ unsigned:
120
+ block: webhook.post
121
+ depends_on: [build]
122
+ # No sign_with, which is the default. The pair exists so the signed flag has something to
123
+ # be compared against.
124
+ config:
125
+ connection: ops-receiver
126
+ path: /post
127
+ body: "${steps.build.output.value}"
128
+
129
+ compare:
130
+ block: transform.jq
131
+ depends_on: [notify, unsigned]
132
+ config:
133
+ input:
134
+ signed: "${steps.notify.output.signed}"
135
+ unsigned: "${steps.unsigned.output.signed}"
136
+ # The receiver echoes the headers it was sent, so the signature the block presented is
137
+ # readable here. A real receiver verifies it and says nothing about it.
138
+ presented: "${steps.notify.output.json_body.headers}"
139
+ program: |
140
+ {
141
+ signed,
142
+ unsigned,
143
+ signature_header: (.presented | keys | map(select(ascii_downcase == "x-dirigent-signature")) | first)
144
+ }
@@ -0,0 +1,92 @@
1
+ # NEEDS THE dirigent-storage-s3 PACKAGE, which registers the s3:// scheme, and an
2
+ # S3-compatible endpoint set up as examples/s3/s3-round-trip.yaml documents.
3
+ #
4
+ # This is the blueprint's worked example, and the thing it demonstrates is that a storage
5
+ # backend is not a step. Nothing here says "connect to S3": s3:// is a registered scheme,
6
+ # and every block that takes a URI can address it, including the sensor that waits for the
7
+ # object to appear. Swapping the drop from s3:// to gs:// is a one-word edit in one line.
8
+ #
9
+ # The parquet never becomes a step output, and it could not: a value comes into a run only
10
+ # through storage.read, which reads text and json and refuses a format it cannot decode.
11
+ # What travels between the steps is the address of the object, which is what makes the
12
+ # fan-out below cheap: each region is handed the same URI, and nothing is copied to do it.
13
+ # The ingestion endpoint reads the object from the bucket it is already looking at.
14
+ #
15
+ # Hop by hop:
16
+ #
17
+ # wait_for_drop the sensor. Its output says which object landed and how big it is.
18
+ # push one call per region, each carrying the address and the size rather than
19
+ # the bytes.
20
+ # notify_failure one_failed, so a region that could not be pushed is heard about.
21
+
22
+ format: dirigent/v1
23
+ kind: pipeline
24
+ code: s3-parquet-to-ingestion
25
+ name: Parquet from S3 to an ingestion endpoint
26
+ description: Wait for the daily parquet drop, hand each region its address, alert on failure.
27
+
28
+ tags: [preview, http, sensor, storage]
29
+
30
+ concurrency: skip
31
+
32
+ requires:
33
+ blocks:
34
+ - storage.exists
35
+ - http.request
36
+ connections:
37
+ - modelling-api
38
+ - ops-webhook
39
+ storage:
40
+ - s3
41
+
42
+ params:
43
+ type: object
44
+ required: [day]
45
+ properties:
46
+ day:
47
+ type: string
48
+ format: date
49
+ regions:
50
+ type: array
51
+ default: [east, west]
52
+ items:
53
+ type: string
54
+
55
+ steps:
56
+ wait_for_drop:
57
+ block: storage.exists
58
+ poll: 5m
59
+ deadline: 6h
60
+ on_timeout: skip
61
+ config:
62
+ uri: "s3://drops/climate/${params.day}.parquet"
63
+
64
+ push:
65
+ block: http.request
66
+ depends_on: [wait_for_drop]
67
+ for_each: "${params.regions}"
68
+ items: continue
69
+ retry:
70
+ max_attempts: 5
71
+ backoff: 30s
72
+ config:
73
+ connection: modelling-api
74
+ path: "/v1/ingest/${item}"
75
+ method: POST
76
+ # The object the sensor found, named rather than carried. The size is what the
77
+ # endpoint checks the drop against before it starts reading.
78
+ body:
79
+ day: "${params.day}"
80
+ region: "${item}"
81
+ source: "${steps.wait_for_drop.output.uri}"
82
+ source_bytes: "${steps.wait_for_drop.output.size}"
83
+
84
+ notify_failure:
85
+ block: http.request
86
+ depends_on: [push]
87
+ rule: one_failed
88
+ config:
89
+ connection: ops-webhook
90
+ method: POST
91
+ body:
92
+ text: "s3-parquet-to-ingestion failed for ${params.day}"
@@ -0,0 +1,31 @@
1
+ # Driving an instance from Python
2
+
3
+ The YAML files above are pipelines. These are programs that drive an instance: each one is a
4
+ single file using [`dirigent-client`](../../packages/dirigent-client), the typed async SDK.
5
+
6
+ Every script reads `DG_URL` and `DG_TOKEN` from the environment, exactly as `dg` does:
7
+
8
+ ```bash
9
+ export DG_URL=http://127.0.0.1:3333 DG_TOKEN=... # dg dev prints both
10
+ uv run python examples/python/apply_and_run.py
11
+ ```
12
+
13
+ | Script | What it shows |
14
+ | --- | --- |
15
+ | [`apply_and_run.py`](apply_and_run.py) | Plan, apply, run, wait for the terminal state, print the report. |
16
+ | [`follow_logs.py`](follow_logs.py) | Stream a run's log entries as the workers write them. |
17
+ | [`list_and_filter.py`](list_and_filter.py) | Query pipelines and runs by pipeline, status, and window. |
18
+ | [`connections.py`](connections.py) | Create a credential record, check it, read it back redacted, delete it. |
19
+ | [`error_handling.py`](error_handling.py) | Provoke each typed refusal, and handle each on its own terms. |
20
+ | [`ci_gate.py`](ci_gate.py) | Apply and run in a build job, exiting non-zero when no run started or the run does not succeed. |
21
+
22
+ `apply_and_run.py`, `follow_logs.py`, and `ci_gate.py` apply
23
+ [`hello-world.yaml`](../hello-world.yaml), which uses `shell.run`. That block executes code
24
+ on the worker, so the instance must allowlist it:
25
+
26
+ ```bash
27
+ export DIRIGENT_ENABLED_UNSAFE_BLOCKS='["shell.run"]'
28
+ ```
29
+
30
+ The REST API remains available directly; see [docs/python.md](../../docs/python.md) for the
31
+ SDK reference and for what the raw endpoints answer with.
@@ -0,0 +1,52 @@
1
+ """Apply a document, start a run, wait for it, and report what happened.
2
+
3
+ The canonical first script: everything a CI job or a scheduler-of-schedulers does.
4
+
5
+ export DG_URL=http://127.0.0.1:3333 DG_TOKEN=...
6
+ uv run python examples/python/apply_and_run.py
7
+
8
+ hello-world.yaml uses shell.run, which executes code on the worker, so the instance must
9
+ allowlist it: DIRIGENT_ENABLED_UNSAFE_BLOCKS='["shell.run"]'.
10
+ """
11
+
12
+ import asyncio
13
+ import os
14
+ from datetime import timedelta
15
+ from pathlib import Path
16
+
17
+ from dirigent_client import Dirigent, PlanAction, RunStatus
18
+
19
+ DOCUMENT = Path(__file__).resolve().parents[1] / "hello-world.yaml"
20
+
21
+
22
+ async def main() -> int:
23
+ """Apply the document, run it, and wait for the run to settle."""
24
+ async with Dirigent(url=os.environ["DG_URL"], token=os.environ["DG_TOKEN"]) as dg:
25
+ plan = await dg.pipelines.apply(DOCUMENT, dry_run=True)
26
+ print(f"plan: {plan.plan.action.value} {plan.plan.code}")
27
+ if plan.plan.action is PlanAction.INVALID:
28
+ for issue in plan.plan.issues:
29
+ print(f" - {issue}")
30
+ return 1
31
+
32
+ applied = await dg.pipelines.apply(DOCUMENT)
33
+ print(f"applied: {applied.plan.code} version {applied.version or applied.plan.current_version}")
34
+
35
+ accepted = await dg.pipelines.run(applied.plan.code)
36
+ if accepted.run_id is None:
37
+ # Not a failure: the pipeline's concurrency policy declined a second run.
38
+ print(f"not started: {accepted.detail}")
39
+ return 0
40
+
41
+ print(f"started: run {accepted.run_id}")
42
+ run = await dg.runs.wait(accepted.run_id, timeout=timedelta(minutes=5))
43
+ print(f"finished: {run.status.value} in {run.pipeline}")
44
+
45
+ report = await dg.runs.report(run.id)
46
+ for step in report.steps:
47
+ print(f" {step.outcome:<10} {step.step} ({step.block})")
48
+ return 0 if run.status is RunStatus.SUCCEEDED else 1
49
+
50
+
51
+ if __name__ == "__main__":
52
+ raise SystemExit(asyncio.run(main()))
@@ -0,0 +1,76 @@
1
+ """Apply a document and run it, exiting non-zero when no run started or the run does not succeed.
2
+
3
+ The shape of a deploy job: one script, one exit code, no interpretation needed by whoever
4
+ reads the build log.
5
+
6
+ export DG_URL=https://dirigent.example.org DG_TOKEN=...
7
+ uv run python examples/python/ci_gate.py pipelines/daily-load.yaml
8
+
9
+ In a GitHub workflow:
10
+
11
+ - run: uv run python examples/python/ci_gate.py pipelines/daily-load.yaml
12
+ env:
13
+ DG_URL: ${{ vars.DG_URL }}
14
+ DG_TOKEN: ${{ secrets.DG_TOKEN }}
15
+ """
16
+
17
+ import asyncio
18
+ import os
19
+ import sys
20
+ from datetime import timedelta
21
+ from pathlib import Path
22
+
23
+ from dirigent_client import Dirigent, DirigentError, PlanAction, ProvenanceSource, RunStatus, WaitTimeout
24
+
25
+ #: How long the job is willing to wait for the run before it gives up on it.
26
+ BUDGET = timedelta(hours=2)
27
+
28
+ #: Statuses this gate accepts. A run that tolerated an item failure is not a green build.
29
+ ACCEPTED = (RunStatus.SUCCEEDED,)
30
+
31
+
32
+ async def gate(document: Path, reference: str) -> int:
33
+ """Apply, run, wait, and report; the return value is the job's exit code."""
34
+ async with Dirigent(url=os.environ["DG_URL"], token=os.environ["DG_TOKEN"]) as dg:
35
+ applied = await dg.pipelines.apply(document, source=ProvenanceSource.FILE, source_ref=reference)
36
+ plan = applied.plan
37
+ if plan.action is PlanAction.INVALID:
38
+ print(f"::error::{document} does not validate against this instance")
39
+ for issue in plan.issues:
40
+ print(f" {issue}")
41
+ return 1
42
+ print(f"{plan.action.value} {plan.code} -> version {applied.version or plan.current_version}")
43
+
44
+ accepted = await dg.pipelines.run(plan.code)
45
+ if accepted.run_id is None:
46
+ print(f"::error::not started: {accepted.detail}")
47
+ return 1
48
+
49
+ try:
50
+ run = await dg.runs.wait(accepted.run_id, timeout=BUDGET)
51
+ except WaitTimeout as timed_out:
52
+ print(f"::error::{timed_out.message}")
53
+ return 1
54
+
55
+ report = await dg.runs.report(run.id)
56
+ for step in report.steps:
57
+ print(f" {step.outcome:<10} {step.step:<24} {step.error or ''}")
58
+ if run.status in ACCEPTED:
59
+ print(f"{run.status.value} in {(report.duration_ms or 0) / 1000:.1f}s")
60
+ return 0
61
+ print(f"::error::run {run.id} {run.status.value}: {run.error or 'see the step table above'}")
62
+ return 1
63
+
64
+
65
+ def main() -> int:
66
+ """Read the document from the command line and run the gate over it."""
67
+ reference = sys.argv[1] if len(sys.argv) > 1 else str(Path(__file__).resolve().parents[1] / "hello-world.yaml")
68
+ try:
69
+ return asyncio.run(gate(Path(reference), reference))
70
+ except DirigentError as refusal:
71
+ print(f"::error::{refusal.message}")
72
+ return 1
73
+
74
+
75
+ if __name__ == "__main__":
76
+ raise SystemExit(main())
@@ -0,0 +1,61 @@
1
+ """Create a connection, ask whether it answers, and remove it again.
2
+
3
+ export DG_URL=http://127.0.0.1:3333 DG_TOKEN=...
4
+ uv run python examples/python/connections.py
5
+
6
+ A connection's kind declares which of its fields are secret by marking them SecretStr. That
7
+ one declaration is what makes the API redact them and the engine encrypt them, so a read
8
+ never returns a credential: this script writes one and gets the redaction marker back.
9
+ """
10
+
11
+ import asyncio
12
+ import os
13
+
14
+ from dirigent_client import Conflict, Dirigent
15
+
16
+ NAME = "example-http"
17
+
18
+
19
+ async def main() -> int:
20
+ """Create a connection, check it, read it back redacted, and delete it."""
21
+ async with Dirigent(url=os.environ["DG_URL"], token=os.environ["DG_TOKEN"]) as dg:
22
+ catalog = await dg.blocks.catalog()
23
+ print(f"connection kinds: {', '.join(entry.id for entry in catalog.connection_kinds) or 'none'}")
24
+
25
+ created_here = False
26
+ try:
27
+ created = await dg.connections.create(
28
+ NAME,
29
+ kind="http",
30
+ description="An example credential; nothing behind it is real.",
31
+ config={
32
+ # A reserved name that never resolves, so the check below fails without
33
+ # this example ever reaching a real service.
34
+ "base_url": "https://service.invalid",
35
+ "bearer_token": "not-a-real-token",
36
+ },
37
+ )
38
+ created_here = True
39
+ except Conflict:
40
+ print(f"{NAME} already exists; reading it instead")
41
+ created = await dg.connections.get(NAME)
42
+
43
+ print(f"created: {created.code} ({created.kind})")
44
+ print(f" secret fields: {', '.join(created.secret_fields) or 'none'}")
45
+ for field, value in created.config.items():
46
+ print(f" {field}: {value}")
47
+
48
+ report = await dg.connections.check(NAME)
49
+ print(f"check: {'healthy' if report.healthy else 'unhealthy'} -- {report.detail or ''}")
50
+
51
+ # Only remove what this run made; a connection that was already here is left in place.
52
+ if created_here:
53
+ await dg.connections.delete(NAME)
54
+ print(f"deleted: {NAME}")
55
+ else:
56
+ print(f"leaving pre-existing {NAME} in place")
57
+ return 0
58
+
59
+
60
+ if __name__ == "__main__":
61
+ raise SystemExit(asyncio.run(main()))
@@ -0,0 +1,84 @@
1
+ """Provoke each typed refusal, and show the shape of handling it.
2
+
3
+ export DG_URL=http://127.0.0.1:3333 DG_TOKEN=...
4
+ uv run python examples/python/error_handling.py
5
+
6
+ Every exception carries the status, the URL, and the parsed problem body, so a caller can
7
+ print one sentence, branch on a status, or read the field list a validation failure names.
8
+ """
9
+
10
+ import asyncio
11
+ import os
12
+
13
+ from dirigent_client import (
14
+ Dirigent,
15
+ DirigentError,
16
+ NotDirigent,
17
+ NotFound,
18
+ Unauthorized,
19
+ ValidationFailed,
20
+ )
21
+
22
+ DOCUMENT = """
23
+ format: dirigent/v1
24
+ kind: pipeline
25
+ code: error-handling-demo
26
+ params:
27
+ type: object
28
+ required: [day]
29
+ properties:
30
+ day:
31
+ type: string
32
+ steps:
33
+ gate:
34
+ block: time.window
35
+ config:
36
+ after: "00:00"
37
+ before: "23:59"
38
+ """
39
+
40
+
41
+ async def main() -> int:
42
+ """Ask for four things that cannot work, and handle each refusal on its own terms."""
43
+ url = os.environ["DG_URL"]
44
+ token = os.environ["DG_TOKEN"]
45
+
46
+ async with Dirigent(url=url, token=token) as dg:
47
+ try:
48
+ await dg.pipelines.get("no-pipeline-has-this-code")
49
+ except NotFound as refusal:
50
+ print(f"not found: {refusal.message} [{refusal.status} from {refusal.url}]")
51
+
52
+ await dg.pipelines.apply(DOCUMENT)
53
+ try:
54
+ await dg.pipelines.run("error-handling-demo", params={"day": 7})
55
+ except ValidationFailed as refusal:
56
+ print(f"refused: {refusal.message}")
57
+ for problem in refusal.problems:
58
+ print(f" - {problem}")
59
+ finally:
60
+ await dg.pipelines.delete("error-handling-demo")
61
+
62
+ async with Dirigent(url=url, token="not-a-real-token") as dg:
63
+ try:
64
+ await dg.auth.whoami()
65
+ except Unauthorized as refusal:
66
+ print(f"unauthorized: {refusal.message}")
67
+
68
+ # A URL pointing at the wrong thing has two shapes. Nothing listening is a
69
+ # TransportError; something answering without X-Dirigent-Version -- a docs server, a
70
+ # proxy, another service -- is NotDirigent, and both mean the URL rather than the
71
+ # request is wrong. Every refusal is a DirigentError, so one clause is enough when the
72
+ # distinction does not matter.
73
+ async with Dirigent(url="http://127.0.0.1:1", token=token, retries=0) as dg:
74
+ try:
75
+ await dg.system.info()
76
+ except NotDirigent as refusal:
77
+ print(f"wrong address: {refusal.message}")
78
+ except DirigentError as refusal:
79
+ print(f"unreachable: {refusal.message}")
80
+ return 0
81
+
82
+
83
+ if __name__ == "__main__":
84
+ raise SystemExit(asyncio.run(main()))
@@ -0,0 +1,39 @@
1
+ """Start a run and print its log entries as the workers write them.
2
+
3
+ export DG_URL=http://127.0.0.1:3333 DG_TOKEN=...
4
+ uv run python examples/python/follow_logs.py
5
+
6
+ The tail is server-sent events; the client reopens a dropped connection from the last entry
7
+ it yielded, so a proxy timing the stream out does not lose or repeat a line.
8
+ """
9
+
10
+ import asyncio
11
+ import os
12
+ from pathlib import Path
13
+
14
+ from dirigent_client import Dirigent
15
+
16
+ DOCUMENT = Path(__file__).resolve().parents[1] / "hello-world.yaml"
17
+
18
+
19
+ async def main() -> int:
20
+ """Apply, run, and stream the run's log entries until it settles."""
21
+ async with Dirigent(url=os.environ["DG_URL"], token=os.environ["DG_TOKEN"]) as dg:
22
+ applied = await dg.pipelines.apply(DOCUMENT)
23
+ accepted = await dg.pipelines.run(applied.plan.code)
24
+ if accepted.run_id is None:
25
+ print(f"not started: {accepted.detail}")
26
+ return 0
27
+
28
+ print(f"following run {accepted.run_id}")
29
+ async for entry in dg.runs.follow_logs(accepted.run_id):
30
+ where = entry.step_name or "-"
31
+ print(f" {entry.level.value:<7} {where:<12} {entry.message}")
32
+
33
+ run = (await dg.runs.get(accepted.run_id)).run
34
+ print(f"{run.status.value}")
35
+ return 0
36
+
37
+
38
+ if __name__ == "__main__":
39
+ raise SystemExit(asyncio.run(main()))
@@ -0,0 +1,52 @@
1
+ """Query runs by pipeline, status, and how far back to look, and print them as a table.
2
+
3
+ export DG_URL=http://127.0.0.1:3333 DG_TOKEN=...
4
+ uv run python examples/python/list_and_filter.py
5
+
6
+ Every listing answers a page: `items`, and a `next` cursor to pass back as `after` when
7
+ there is more. A window (`since`) and a limit are still how a report bounds itself; the
8
+ cursor is how it reads past the first page without asking for a bigger one.
9
+ """
10
+
11
+ import asyncio
12
+ import os
13
+
14
+ from dirigent_client import Dirigent, RunStatus
15
+
16
+ #: The window a report covers, in the humane duration grammar the whole product uses.
17
+ WINDOW = "24h"
18
+
19
+
20
+ async def main() -> int:
21
+ """Print the pipelines this instance holds, and its recent runs."""
22
+ async with Dirigent(url=os.environ["DG_URL"], token=os.environ["DG_TOKEN"]) as dg:
23
+ pipelines = await dg.pipelines.list()
24
+ print(f"{'pipeline':<28} {'version':<8} {'active':<7} {'in flight'}")
25
+ for pipeline in pipelines.items:
26
+ version = str(pipeline.current_version or "-")
27
+ print(f"{pipeline.code:<28} {version:<8} {str(pipeline.active):<7} {pipeline.active_runs}")
28
+
29
+ print(f"\nruns in the last {WINDOW}")
30
+ print(f"{'run':<38} {'pipeline':<20} {'status':<22} {'started'}")
31
+ for run in (await dg.runs.list(since=WINDOW, limit=20)).items:
32
+ started = run.started_at.isoformat() if run.started_at else "-"
33
+ print(f"{run.id!s:<38} {run.pipeline:<20} {run.status.value:<22} {started}")
34
+
35
+ # Walking the cursor is how a report covers a window larger than one page.
36
+ failed = []
37
+ after = None
38
+ while True:
39
+ page = await dg.runs.list(status=RunStatus.FAILED, since=WINDOW, after=after)
40
+ failed.extend(page.items)
41
+ if page.next is None:
42
+ break
43
+ after = page.next
44
+
45
+ print(f"\n{len(failed)} failed in the last {WINDOW}")
46
+ for run in failed:
47
+ print(f" {run.id} {run.pipeline} {run.error or 'no error recorded'}")
48
+ return 0
49
+
50
+
51
+ if __name__ == "__main__":
52
+ raise SystemExit(asyncio.run(main()))