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.
- dirigent_examples/__init__.py +22 -0
- dirigent_examples/py.typed +0 -0
- dirigent_examples/shelves/README.md +299 -0
- dirigent_examples/shelves/composition/README.md +18 -0
- dirigent_examples/shelves/composition/chained-instances.yaml +97 -0
- dirigent_examples/shelves/composition/composition-child.yaml +64 -0
- dirigent_examples/shelves/composition/composition-parent.yaml +89 -0
- dirigent_examples/shelves/connections.yaml +52 -0
- dirigent_examples/shelves/demo/README.md +19 -0
- dirigent_examples/shelves/demo/markdown-showcase.yaml +117 -0
- dirigent_examples/shelves/demo/params-showcase.yaml +92 -0
- dirigent_examples/shelves/demo/requires.yaml +65 -0
- dirigent_examples/shelves/demo/weekly-import-malawi.yaml +54 -0
- dirigent_examples/shelves/demo/weekly-import-nepal.yaml +75 -0
- dirigent_examples/shelves/docker/README.md +29 -0
- dirigent_examples/shelves/docker/docker-build-push.yaml +110 -0
- dirigent_examples/shelves/docker/docker-build-run.yaml +106 -0
- dirigent_examples/shelves/docker/docker-compose-database.yaml +124 -0
- dirigent_examples/shelves/docker/docker-compose-failing-up.yaml +83 -0
- dirigent_examples/shelves/docker/docker-compose-file.yaml +117 -0
- dirigent_examples/shelves/docker/docker-compose-profiles-env.yaml +133 -0
- dirigent_examples/shelves/docker/docker-compose-stack.yaml +70 -0
- dirigent_examples/shelves/docker/docker-hello.yaml +53 -0
- dirigent_examples/shelves/docker/docker-remote-daemon.yaml +92 -0
- dirigent_examples/shelves/docker/docker-run-failing-teardown.yaml +94 -0
- dirigent_examples/shelves/docker/docker-ticker.yaml +49 -0
- dirigent_examples/shelves/execute/README.md +16 -0
- dirigent_examples/shelves/execute/long-log.yaml +89 -0
- dirigent_examples/shelves/failure/README.md +20 -0
- dirigent_examples/shelves/failure/error-handler.yaml +89 -0
- dirigent_examples/shelves/failure/optional-step.yaml +82 -0
- dirigent_examples/shelves/failure/retries.yaml +75 -0
- dirigent_examples/shelves/failure/retry-budget.yaml +82 -0
- dirigent_examples/shelves/failure/step-timeout.yaml +96 -0
- dirigent_examples/shelves/git/README.md +32 -0
- dirigent_examples/shelves/git/git-checkout-build.yaml +125 -0
- dirigent_examples/shelves/git/git-checkout-compose.yaml +138 -0
- dirigent_examples/shelves/git/git-checkout-public.yaml +84 -0
- dirigent_examples/shelves/graph/README.md +22 -0
- dirigent_examples/shelves/graph/deep-chain.yaml +119 -0
- dirigent_examples/shelves/graph/fan-in.yaml +80 -0
- dirigent_examples/shelves/graph/fan-out.yaml +66 -0
- dirigent_examples/shelves/graph/linear.yaml +66 -0
- dirigent_examples/shelves/graph/parallel-branches.yaml +57 -0
- dirigent_examples/shelves/graph/parallel-sleep.yaml +55 -0
- dirigent_examples/shelves/graph/skip-diamond.yaml +105 -0
- dirigent_examples/shelves/graph/wide-fan.yaml +147 -0
- dirigent_examples/shelves/hello-world.yaml +30 -0
- dirigent_examples/shelves/open-data/README.md +67 -0
- dirigent_examples/shelves/open-data/feeds-composition.yaml +120 -0
- dirigent_examples/shelves/open-data/gdacs-disaster-updates.yaml +230 -0
- dirigent_examples/shelves/open-data/github-releases-relay.yaml +225 -0
- dirigent_examples/shelves/open-data/hdx-dataset-watch.yaml +211 -0
- dirigent_examples/shelves/open-data/kobo-submissions-to-csv.yaml +149 -0
- dirigent_examples/shelves/open-data/nominatim-geocode-facilities.yaml +169 -0
- dirigent_examples/shelves/open-data/odk-central-submissions.yaml +146 -0
- dirigent_examples/shelves/open-data/open-meteo-weekly-report.yaml +142 -0
- dirigent_examples/shelves/open-data/overpass-health-facilities.yaml +154 -0
- dirigent_examples/shelves/open-data/usgs-earthquakes-alert.yaml +208 -0
- dirigent_examples/shelves/open-data/who-gho-indicators-to-parquet.yaml +163 -0
- dirigent_examples/shelves/open-data/wikidata-country-reference.yaml +146 -0
- dirigent_examples/shelves/open-data/world-bank-population-trend.yaml +166 -0
- dirigent_examples/shelves/patterns/README.md +144 -0
- dirigent_examples/shelves/patterns/concurrency-queue.yaml +89 -0
- dirigent_examples/shelves/patterns/concurrency-replace.yaml +92 -0
- dirigent_examples/shelves/patterns/concurrency-skip.yaml +97 -0
- dirigent_examples/shelves/patterns/connections-referenced-vs-carried.yaml +140 -0
- dirigent_examples/shelves/patterns/deadline-on-a-sensor.yaml +106 -0
- dirigent_examples/shelves/patterns/fan-out-continue.yaml +88 -0
- dirigent_examples/shelves/patterns/fan-out-fail-fast.yaml +80 -0
- dirigent_examples/shelves/patterns/fan-out-from-params.yaml +84 -0
- dirigent_examples/shelves/patterns/fan-out-item-wise.yaml +111 -0
- dirigent_examples/shelves/patterns/fan-out-literal-list.yaml +81 -0
- dirigent_examples/shelves/patterns/fan-out-nested-objects.yaml +107 -0
- dirigent_examples/shelves/patterns/fan-out-then-join.yaml +86 -0
- dirigent_examples/shelves/patterns/log-levels.yaml +119 -0
- dirigent_examples/shelves/patterns/outputs-inline-vs-storage.yaml +140 -0
- dirigent_examples/shelves/patterns/params-every-type.yaml +259 -0
- dirigent_examples/shelves/patterns/params-validation-refuses.yaml +131 -0
- dirigent_examples/shelves/patterns/pipeline-run-child.yaml +96 -0
- dirigent_examples/shelves/patterns/pipeline-run-fire-and-forget.yaml +99 -0
- dirigent_examples/shelves/patterns/pipeline-run-strict.yaml +103 -0
- dirigent_examples/shelves/patterns/pipeline-run-wait.yaml +107 -0
- dirigent_examples/shelves/patterns/pipeline-run-with-params.yaml +124 -0
- dirigent_examples/shelves/patterns/poll-cadence.yaml +102 -0
- dirigent_examples/shelves/patterns/priority-layered.yaml +120 -0
- dirigent_examples/shelves/patterns/references-cheat-sheet.yaml +186 -0
- dirigent_examples/shelves/patterns/retry-budget-exhausted.yaml +92 -0
- dirigent_examples/shelves/patterns/retry-exponential-backoff.yaml +88 -0
- dirigent_examples/shelves/patterns/retry-only-transient.yaml +108 -0
- dirigent_examples/shelves/patterns/retry-with-jitter.yaml +102 -0
- dirigent_examples/shelves/patterns/rule-all-done.yaml +83 -0
- dirigent_examples/shelves/patterns/rule-all-success.yaml +79 -0
- dirigent_examples/shelves/patterns/rule-always.yaml +92 -0
- dirigent_examples/shelves/patterns/rule-one-failed.yaml +86 -0
- dirigent_examples/shelves/patterns/schedule-at-once.yaml +110 -0
- dirigent_examples/shelves/patterns/schedule-cron-timezone.yaml +121 -0
- dirigent_examples/shelves/patterns/schedule-interval.yaml +109 -0
- dirigent_examples/shelves/patterns/schedule-window-half-open.yaml +105 -0
- dirigent_examples/shelves/patterns/sensor-http-ready.yaml +121 -0
- dirigent_examples/shelves/patterns/sensor-storage-exists.yaml +134 -0
- dirigent_examples/shelves/patterns/step-names-and-keys.yaml +99 -0
- dirigent_examples/shelves/patterns/timeout-fails-the-step.yaml +94 -0
- dirigent_examples/shelves/patterns/timeout-skips-the-step.yaml +102 -0
- dirigent_examples/shelves/patterns/webhook-mapping-nested-payload.yaml +125 -0
- dirigent_examples/shelves/patterns/webhook-signed.yaml +144 -0
- dirigent_examples/shelves/preview/s3-parquet-to-ingestion.yaml +92 -0
- dirigent_examples/shelves/python/README.md +31 -0
- dirigent_examples/shelves/python/apply_and_run.py +52 -0
- dirigent_examples/shelves/python/ci_gate.py +76 -0
- dirigent_examples/shelves/python/connections.py +61 -0
- dirigent_examples/shelves/python/error_handling.py +84 -0
- dirigent_examples/shelves/python/follow_logs.py +39 -0
- dirigent_examples/shelves/python/list_and_filter.py +52 -0
- dirigent_examples/shelves/queues/README.md +59 -0
- dirigent_examples/shelves/queues/kafka-consume-then-transform.yaml +105 -0
- dirigent_examples/shelves/queues/kafka-produce-then-consume.yaml +124 -0
- dirigent_examples/shelves/queues/rabbitmq-consume-ack-on-success.yaml +102 -0
- dirigent_examples/shelves/queues/report-to-kafka.yaml +105 -0
- dirigent_examples/shelves/queues/report-to-rabbitmq.yaml +105 -0
- dirigent_examples/shelves/recipes/README.md +130 -0
- dirigent_examples/shelves/recipes/csv-header-rules.yaml +147 -0
- dirigent_examples/shelves/recipes/csv-to-ndjson.yaml +107 -0
- dirigent_examples/shelves/recipes/etl-csv-clean-validate-parquet.yaml +207 -0
- dirigent_examples/shelves/recipes/filter-by-predicate.yaml +116 -0
- dirigent_examples/shelves/recipes/filter-then-map-then-reduce.yaml +109 -0
- dirigent_examples/shelves/recipes/http-fetch-validate-post.yaml +142 -0
- dirigent_examples/shelves/recipes/http-follow-redirects.yaml +100 -0
- dirigent_examples/shelves/recipes/http-get-with-query.yaml +96 -0
- dirigent_examples/shelves/recipes/http-headers-and-auth-connection.yaml +110 -0
- dirigent_examples/shelves/recipes/http-post-file-from-storage.yaml +117 -0
- dirigent_examples/shelves/recipes/http-post-json-echo.yaml +105 -0
- dirigent_examples/shelves/recipes/http-post-report.yaml +183 -0
- dirigent_examples/shelves/recipes/http-save-body-to-storage.yaml +106 -0
- dirigent_examples/shelves/recipes/http-success-status-list.yaml +80 -0
- dirigent_examples/shelves/recipes/http-timeout-override.yaml +104 -0
- dirigent_examples/shelves/recipes/jq-dedupe-by-key.yaml +78 -0
- dirigent_examples/shelves/recipes/jq-defaults-and-nulls.yaml +91 -0
- dirigent_examples/shelves/recipes/jq-group-by-and-sum.yaml +74 -0
- dirigent_examples/shelves/recipes/jq-join-two-lists.yaml +77 -0
- dirigent_examples/shelves/recipes/jq-long-to-wide.yaml +76 -0
- dirigent_examples/shelves/recipes/jq-nested-to-flat.yaml +89 -0
- dirigent_examples/shelves/recipes/jq-pivot-wide-to-long.yaml +65 -0
- dirigent_examples/shelves/recipes/jq-running-totals.yaml +82 -0
- dirigent_examples/shelves/recipes/jq-string-cleaning.yaml +88 -0
- dirigent_examples/shelves/recipes/jq-top-n.yaml +85 -0
- dirigent_examples/shelves/recipes/jq-validate-in-jq-vs-schema.yaml +124 -0
- dirigent_examples/shelves/recipes/jq-window-dates.yaml +82 -0
- dirigent_examples/shelves/recipes/json-to-csv-flattening.yaml +141 -0
- dirigent_examples/shelves/recipes/large-output-to-storage.yaml +134 -0
- dirigent_examples/shelves/recipes/map-enrich-with-lookup.yaml +96 -0
- dirigent_examples/shelves/recipes/ndjson-to-parquet.yaml +130 -0
- dirigent_examples/shelves/recipes/pagination-by-fan-out.yaml +115 -0
- dirigent_examples/shelves/recipes/parquet-round-trip-types.yaml +163 -0
- dirigent_examples/shelves/recipes/reconcile-two-sources.yaml +159 -0
- dirigent_examples/shelves/recipes/report-built-in.yaml +72 -0
- dirigent_examples/shelves/recipes/report-daily-digest.yaml +186 -0
- dirigent_examples/shelves/recipes/report-to-file.yaml +131 -0
- dirigent_examples/shelves/recipes/report-to-webhook.yaml +136 -0
- dirigent_examples/shelves/recipes/schema-carried.yaml +112 -0
- dirigent_examples/shelves/recipes/schema-formats.yaml +107 -0
- dirigent_examples/shelves/recipes/schema-referenced.yaml +86 -0
- dirigent_examples/shelves/recipes/schema-refuses-then-rule.yaml +127 -0
- dirigent_examples/shelves/recipes/storage-copy-dated-archive.yaml +114 -0
- dirigent_examples/shelves/recipes/storage-exists-gate.yaml +127 -0
- dirigent_examples/shelves/recipes/storage-manifest-of-a-fan-out.yaml +104 -0
- dirigent_examples/shelves/recipes/storage-write-then-read.yaml +119 -0
- dirigent_examples/shelves/recipes/webhook-post-hmac.yaml +132 -0
- dirigent_examples/shelves/recipes/webhook-post-summary.yaml +142 -0
- dirigent_examples/shelves/s3/README.md +34 -0
- dirigent_examples/shelves/s3/report-to-s3.yaml +93 -0
- dirigent_examples/shelves/s3/s3-copy-and-verify.yaml +105 -0
- dirigent_examples/shelves/s3/s3-csv-report.yaml +87 -0
- dirigent_examples/shelves/s3/s3-parquet-report.yaml +77 -0
- dirigent_examples/shelves/s3/s3-round-trip.yaml +125 -0
- dirigent_examples/shelves/schemas/README.md +36 -0
- dirigent_examples/shelves/schemas/echo-reading.json +18 -0
- dirigent_examples/shelves/schemas/ou-record.json +13 -0
- dirigent_examples/shelves/schemas/station-reading.json +13 -0
- dirigent_examples/shelves/sensors/README.md +16 -0
- dirigent_examples/shelves/sensors/sensor-gate.yaml +65 -0
- dirigent_examples/shelves/sensors/time-window.yaml +61 -0
- dirigent_examples/shelves/sql/README.md +52 -0
- dirigent_examples/shelves/sql/duckdb-parquet-to-report.yaml +146 -0
- dirigent_examples/shelves/sql/sql-postgres-readonly.yaml +111 -0
- dirigent_examples/shelves/sql/sql-query-to-storage.yaml +85 -0
- dirigent_examples/shelves/sql/sql-sqlite-roundtrip.yaml +114 -0
- dirigent_examples/shelves/sql/warehouse.sql +42 -0
- dirigent_examples/shelves/transform/README.md +36 -0
- dirigent_examples/shelves/transform/csv-report.yaml +55 -0
- dirigent_examples/shelves/transform/jq-filter-and-map.yaml +70 -0
- dirigent_examples/shelves/transform/jq-group-and-aggregate.yaml +70 -0
- dirigent_examples/shelves/transform/jq-join-two-sources.yaml +98 -0
- dirigent_examples/shelves/transform/jq-reshape.yaml +91 -0
- dirigent_examples/shelves/transform/jq-stream-through-storage.yaml +112 -0
- dirigent_examples/shelves/transform/ndjson-round-trip.yaml +56 -0
- dirigent_examples/shelves/transform/parquet-round-trip.yaml +68 -0
- dirigent_examples/shelves/transform/std-convert-fan-out.yaml +142 -0
- dirigent_examples/shelves/transform/xml-feed-to-ndjson.yaml +116 -0
- dirigent_examples/shelves/transform/yaml-config-to-json.yaml +104 -0
- dirigent_examples/shelves/triggers/README.md +45 -0
- dirigent_examples/shelves/triggers/at-one-time.yaml +78 -0
- dirigent_examples/shelves/triggers/cron-nightly.yaml +79 -0
- dirigent_examples/shelves/triggers/cron-windowed.yaml +86 -0
- dirigent_examples/shelves/triggers/document-nightly.yaml +80 -0
- dirigent_examples/shelves/triggers/interval-rolling.yaml +88 -0
- dirigent_examples/shelves/triggers/managed-and-manual.yaml +109 -0
- dirigent_examples/shelves/triggers/webhook-trigger.yaml +75 -0
- dirigent_examples/shelves/validate/README.md +31 -0
- dirigent_examples/shelves/validate/expects-a-shape.yaml +56 -0
- dirigent_examples/shelves/validate/the-shape-is-wrong.yaml +46 -0
- dirigent_examples-0.15.0.dist-info/METADATA +21 -0
- dirigent_examples-0.15.0.dist-info/RECORD +216 -0
- dirigent_examples-0.15.0.dist-info/WHEEL +4 -0
- dirigent_examples-0.15.0.dist-info/entry_points.txt +3 -0
- dirigent_examples-0.15.0.dist-info/licenses/LICENSE +18 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# A shape written into the document itself, so the pipeline runs anywhere with nothing to
|
|
2
|
+
# hand it.
|
|
3
|
+
#
|
|
4
|
+
# In: a payload shaped like an API read -- an envelope around a list of org units.
|
|
5
|
+
# Out: the same payload, passed through the gate unchanged, and a count taken from the
|
|
6
|
+
# gate's output rather than from the step before it.
|
|
7
|
+
#
|
|
8
|
+
# A validate.schema gate never writes a shape inline in its config: its schema field is a
|
|
9
|
+
# code, and the code resolves either to a schema the instance holds or to one this document
|
|
10
|
+
# carries in its top-level schemas: section. This recipe is the carried half; its pair,
|
|
11
|
+
# schema-referenced.yaml, is the same check written the other way.
|
|
12
|
+
#
|
|
13
|
+
# Carried is the right choice when the shape belongs to this pipeline and nothing else, and
|
|
14
|
+
# when the document has to run somewhere that holds nothing -- a --local run, a colleague's
|
|
15
|
+
# checkout, a CI lane. The cost is that two documents carrying the same shape are two copies
|
|
16
|
+
# that drift.
|
|
17
|
+
#
|
|
18
|
+
# What the shape says, and why each keyword is there:
|
|
19
|
+
#
|
|
20
|
+
# required presence, which is a different question from type. A field named in
|
|
21
|
+
# properties but not required may be absent; one that is present is
|
|
22
|
+
# still held to its type.
|
|
23
|
+
# additionalProperties: false
|
|
24
|
+
# closes the door. JSON Schema is open by default, so an unexpected
|
|
25
|
+
# field passes silently unless the schema says otherwise. Closed is
|
|
26
|
+
# right for a payload this pipeline fully owns and wrong for one an
|
|
27
|
+
# upstream is free to extend.
|
|
28
|
+
# minItems an empty list is a shape that fits every item schema, so "there is
|
|
29
|
+
# at least one" has to be said out loud.
|
|
30
|
+
# pattern, minimum the value-level narrowing that turns a type check into a real gate.
|
|
31
|
+
#
|
|
32
|
+
# The gate is also a waypoint. Steps below it read ${steps.gate.output.value}, which is the
|
|
33
|
+
# input unchanged, and that reference is the document's own proof that nothing bypassed the
|
|
34
|
+
# check.
|
|
35
|
+
#
|
|
36
|
+
# To change it: -p level=0 makes the payload violate `minimum: 1`, and that run fails at the
|
|
37
|
+
# gate with the failing instance path and keyword named.
|
|
38
|
+
#
|
|
39
|
+
# The schema is carried in the document, so no server stores this one: it runs with
|
|
40
|
+
# `dg run --local`, and on an instance the same shape is created once with `dg schema create`.
|
|
41
|
+
#
|
|
42
|
+
# dg run --local examples/recipes/schema-carried.yaml
|
|
43
|
+
# dg run --local examples/recipes/schema-carried.yaml -p level=0
|
|
44
|
+
|
|
45
|
+
format: dirigent/v1
|
|
46
|
+
kind: pipeline
|
|
47
|
+
code: schema-carried
|
|
48
|
+
name: A schema carried in the document
|
|
49
|
+
description: Validate a payload against a shape the document carries, and read the gate's output downstream as proof of the check.
|
|
50
|
+
|
|
51
|
+
tags: [recipes, transform, validate]
|
|
52
|
+
|
|
53
|
+
requires:
|
|
54
|
+
blocks:
|
|
55
|
+
- value.const
|
|
56
|
+
- transform.jq
|
|
57
|
+
- validate.schema
|
|
58
|
+
|
|
59
|
+
schemas:
|
|
60
|
+
recipe-org-units:
|
|
61
|
+
title: Organisation units read
|
|
62
|
+
description: The envelope an org-unit read answers with, and the fields a loader depends on.
|
|
63
|
+
type: object
|
|
64
|
+
required: [organisationUnits]
|
|
65
|
+
additionalProperties: false
|
|
66
|
+
properties:
|
|
67
|
+
organisationUnits:
|
|
68
|
+
type: array
|
|
69
|
+
minItems: 1
|
|
70
|
+
items:
|
|
71
|
+
type: object
|
|
72
|
+
required: [id, displayName, level]
|
|
73
|
+
additionalProperties: false
|
|
74
|
+
properties:
|
|
75
|
+
id: { type: string, pattern: "^[A-Za-z][A-Za-z0-9]{10}$" }
|
|
76
|
+
displayName: { type: string, minLength: 1 }
|
|
77
|
+
level: { type: integer, minimum: 1 }
|
|
78
|
+
|
|
79
|
+
params:
|
|
80
|
+
type: object
|
|
81
|
+
properties:
|
|
82
|
+
level:
|
|
83
|
+
type: integer
|
|
84
|
+
default: 2
|
|
85
|
+
description: The level the second org unit reports; 0 makes the payload fail the gate.
|
|
86
|
+
|
|
87
|
+
steps:
|
|
88
|
+
payload:
|
|
89
|
+
block: value.const
|
|
90
|
+
config:
|
|
91
|
+
value:
|
|
92
|
+
organisationUnits:
|
|
93
|
+
- { id: ImspTQPwCqd, displayName: Sierra Leone, level: 1 }
|
|
94
|
+
# The level is a parameter so the gate can be made to refuse without editing the
|
|
95
|
+
# document: value.const emits what it is given, references included.
|
|
96
|
+
- { id: O6uvpzGd5pu, displayName: Bo, level: "${params.level}" }
|
|
97
|
+
|
|
98
|
+
gate:
|
|
99
|
+
block: validate.schema
|
|
100
|
+
depends_on: [payload]
|
|
101
|
+
config:
|
|
102
|
+
input: ${steps.payload.output.value}
|
|
103
|
+
schema: recipe-org-units
|
|
104
|
+
|
|
105
|
+
count:
|
|
106
|
+
block: transform.jq
|
|
107
|
+
depends_on: [gate]
|
|
108
|
+
config:
|
|
109
|
+
input: ${steps.gate.output.value}
|
|
110
|
+
program: |
|
|
111
|
+
{units: (.organisationUnits | length),
|
|
112
|
+
levels: [.organisationUnits[].level] | unique}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# format: the keyword that says what kind of string a string is, and here actually asserts.
|
|
2
|
+
#
|
|
3
|
+
# In: one record of strings -- a date, a timestamp, an email, a uri, a uuid, a hex digest,
|
|
4
|
+
# and one string carrying a format no pack on this instance contributes.
|
|
5
|
+
# Out: the record, through a gate that checked every one of them.
|
|
6
|
+
#
|
|
7
|
+
# In JSON Schema, format is an annotation by default: a validator only asserts it when it is
|
|
8
|
+
# handed a format checker, and plenty are not. Dirigent's engine always hands its validator
|
|
9
|
+
# one, so a format written here is enforced rather than decorative. That is the difference
|
|
10
|
+
# worth knowing before writing a schema for this engine and reusing it somewhere else.
|
|
11
|
+
#
|
|
12
|
+
# The standard formats are all available -- date, date-time, time, email, uri, hostname,
|
|
13
|
+
# ipv4, ipv6, regex, and the rest -- and dirigent registers the ones its own data speaks:
|
|
14
|
+
# ulid, uuid4, uuid7, md5, sha1, sha256, sha512, base64.
|
|
15
|
+
#
|
|
16
|
+
# A format only ever narrows a string. A value of the wrong type is caught by `type`, and
|
|
17
|
+
# format speaks only once the value is already a string, so `{type: integer, format: date}`
|
|
18
|
+
# asserts nothing about anything.
|
|
19
|
+
#
|
|
20
|
+
# The last field is the interesting one. acme-site-id is a format a plugin pack
|
|
21
|
+
# contributes; on an instance with that pack installed it asserts, and on one without it the
|
|
22
|
+
# keyword is a passing annotation and the value is simply unchecked. That is what keeps a
|
|
23
|
+
# schema portable: the same file asserts more on an instance that knows more, and refuses
|
|
24
|
+
# nothing it cannot check. This recipe runs green either way, and on an instance with the
|
|
25
|
+
# pack a site id of the wrong shape there would fail it.
|
|
26
|
+
#
|
|
27
|
+
# To change it: -p email=not-an-address, -p when=last%20Tuesday, or -p digest=abc all fail
|
|
28
|
+
# the gate, each naming the field and the format it did not satisfy.
|
|
29
|
+
#
|
|
30
|
+
# The schema is carried in the document, so no server stores this one: it runs with
|
|
31
|
+
# `dg run --local`, and on an instance the same shape is created once with `dg schema create`.
|
|
32
|
+
#
|
|
33
|
+
# dg run --local examples/recipes/schema-formats.yaml
|
|
34
|
+
# dg run --local examples/recipes/schema-formats.yaml -p email=not-an-address
|
|
35
|
+
|
|
36
|
+
format: dirigent/v1
|
|
37
|
+
kind: pipeline
|
|
38
|
+
code: schema-formats
|
|
39
|
+
name: Formats that actually assert
|
|
40
|
+
description: Hold a record of strings to date, date-time, email, uri, uuid4 and digest formats, and show what a pack-contributed format does.
|
|
41
|
+
|
|
42
|
+
tags: [recipes, validate]
|
|
43
|
+
|
|
44
|
+
requires:
|
|
45
|
+
blocks:
|
|
46
|
+
- value.const
|
|
47
|
+
- validate.schema
|
|
48
|
+
|
|
49
|
+
schemas:
|
|
50
|
+
recipe-formatted-record:
|
|
51
|
+
type: object
|
|
52
|
+
required: [day, when, email, endpoint, request_id, digest, site]
|
|
53
|
+
additionalProperties: false
|
|
54
|
+
properties:
|
|
55
|
+
day: { type: string, format: date }
|
|
56
|
+
when: { type: string, format: date-time }
|
|
57
|
+
email: { type: string, format: email }
|
|
58
|
+
endpoint: { type: string, format: uri }
|
|
59
|
+
request_id: { type: string, format: uuid4 }
|
|
60
|
+
digest: { type: string, format: sha256 }
|
|
61
|
+
# Contributed by a plugin pack. Where that pack is installed this asserts; where it
|
|
62
|
+
# is not, the value passes unchecked and the schema stays portable.
|
|
63
|
+
site: { type: string, format: acme-site-id }
|
|
64
|
+
|
|
65
|
+
params:
|
|
66
|
+
type: object
|
|
67
|
+
properties:
|
|
68
|
+
day:
|
|
69
|
+
type: string
|
|
70
|
+
default: "2026-01-01"
|
|
71
|
+
description: Held to format date; "last Tuesday" is refused.
|
|
72
|
+
when:
|
|
73
|
+
type: string
|
|
74
|
+
default: "2026-01-01T06:00:00Z"
|
|
75
|
+
description: Held to format date-time, which wants a zone as well as a time.
|
|
76
|
+
email:
|
|
77
|
+
type: string
|
|
78
|
+
default: ops@example.org
|
|
79
|
+
description: Held to format email.
|
|
80
|
+
endpoint:
|
|
81
|
+
type: string
|
|
82
|
+
default: https://postman-echo.com/get
|
|
83
|
+
description: Held to format uri.
|
|
84
|
+
digest:
|
|
85
|
+
type: string
|
|
86
|
+
default: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
|
|
87
|
+
description: Held to format sha256, which is exactly 64 hex characters.
|
|
88
|
+
|
|
89
|
+
steps:
|
|
90
|
+
record:
|
|
91
|
+
block: value.const
|
|
92
|
+
config:
|
|
93
|
+
value:
|
|
94
|
+
day: "${params.day}"
|
|
95
|
+
when: "${params.when}"
|
|
96
|
+
email: "${params.email}"
|
|
97
|
+
endpoint: "${params.endpoint}"
|
|
98
|
+
request_id: f47ac10b-58cc-4372-a567-0e02b2c3d479
|
|
99
|
+
digest: "${params.digest}"
|
|
100
|
+
site: NO-OSLO-01
|
|
101
|
+
|
|
102
|
+
gate:
|
|
103
|
+
block: validate.schema
|
|
104
|
+
depends_on: [record]
|
|
105
|
+
config:
|
|
106
|
+
input: ${steps.record.output.value}
|
|
107
|
+
schema: recipe-formatted-record
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# The same gate, written the other way: the shape lives on the instance and the document
|
|
2
|
+
# names it.
|
|
3
|
+
#
|
|
4
|
+
# In: one org-unit record.
|
|
5
|
+
# Out: that record, passed through a gate that names ou-record -- a schema the instance
|
|
6
|
+
# holds, not one this file carries.
|
|
7
|
+
#
|
|
8
|
+
# A schema is a resource in its own right, applied like a connection rather than as part of
|
|
9
|
+
# a pipeline:
|
|
10
|
+
#
|
|
11
|
+
# dg schema create examples/schemas/ou-record.json
|
|
12
|
+
# dg schema list
|
|
13
|
+
#
|
|
14
|
+
# and its identity comes out of its own keywords: $id becomes the code every reference uses,
|
|
15
|
+
# title its name, description its body. So the code in the gate below is the $id in that
|
|
16
|
+
# file and nothing else.
|
|
17
|
+
#
|
|
18
|
+
# Referenced is the right choice as soon as a second pipeline holds the same picture of the
|
|
19
|
+
# same payload. One shape, one place to change it, and every pipeline that names it is
|
|
20
|
+
# refused the moment they stop agreeing. It costs a dependency: requires.schemas is where
|
|
21
|
+
# the document says so, and an instance that does not hold ou-record refuses this document
|
|
22
|
+
# at apply, naming what to create first, rather than failing at three in the morning.
|
|
23
|
+
#
|
|
24
|
+
# A --local run starts on an empty throwaway instance, so the schema is handed to it the way
|
|
25
|
+
# connections are:
|
|
26
|
+
#
|
|
27
|
+
# dg run --local --schema examples/schemas/ou-record.json \
|
|
28
|
+
# examples/recipes/schema-referenced.yaml
|
|
29
|
+
#
|
|
30
|
+
# Without --schema that run is refused before anything executes, which is the preflight
|
|
31
|
+
# doing its job.
|
|
32
|
+
#
|
|
33
|
+
# To change it: -p level=zero sends a string where the schema wants an integer, and that run
|
|
34
|
+
# fails at the gate naming the instance path and the keyword.
|
|
35
|
+
#
|
|
36
|
+
# dg run --local --schema examples/schemas/ou-record.json examples/recipes/schema-referenced.yaml
|
|
37
|
+
# dg run --local --schema examples/schemas/ou-record.json examples/recipes/schema-referenced.yaml -p level=zero
|
|
38
|
+
|
|
39
|
+
format: dirigent/v1
|
|
40
|
+
kind: pipeline
|
|
41
|
+
code: schema-referenced
|
|
42
|
+
name: A schema the instance holds
|
|
43
|
+
description: Validate a record against ou-record, a schema applied to the instance on its own, and declare the dependency in requires.
|
|
44
|
+
|
|
45
|
+
tags: [recipes, transform, validate]
|
|
46
|
+
|
|
47
|
+
requires:
|
|
48
|
+
blocks:
|
|
49
|
+
- value.const
|
|
50
|
+
- transform.jq
|
|
51
|
+
- validate.schema
|
|
52
|
+
# The preflight that refuses this document on an instance holding no such schema.
|
|
53
|
+
schemas:
|
|
54
|
+
- ou-record
|
|
55
|
+
|
|
56
|
+
params:
|
|
57
|
+
type: object
|
|
58
|
+
properties:
|
|
59
|
+
level:
|
|
60
|
+
default: 2
|
|
61
|
+
description: The record's level; anything but an integer is refused at the gate.
|
|
62
|
+
|
|
63
|
+
steps:
|
|
64
|
+
record:
|
|
65
|
+
block: value.const
|
|
66
|
+
config:
|
|
67
|
+
value:
|
|
68
|
+
id: O6uvpzGd5pu
|
|
69
|
+
name: Bo
|
|
70
|
+
level: "${params.level}"
|
|
71
|
+
|
|
72
|
+
gate:
|
|
73
|
+
block: validate.schema
|
|
74
|
+
depends_on: [record]
|
|
75
|
+
config:
|
|
76
|
+
input: ${steps.record.output.value}
|
|
77
|
+
# A code, never a shape: this one names the schema stored under $id ou-record.
|
|
78
|
+
schema: ou-record
|
|
79
|
+
|
|
80
|
+
describe:
|
|
81
|
+
block: transform.jq
|
|
82
|
+
depends_on: [gate]
|
|
83
|
+
config:
|
|
84
|
+
input: ${steps.gate.output.value}
|
|
85
|
+
program: |
|
|
86
|
+
{checked_id: .id, at_level: .level}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# A gate that refuses, and the two branches below it: the one that runs and the one that
|
|
2
|
+
# does not.
|
|
3
|
+
#
|
|
4
|
+
# THIS RUN FAILS ON PURPOSE. It ends `failed`, with the gate failed, the load skipped, and
|
|
5
|
+
# the alert succeeded. That is the outcome the recipe is here to show; nothing is broken.
|
|
6
|
+
#
|
|
7
|
+
# In: a payload whose second record carries a level of 0, which the carried schema refuses.
|
|
8
|
+
# Out: a failed run whose alert step has a summary of what went wrong, and a load step that
|
|
9
|
+
# never started.
|
|
10
|
+
#
|
|
11
|
+
# The edge into a step carries a rule, and the rule is what decides whether that step runs
|
|
12
|
+
# once its dependencies have settled:
|
|
13
|
+
#
|
|
14
|
+
# all_success the default. Every dependency succeeded. The load below never starts.
|
|
15
|
+
# one_failed at least one dependency failed. This is an error handler, and it is drawn
|
|
16
|
+
# in the DAG rather than hidden inside a block's config.
|
|
17
|
+
# all_done every dependency finished, whatever it finished as. This is the cleanup
|
|
18
|
+
# edge: a step that must run whether the work passed or failed.
|
|
19
|
+
#
|
|
20
|
+
# A handler cannot read the output of the step that failed, because a failed step has no
|
|
21
|
+
# stored output; there is nothing to reference. So the alert reads what it can -- the run's
|
|
22
|
+
# own identity, and the payload as it was before the gate -- and says which pipeline and
|
|
23
|
+
# which run to look at. Reaching for ${steps.gate.output.value} in a one_failed handler is
|
|
24
|
+
# refused when the document is applied, which is the format catching the mistake early.
|
|
25
|
+
#
|
|
26
|
+
# The run's status is still `failed`, and that is deliberate: handling a failure is not the
|
|
27
|
+
# same as not having had one. A step that is allowed to fail without failing the run says so
|
|
28
|
+
# with `continue_on_failure`, which is a different recipe and a different intent.
|
|
29
|
+
#
|
|
30
|
+
# To change it: -p level=2 makes the payload fit the schema, and the same document runs
|
|
31
|
+
# green with the load taken and the alert skipped -- the pair worth running back to back.
|
|
32
|
+
#
|
|
33
|
+
# The schema is carried in the document, so no server stores this one: it runs with
|
|
34
|
+
# `dg run --local`, and on an instance the same shape is created once with `dg schema create`.
|
|
35
|
+
#
|
|
36
|
+
# dg run --local examples/recipes/schema-refuses-then-rule.yaml # fails, by design
|
|
37
|
+
# dg run --local examples/recipes/schema-refuses-then-rule.yaml -p level=2 # succeeds
|
|
38
|
+
|
|
39
|
+
format: dirigent/v1
|
|
40
|
+
kind: pipeline
|
|
41
|
+
code: schema-refuses-then-rule
|
|
42
|
+
name: A refused gate and what runs after it
|
|
43
|
+
description: A validation that fails on purpose, an error branch on rule one_failed, a load that is skipped, and a cleanup on all_done.
|
|
44
|
+
|
|
45
|
+
tags: [recipes, transform, validate, failure]
|
|
46
|
+
|
|
47
|
+
requires:
|
|
48
|
+
blocks:
|
|
49
|
+
- value.const
|
|
50
|
+
- transform.jq
|
|
51
|
+
- validate.schema
|
|
52
|
+
|
|
53
|
+
schemas:
|
|
54
|
+
recipe-levelled-units:
|
|
55
|
+
type: object
|
|
56
|
+
required: [organisationUnits]
|
|
57
|
+
properties:
|
|
58
|
+
organisationUnits:
|
|
59
|
+
type: array
|
|
60
|
+
minItems: 1
|
|
61
|
+
items:
|
|
62
|
+
type: object
|
|
63
|
+
required: [id, level]
|
|
64
|
+
properties:
|
|
65
|
+
id: { type: string, minLength: 1 }
|
|
66
|
+
level: { type: integer, minimum: 1 }
|
|
67
|
+
|
|
68
|
+
params:
|
|
69
|
+
type: object
|
|
70
|
+
properties:
|
|
71
|
+
level:
|
|
72
|
+
type: integer
|
|
73
|
+
default: 0
|
|
74
|
+
description: The second unit's level. The default of 0 is what the schema refuses.
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
payload:
|
|
78
|
+
block: value.const
|
|
79
|
+
config:
|
|
80
|
+
value:
|
|
81
|
+
organisationUnits:
|
|
82
|
+
- { id: ImspTQPwCqd, level: 1 }
|
|
83
|
+
- { id: O6uvpzGd5pu, level: "${params.level}" }
|
|
84
|
+
|
|
85
|
+
gate:
|
|
86
|
+
block: validate.schema
|
|
87
|
+
depends_on: [payload]
|
|
88
|
+
config:
|
|
89
|
+
input: ${steps.payload.output.value}
|
|
90
|
+
schema: recipe-levelled-units
|
|
91
|
+
|
|
92
|
+
load:
|
|
93
|
+
block: transform.jq
|
|
94
|
+
depends_on: [gate]
|
|
95
|
+
# The default rule, written out because this document is about rules. The gate failed,
|
|
96
|
+
# so this step is skipped and nothing it would have done happened.
|
|
97
|
+
rule: all_success
|
|
98
|
+
config:
|
|
99
|
+
input: ${steps.gate.output.value}
|
|
100
|
+
program: |
|
|
101
|
+
{loaded: (.organisationUnits | length)}
|
|
102
|
+
|
|
103
|
+
alert:
|
|
104
|
+
block: transform.jq
|
|
105
|
+
depends_on: [gate]
|
|
106
|
+
rule: one_failed
|
|
107
|
+
config:
|
|
108
|
+
# A failed step has no stored output, so the handler reads the run's own identity and
|
|
109
|
+
# the payload as it stood before the gate.
|
|
110
|
+
input:
|
|
111
|
+
run: ${run.id}
|
|
112
|
+
payload: ${steps.payload.output.value}
|
|
113
|
+
program: |
|
|
114
|
+
{alert: "org unit levels failed validation",
|
|
115
|
+
run: .run,
|
|
116
|
+
units: (.payload.organisationUnits | length),
|
|
117
|
+
suspect: [.payload.organisationUnits[] | select(.level < 1) | .id]}
|
|
118
|
+
|
|
119
|
+
close_out:
|
|
120
|
+
block: transform.jq
|
|
121
|
+
depends_on: [load, alert]
|
|
122
|
+
# Satisfied by a skip as much as by a success, so the run always finishes here.
|
|
123
|
+
rule: all_done
|
|
124
|
+
config:
|
|
125
|
+
input: ${run.id}
|
|
126
|
+
program: |
|
|
127
|
+
{closed_run: .}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# Keep a dated copy of what a run produced, with the layout decided in the document.
|
|
2
|
+
#
|
|
3
|
+
# In: a payload built inline and handed to storage.write.
|
|
4
|
+
# Out: that file, plus a copy at a dated archive key, and a manifest naming both with the
|
|
5
|
+
# byte count the copy reported.
|
|
6
|
+
#
|
|
7
|
+
# The first two hops are the pair every value makes on its way out: transform.jq produces a
|
|
8
|
+
# value, storage.write puts it at a URI, and the copy below works on the URI the write
|
|
9
|
+
# reported rather than on anything the run is still holding.
|
|
10
|
+
#
|
|
11
|
+
# storage.copy moves bytes from one URI to another and knows nothing else. It is not an S3
|
|
12
|
+
# block or a file block: the scheme in the URI decides the backend, so file:// to file://,
|
|
13
|
+
# file:// to s3:// and s3:// to s3:// are all the same step, and adding an object store to
|
|
14
|
+
# an instance changes no document.
|
|
15
|
+
#
|
|
16
|
+
# The archive key is where the thinking goes, because a key layout is a decision nobody
|
|
17
|
+
# revisits once a year of files is written under it. This one is prefix/YYYY/MM/DD/name:
|
|
18
|
+
#
|
|
19
|
+
# Date parts are separate segments, so a listing can be taken per year or per month
|
|
20
|
+
# without parsing names, and a lifecycle rule can expire a prefix.
|
|
21
|
+
# The date is the run's parameter and never the wall clock, so a rerun for last Tuesday
|
|
22
|
+
# writes last Tuesday's key rather than today's.
|
|
23
|
+
# The name is stable inside the dated prefix, so the newest file is always the one under
|
|
24
|
+
# the newest key rather than the one with the highest suffix.
|
|
25
|
+
#
|
|
26
|
+
# The copy reports bytes_copied, which is the cheapest possible integrity check: a manifest
|
|
27
|
+
# carrying it lets a later run notice a zero-byte archive without opening it.
|
|
28
|
+
#
|
|
29
|
+
# To change it: -p archive_prefix=... points the same copy at another root, and -p
|
|
30
|
+
# day=2026-02-03 writes into 2026/02/03 instead.
|
|
31
|
+
#
|
|
32
|
+
# dg run --local examples/recipes/storage-copy-dated-archive.yaml
|
|
33
|
+
# dg run --local examples/recipes/storage-copy-dated-archive.yaml -p day=2026-02-03
|
|
34
|
+
|
|
35
|
+
format: dirigent/v1
|
|
36
|
+
kind: pipeline
|
|
37
|
+
code: storage-copy-dated-archive
|
|
38
|
+
name: A dated archive copy
|
|
39
|
+
description: Write a file, copy it to a date-partitioned archive key, and record both URIs and the bytes copied in a manifest.
|
|
40
|
+
|
|
41
|
+
tags: [recipes, storage, transform]
|
|
42
|
+
|
|
43
|
+
requires:
|
|
44
|
+
blocks:
|
|
45
|
+
- transform.jq
|
|
46
|
+
- storage.write
|
|
47
|
+
- storage.copy
|
|
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 being archived; the archive key is built from it, not from now.
|
|
57
|
+
archive_prefix:
|
|
58
|
+
type: string
|
|
59
|
+
default: archive/readings
|
|
60
|
+
description: The root the dated key hangs under.
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
build:
|
|
64
|
+
block: transform.jq
|
|
65
|
+
config:
|
|
66
|
+
input:
|
|
67
|
+
day: ${params.day}
|
|
68
|
+
program: |
|
|
69
|
+
{day: .day,
|
|
70
|
+
readings: [range(5) | {station: "st-\(.)", celsius: (. * 2) - 3}]}
|
|
71
|
+
|
|
72
|
+
store:
|
|
73
|
+
block: storage.write
|
|
74
|
+
depends_on: [build]
|
|
75
|
+
config:
|
|
76
|
+
target: ${run.scratch}/current/readings.json
|
|
77
|
+
value: ${steps.build.output.value}
|
|
78
|
+
|
|
79
|
+
key:
|
|
80
|
+
block: transform.jq
|
|
81
|
+
depends_on: [build]
|
|
82
|
+
config:
|
|
83
|
+
input:
|
|
84
|
+
day: ${params.day}
|
|
85
|
+
prefix: ${params.archive_prefix}
|
|
86
|
+
program: |
|
|
87
|
+
. as {$day, $prefix}
|
|
88
|
+
| ($day | split("-")) as [$year, $month, $date]
|
|
89
|
+
| {key: "\($prefix)/\($year)/\($month)/\($date)/readings.json",
|
|
90
|
+
year: $year, month: $month, date: $date}
|
|
91
|
+
|
|
92
|
+
archive:
|
|
93
|
+
block: storage.copy
|
|
94
|
+
depends_on: [store, key]
|
|
95
|
+
config:
|
|
96
|
+
source: ${steps.store.output.uri}
|
|
97
|
+
# The key comes from the step that built it, so the layout is written once.
|
|
98
|
+
target: ${run.scratch}/${steps.key.output.value.key}
|
|
99
|
+
|
|
100
|
+
manifest:
|
|
101
|
+
block: transform.jq
|
|
102
|
+
depends_on: [archive]
|
|
103
|
+
config:
|
|
104
|
+
input:
|
|
105
|
+
source: ${steps.archive.output.source}
|
|
106
|
+
target: ${steps.archive.output.target}
|
|
107
|
+
bytes: ${steps.archive.output.bytes_copied}
|
|
108
|
+
day: ${params.day}
|
|
109
|
+
program: |
|
|
110
|
+
{day, source, target,
|
|
111
|
+
archived_bytes: .bytes,
|
|
112
|
+
# A zero-byte archive is the failure a manifest is for: it is visible here without
|
|
113
|
+
# anybody opening the object.
|
|
114
|
+
empty: (.bytes == 0)}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# Wait for an object before reading it, and set the floor that stops a half-written file
|
|
2
|
+
# being read.
|
|
3
|
+
#
|
|
4
|
+
# In: a file this pipeline writes itself, so the sensor has something to find.
|
|
5
|
+
# Out: the file's URI, size and modification time from the sensor, and a summary read from
|
|
6
|
+
# it afterwards.
|
|
7
|
+
#
|
|
8
|
+
# storage.exists is a sensor, not an operator. It observes and changes nothing, each poke is
|
|
9
|
+
# one cheap head request, and "not yet" is the expected answer rather than a failure. While
|
|
10
|
+
# it waits, the run holds no worker: it parks and is poked again on the schedule the step
|
|
11
|
+
# sets.
|
|
12
|
+
#
|
|
13
|
+
# The three knobs are step-level engine semantics, uniform across every sensor and never
|
|
14
|
+
# inside a block's config:
|
|
15
|
+
#
|
|
16
|
+
# poll how often to look. Seconds here because the file is already there; minutes
|
|
17
|
+
# is the honest setting for a drop somebody else produces.
|
|
18
|
+
# deadline how long to keep looking before giving up. A sensor without one waits 24h.
|
|
19
|
+
# on_timeout what a deadline means. `skip` says nobody dropped a file inside the window,
|
|
20
|
+
# which is a quiet day rather than a broken pipeline, and the steps below skip
|
|
21
|
+
# with it. `fail` says the drop was promised, and its absence is an incident.
|
|
22
|
+
#
|
|
23
|
+
# min_size is the one that saves a real pipeline. A producer uploading a large object makes
|
|
24
|
+
# the key visible before the bytes are all there, so a sensor without a floor hands a
|
|
25
|
+
# truncated file to the reader below it. The floor is set to what the smallest legitimate
|
|
26
|
+
# file could be, never to what a typical one is.
|
|
27
|
+
#
|
|
28
|
+
# The uri may glob in its final segments, so `${run.scratch}/drops/*.json` waits for
|
|
29
|
+
# whichever file arrives; the sensor's output names the one it actually found, which is why
|
|
30
|
+
# the reader below reads ${steps.dropped.output.uri} and not the pattern.
|
|
31
|
+
#
|
|
32
|
+
# The sensor observes, it does not read: it reports a URI, a size and a time, and nothing
|
|
33
|
+
# about what is inside. Turning that into a value is storage.read's job, one hop later, and
|
|
34
|
+
# that hop is where max_size decides how much of the drop this run will hold.
|
|
35
|
+
#
|
|
36
|
+
# To change it: point uri at a drop somebody else writes and raise the deadline; with
|
|
37
|
+
# on_timeout: skip that run ends green having done nothing, which is what a quiet day
|
|
38
|
+
# should look like.
|
|
39
|
+
#
|
|
40
|
+
# dg run --local examples/recipes/storage-exists-gate.yaml
|
|
41
|
+
# dg run --local examples/recipes/storage-exists-gate.yaml -p min_size=1mb # skips: no file is that big
|
|
42
|
+
|
|
43
|
+
format: dirigent/v1
|
|
44
|
+
kind: pipeline
|
|
45
|
+
code: storage-exists-gate
|
|
46
|
+
name: Wait for an object, then read it
|
|
47
|
+
description: Park on a storage.exists sensor with a size floor and a deadline, then read whatever file the sensor reports it found.
|
|
48
|
+
|
|
49
|
+
tags: [recipes, sensor, storage, transform]
|
|
50
|
+
|
|
51
|
+
requires:
|
|
52
|
+
blocks:
|
|
53
|
+
- transform.jq
|
|
54
|
+
- storage.write
|
|
55
|
+
- storage.exists
|
|
56
|
+
- storage.read
|
|
57
|
+
|
|
58
|
+
params:
|
|
59
|
+
type: object
|
|
60
|
+
properties:
|
|
61
|
+
day:
|
|
62
|
+
type: string
|
|
63
|
+
format: date
|
|
64
|
+
default: "2026-01-01"
|
|
65
|
+
description: The day the drop is named for.
|
|
66
|
+
min_size:
|
|
67
|
+
type: string
|
|
68
|
+
default: 32b
|
|
69
|
+
description: The floor that stops a half-written object being read. 1mb makes this run skip.
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
produce:
|
|
73
|
+
block: transform.jq
|
|
74
|
+
config:
|
|
75
|
+
input:
|
|
76
|
+
day: ${params.day}
|
|
77
|
+
program: |
|
|
78
|
+
{day: .day, readings: [range(8) | {station: "st-\(.)", celsius: (. - 3)}]}
|
|
79
|
+
|
|
80
|
+
drop:
|
|
81
|
+
block: storage.write
|
|
82
|
+
depends_on: [produce]
|
|
83
|
+
config:
|
|
84
|
+
target: ${run.scratch}/drops/${params.day}.json
|
|
85
|
+
value: ${steps.produce.output.value}
|
|
86
|
+
|
|
87
|
+
dropped:
|
|
88
|
+
block: storage.exists
|
|
89
|
+
depends_on: [drop]
|
|
90
|
+
poll: 2s
|
|
91
|
+
deadline: 30s
|
|
92
|
+
# A missing drop is a quiet day: the run ends green with this and everything below it
|
|
93
|
+
# skipped, rather than raising an incident nobody can act on.
|
|
94
|
+
on_timeout: skip
|
|
95
|
+
config:
|
|
96
|
+
# A glob, so whichever file lands that day is found; the sensor reports which.
|
|
97
|
+
uri: ${run.scratch}/drops/*.json
|
|
98
|
+
min_size: ${params.min_size}
|
|
99
|
+
|
|
100
|
+
read:
|
|
101
|
+
block: storage.read
|
|
102
|
+
depends_on: [dropped]
|
|
103
|
+
config:
|
|
104
|
+
# The file the sensor found, not the pattern it was given.
|
|
105
|
+
source: ${steps.dropped.output.uri}
|
|
106
|
+
max_size: 1mb
|
|
107
|
+
|
|
108
|
+
summary:
|
|
109
|
+
block: transform.jq
|
|
110
|
+
depends_on: [read]
|
|
111
|
+
config:
|
|
112
|
+
input: ${steps.read.output.value}
|
|
113
|
+
program: |
|
|
114
|
+
{day, rows: (.readings | length),
|
|
115
|
+
warmest: (.readings | max_by(.celsius) | .station)}
|
|
116
|
+
|
|
117
|
+
receipt:
|
|
118
|
+
block: transform.jq
|
|
119
|
+
depends_on: [dropped, summary]
|
|
120
|
+
config:
|
|
121
|
+
input:
|
|
122
|
+
uri: ${steps.dropped.output.uri}
|
|
123
|
+
size: ${steps.dropped.output.size}
|
|
124
|
+
modified_at: ${steps.dropped.output.modified_at}
|
|
125
|
+
summary: ${steps.summary.output.value}
|
|
126
|
+
program: |
|
|
127
|
+
{found: .uri, size_bytes: .size, modified_at, summary}
|