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,104 @@
|
|
|
1
|
+
# One file per item, then one manifest listing all of them.
|
|
2
|
+
#
|
|
3
|
+
# In: a list of regions, as a parameter.
|
|
4
|
+
# Out: one JSON file per region in the run's scratch space, and a manifest naming every file
|
|
5
|
+
# with its byte count.
|
|
6
|
+
#
|
|
7
|
+
# A fan-out is `for_each` on a step: the run gets one item per element, each with its own
|
|
8
|
+
# status, error, retry budget and output. Two things about it shape this recipe:
|
|
9
|
+
#
|
|
10
|
+
# for_each is expanded when the run is created, so it reads params.*, run.*, and another
|
|
11
|
+
# fan-out's grid as ${steps.<step>.items} -- never a step's output. That is why the regions
|
|
12
|
+
# are a parameter here and not the result of an upstream read: the item grid exists from the
|
|
13
|
+
# moment the run is visible.
|
|
14
|
+
# A fan-out step's output is the list of its items' outputs, in item order. So the step
|
|
15
|
+
# after the fan reads the whole batch at once, and it reads ${steps.<step>.output} --
|
|
16
|
+
# the list -- rather than ${steps.<step>.output.uri}, which is a field of one item's
|
|
17
|
+
# output and not of the list.
|
|
18
|
+
#
|
|
19
|
+
# The fanned step is the storage.write itself, which is the shortest form when one step is
|
|
20
|
+
# doing the work and the writing. Where they are two steps, the second maps over the first's
|
|
21
|
+
# grid with for_each: ${steps.<step>.items} and reads its match -- that is
|
|
22
|
+
# examples/patterns/fan-out-item-wise.yaml.
|
|
23
|
+
#
|
|
24
|
+
# ${item} is the element, and it goes into the target as well as into the value, which is
|
|
25
|
+
# what keeps two items from writing the same key. Items run concurrently, so a shared target
|
|
26
|
+
# would be a race with a winner nobody chose.
|
|
27
|
+
#
|
|
28
|
+
# The manifest is the point. A fan-out that writes twelve files and reports nothing leaves
|
|
29
|
+
# "did region nine actually land" to somebody listing a bucket by hand; a manifest step
|
|
30
|
+
# turns the run itself into the answer, and it costs one jq program over outputs the engine
|
|
31
|
+
# already collected.
|
|
32
|
+
#
|
|
33
|
+
# items: continue is the other half of the same idea -- under it one bad region does not
|
|
34
|
+
# fail the step, the failures are recorded against their own items, and the run reports
|
|
35
|
+
# completed_with_errors. The manifest then lists what did land, which is exactly what a
|
|
36
|
+
# partial run needs to say.
|
|
37
|
+
#
|
|
38
|
+
# To change it: -p regions='["east","west"]' fans two ways instead of four.
|
|
39
|
+
#
|
|
40
|
+
# dg run --local examples/recipes/storage-manifest-of-a-fan-out.yaml
|
|
41
|
+
# dg run --local examples/recipes/storage-manifest-of-a-fan-out.yaml -p regions='["east"]'
|
|
42
|
+
|
|
43
|
+
format: dirigent/v1
|
|
44
|
+
kind: pipeline
|
|
45
|
+
code: storage-manifest-of-a-fan-out
|
|
46
|
+
name: A manifest of a fan-out
|
|
47
|
+
description: Write one file per item with for_each, then read the fan's outputs back as one list and record every file in a manifest.
|
|
48
|
+
|
|
49
|
+
tags: [recipes, storage, transform, graph]
|
|
50
|
+
|
|
51
|
+
requires:
|
|
52
|
+
blocks:
|
|
53
|
+
- storage.write
|
|
54
|
+
- transform.jq
|
|
55
|
+
|
|
56
|
+
params:
|
|
57
|
+
type: object
|
|
58
|
+
properties:
|
|
59
|
+
regions:
|
|
60
|
+
type: array
|
|
61
|
+
default: [east, west, north, south]
|
|
62
|
+
items:
|
|
63
|
+
type: string
|
|
64
|
+
description: One item, and one file, per element.
|
|
65
|
+
day:
|
|
66
|
+
type: string
|
|
67
|
+
format: date
|
|
68
|
+
default: "2026-01-01"
|
|
69
|
+
description: The day the files are named for.
|
|
70
|
+
|
|
71
|
+
steps:
|
|
72
|
+
per_region:
|
|
73
|
+
block: storage.write
|
|
74
|
+
for_each: ${params.regions}
|
|
75
|
+
# items: continue would let one bad region be recorded against its own item instead of
|
|
76
|
+
# failing the step; the default, fail_fast, is right when a missing region is an outage.
|
|
77
|
+
items: fail_fast
|
|
78
|
+
config:
|
|
79
|
+
# The item is in the key, so two items never write the same file.
|
|
80
|
+
target: ${run.scratch}/regions/${params.day}/${item}.json
|
|
81
|
+
value:
|
|
82
|
+
region: ${item}
|
|
83
|
+
day: ${params.day}
|
|
84
|
+
rows:
|
|
85
|
+
- { station: "${item}-st-0", celsius: -4 }
|
|
86
|
+
- { station: "${item}-st-1", celsius: -1 }
|
|
87
|
+
- { station: "${item}-st-2", celsius: 2 }
|
|
88
|
+
|
|
89
|
+
manifest:
|
|
90
|
+
block: transform.jq
|
|
91
|
+
depends_on: [per_region]
|
|
92
|
+
config:
|
|
93
|
+
# The fan-out's output is the list of its items' outputs, so this reads .output and
|
|
94
|
+
# not .output.uri.
|
|
95
|
+
input:
|
|
96
|
+
items: ${steps.per_region.output}
|
|
97
|
+
day: ${params.day}
|
|
98
|
+
program: |
|
|
99
|
+
. as {$items, $day}
|
|
100
|
+
| {day: $day,
|
|
101
|
+
files: ($items | length),
|
|
102
|
+
total_bytes: ([$items[].bytes_written] | add),
|
|
103
|
+
entries: [$items[] | {uri, bytes: .bytes_written}],
|
|
104
|
+
empty: [$items[] | select(.bytes_written == 0) | .uri]}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# Hand work from one step to the next through storage, which is how a big payload travels.
|
|
2
|
+
#
|
|
3
|
+
# In: a list built inline.
|
|
4
|
+
# Out: two files in the run's scratch space and a summary read back out of the second one.
|
|
5
|
+
#
|
|
6
|
+
# A value flows through step outputs. storage.write is the only way one goes out and
|
|
7
|
+
# storage.read the only way one comes back in, so a chain that wants to hand work along
|
|
8
|
+
# through storage is written as that pair, twice:
|
|
9
|
+
#
|
|
10
|
+
# storage.write target, and exactly one of text or value. Output: uri, bytes_written,
|
|
11
|
+
# content_type. A value is written as canonical JSON -- sorted keys, no
|
|
12
|
+
# spaces -- so the same value writes the same bytes every run.
|
|
13
|
+
# storage.read source, and max_size: how much of the object this step will hold, 1mb
|
|
14
|
+
# unless it says otherwise. An object above the cap is refused rather than
|
|
15
|
+
# truncated, because half a document is not a smaller one, it is a wrong
|
|
16
|
+
# one. The object comes back as `value` when it is JSON and as `text`
|
|
17
|
+
# otherwise, and a type it cannot decode it refuses instead of guessing.
|
|
18
|
+
#
|
|
19
|
+
# ${run.scratch} is the run's own prefix in whatever storage the instance is configured
|
|
20
|
+
# with. On a --local run that is a directory under a temporary tree that is deleted when the
|
|
21
|
+
# run ends; on a server it is the artifact root, and with an s3 connection configured for
|
|
22
|
+
# the scheme it is a bucket. The document says none of that: it names a scheme-agnostic
|
|
23
|
+
# prefix, and where the bytes land is an instance setting.
|
|
24
|
+
#
|
|
25
|
+
# Every read below is spelled ${steps.<step>.output.uri} rather than the same path typed
|
|
26
|
+
# twice: one place decides where a file went, and a rename is a one-line change.
|
|
27
|
+
#
|
|
28
|
+
# To change it: point the targets at s3://<bucket>/... and nothing else in the document
|
|
29
|
+
# moves.
|
|
30
|
+
#
|
|
31
|
+
# dg run --local examples/recipes/storage-write-then-read.yaml
|
|
32
|
+
# dg run --local examples/recipes/storage-write-then-read.yaml -p day=2026-02-01
|
|
33
|
+
|
|
34
|
+
format: dirigent/v1
|
|
35
|
+
kind: pipeline
|
|
36
|
+
code: storage-write-then-read
|
|
37
|
+
name: Write to storage, then read it back
|
|
38
|
+
description: Put a transform's result in the run's scratch space with storage.write, read it into the next step with storage.read, and do it again with what that step produced.
|
|
39
|
+
|
|
40
|
+
tags: [recipes, storage, transform, starter]
|
|
41
|
+
|
|
42
|
+
requires:
|
|
43
|
+
blocks:
|
|
44
|
+
- transform.jq
|
|
45
|
+
- storage.write
|
|
46
|
+
- storage.read
|
|
47
|
+
|
|
48
|
+
params:
|
|
49
|
+
type: object
|
|
50
|
+
properties:
|
|
51
|
+
day:
|
|
52
|
+
type: string
|
|
53
|
+
format: date
|
|
54
|
+
default: "2026-01-01"
|
|
55
|
+
description: The day the written files are named for.
|
|
56
|
+
stations:
|
|
57
|
+
type: integer
|
|
58
|
+
minimum: 1
|
|
59
|
+
default: 50
|
|
60
|
+
description: How many rows are generated.
|
|
61
|
+
|
|
62
|
+
steps:
|
|
63
|
+
generate:
|
|
64
|
+
block: transform.jq
|
|
65
|
+
config:
|
|
66
|
+
input:
|
|
67
|
+
count: ${params.stations}
|
|
68
|
+
day: ${params.day}
|
|
69
|
+
program: |
|
|
70
|
+
. as {$count, $day}
|
|
71
|
+
| [range($count)
|
|
72
|
+
| {station: "st-\(.)", day: $day,
|
|
73
|
+
celsius: (. % 17) - 5,
|
|
74
|
+
active: (. % 5 != 0)}]
|
|
75
|
+
|
|
76
|
+
save:
|
|
77
|
+
block: storage.write
|
|
78
|
+
depends_on: [generate]
|
|
79
|
+
config:
|
|
80
|
+
target: ${run.scratch}/readings-${params.day}.json
|
|
81
|
+
value: ${steps.generate.output.value}
|
|
82
|
+
|
|
83
|
+
back:
|
|
84
|
+
block: storage.read
|
|
85
|
+
depends_on: [save]
|
|
86
|
+
config:
|
|
87
|
+
# Where the write said the object landed, not the same path typed twice.
|
|
88
|
+
source: ${steps.save.output.uri}
|
|
89
|
+
max_size: 8mb
|
|
90
|
+
|
|
91
|
+
cold:
|
|
92
|
+
block: transform.jq
|
|
93
|
+
depends_on: [back]
|
|
94
|
+
config:
|
|
95
|
+
# The rows as they came off storage, not as they were held in memory upstream.
|
|
96
|
+
input: ${steps.back.output.value}
|
|
97
|
+
program: |
|
|
98
|
+
[.[] | select(.celsius < 0)]
|
|
99
|
+
|
|
100
|
+
save_cold:
|
|
101
|
+
block: storage.write
|
|
102
|
+
depends_on: [cold]
|
|
103
|
+
config:
|
|
104
|
+
target: ${run.scratch}/cold-${params.day}.json
|
|
105
|
+
value: ${steps.cold.output.value}
|
|
106
|
+
|
|
107
|
+
back_cold:
|
|
108
|
+
block: storage.read
|
|
109
|
+
depends_on: [save_cold]
|
|
110
|
+
config:
|
|
111
|
+
source: ${steps.save_cold.output.uri}
|
|
112
|
+
|
|
113
|
+
summary:
|
|
114
|
+
block: transform.jq
|
|
115
|
+
depends_on: [back_cold]
|
|
116
|
+
config:
|
|
117
|
+
input: ${steps.back_cold.output.value}
|
|
118
|
+
program: |
|
|
119
|
+
{cold_rows: length, coldest: (min_by(.celsius) | .station)}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# A signed webhook: what gets signed, with what, and what the receiver checks.
|
|
2
|
+
#
|
|
3
|
+
# In: a small event built inline.
|
|
4
|
+
# Out: the echo service's view of the delivery, including the X-Dirigent-Signature header,
|
|
5
|
+
# and the step's own `signed: true`.
|
|
6
|
+
#
|
|
7
|
+
# webhook.post serialises the body itself and signs the bytes that go on the wire. That
|
|
8
|
+
# order is the entire security property: if the body were handed to an HTTP client that
|
|
9
|
+
# re-encoded it -- a different key order, different spacing -- the signature would cover
|
|
10
|
+
# bytes the receiver never sees, and every delivery would fail verification for a reason
|
|
11
|
+
# nobody could find.
|
|
12
|
+
#
|
|
13
|
+
# The signature is HMAC-SHA256 over the raw body, hex, in X-Dirigent-Signature. A receiver
|
|
14
|
+
# verifies by computing the same HMAC over the body exactly as it arrived -- before parsing,
|
|
15
|
+
# never after re-serialising -- and comparing in constant time. Dirigent's own inbound
|
|
16
|
+
# webhook trigger does precisely that, so an instance can hand work to another instance with
|
|
17
|
+
# nothing shared but the secret.
|
|
18
|
+
#
|
|
19
|
+
# sign_with names a connection, not a secret. The secret is that connection's hmac_secret, a
|
|
20
|
+
# SecretStr sealed at rest and redacted in every response, and a connection without one
|
|
21
|
+
# fails the step saying so rather than sending an unsigned POST that looks signed.
|
|
22
|
+
#
|
|
23
|
+
# The connection is carried inline here, labelled, so the recipe runs standalone -- the
|
|
24
|
+
# secret below is a demo string in a public file and protects nothing. On an instance it is
|
|
25
|
+
# created once and the document names its code.
|
|
26
|
+
#
|
|
27
|
+
# A signature is not a substitute for TLS, and it is not a replay defence. It says the body
|
|
28
|
+
# came from someone holding the secret; it says nothing about when. A receiver that cares
|
|
29
|
+
# about replay checks a timestamp or an id it has seen before, which is why the body carries
|
|
30
|
+
# both.
|
|
31
|
+
#
|
|
32
|
+
# The same body is delivered twice, once with sign_with and once without, through the same
|
|
33
|
+
# connection: signing is the step's decision, and the output shows what the second delivery
|
|
34
|
+
# is missing.
|
|
35
|
+
#
|
|
36
|
+
# To change it: point the connection's base_url at a receiver you own and give it the same
|
|
37
|
+
# hmac_secret; nothing in the steps changes.
|
|
38
|
+
#
|
|
39
|
+
# dg run --local examples/recipes/webhook-post-hmac.yaml
|
|
40
|
+
# dg run --local examples/recipes/webhook-post-hmac.yaml -p day=2026-02-01
|
|
41
|
+
|
|
42
|
+
format: dirigent/v1
|
|
43
|
+
kind: pipeline
|
|
44
|
+
code: webhook-post-hmac
|
|
45
|
+
name: A signed webhook delivery
|
|
46
|
+
description: POST an event with an HMAC-SHA256 signature over the exact bytes sent, using a secret that lives on a connection.
|
|
47
|
+
|
|
48
|
+
tags: [recipes, transform, webhook]
|
|
49
|
+
|
|
50
|
+
requires:
|
|
51
|
+
blocks:
|
|
52
|
+
- transform.jq
|
|
53
|
+
- webhook.post
|
|
54
|
+
|
|
55
|
+
# Carried so the recipe runs standalone. hmac_secret is a SecretStr; this one is a demo
|
|
56
|
+
# string in a public file and protects nothing.
|
|
57
|
+
connections:
|
|
58
|
+
echo-webhook:
|
|
59
|
+
kind: http
|
|
60
|
+
config:
|
|
61
|
+
base_url: https://postman-echo.com
|
|
62
|
+
hmac_secret: a shared secret between two dirigent instances
|
|
63
|
+
timeout: 30s
|
|
64
|
+
health_path: /get
|
|
65
|
+
|
|
66
|
+
params:
|
|
67
|
+
type: object
|
|
68
|
+
properties:
|
|
69
|
+
day:
|
|
70
|
+
type: string
|
|
71
|
+
format: date
|
|
72
|
+
default: "2026-01-01"
|
|
73
|
+
description: The day the event reports on.
|
|
74
|
+
|
|
75
|
+
steps:
|
|
76
|
+
event:
|
|
77
|
+
block: transform.jq
|
|
78
|
+
config:
|
|
79
|
+
input:
|
|
80
|
+
day: ${params.day}
|
|
81
|
+
program: |
|
|
82
|
+
{kind: "import.finished",
|
|
83
|
+
day: .day,
|
|
84
|
+
# A receiver that cares about replay needs an identity and a time; a signature
|
|
85
|
+
# gives it neither.
|
|
86
|
+
event_id: "import-\(.day)",
|
|
87
|
+
at: "\(.day)T06:15:00Z",
|
|
88
|
+
rows: 1440}
|
|
89
|
+
|
|
90
|
+
deliver:
|
|
91
|
+
block: webhook.post
|
|
92
|
+
depends_on: [event]
|
|
93
|
+
config:
|
|
94
|
+
connection: echo-webhook
|
|
95
|
+
path: /post
|
|
96
|
+
body: ${steps.event.output.value}
|
|
97
|
+
# The connection whose hmac_secret signs the bytes. Unset means an unsigned POST.
|
|
98
|
+
sign_with: echo-webhook
|
|
99
|
+
headers:
|
|
100
|
+
x-event-kind: import.finished
|
|
101
|
+
|
|
102
|
+
deliver_unsigned:
|
|
103
|
+
block: webhook.post
|
|
104
|
+
depends_on: [event]
|
|
105
|
+
config:
|
|
106
|
+
connection: echo-webhook
|
|
107
|
+
path: /post
|
|
108
|
+
body: ${steps.event.output.value}
|
|
109
|
+
# No sign_with, and the same connection: signing is the step's decision, not the
|
|
110
|
+
# connection's.
|
|
111
|
+
headers:
|
|
112
|
+
x-event-kind: import.finished
|
|
113
|
+
|
|
114
|
+
receipt:
|
|
115
|
+
block: transform.jq
|
|
116
|
+
depends_on: [deliver, deliver_unsigned]
|
|
117
|
+
config:
|
|
118
|
+
input:
|
|
119
|
+
status: ${steps.deliver.output.status}
|
|
120
|
+
signed: ${steps.deliver.output.signed}
|
|
121
|
+
echoed: ${steps.deliver.output.json_body}
|
|
122
|
+
unsigned_signed: ${steps.deliver_unsigned.output.signed}
|
|
123
|
+
unsigned_echoed: ${steps.deliver_unsigned.output.json_body}
|
|
124
|
+
program: |
|
|
125
|
+
{status, signed,
|
|
126
|
+
# The echo service shows what arrived: the signature header, and the body it
|
|
127
|
+
# parsed out of the exact bytes that were signed.
|
|
128
|
+
signature: (.echoed.headers["x-dirigent-signature"] // null),
|
|
129
|
+
delivered_event: .echoed.json.event_id,
|
|
130
|
+
content_type: .echoed.headers["content-type"],
|
|
131
|
+
unsigned: {signed: .unsigned_signed,
|
|
132
|
+
signature: (.unsigned_echoed.headers["x-dirigent-signature"] // null)}}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Tell somebody what a run did, in a body they can act on rather than a sentence.
|
|
2
|
+
#
|
|
3
|
+
# In: a batch of load results, some of them failures.
|
|
4
|
+
# Out: a notification POSTed to a receiver, and its answer -- with the summary built as
|
|
5
|
+
# structure and the human sentence derived from it, not the other way round.
|
|
6
|
+
#
|
|
7
|
+
# A notification body is an interface, so this recipe builds one rather than formatting a
|
|
8
|
+
# message:
|
|
9
|
+
#
|
|
10
|
+
# Counts before prose. rows, loaded, rejected and a boolean the receiver can route on.
|
|
11
|
+
# The identifiers to act with: the run id, the pipeline's day, and the rejected keys --
|
|
12
|
+
# not "3 records failed", which nobody can look up.
|
|
13
|
+
# A `text` field derived from the numbers, for a chat client that renders one line. It is
|
|
14
|
+
# built last, from the same object, so it can never disagree with the counts beside it.
|
|
15
|
+
# A severity the receiver can route on, decided here where the meaning of the numbers is
|
|
16
|
+
# known, rather than by a rule written in the receiver's config.
|
|
17
|
+
#
|
|
18
|
+
# webhook.post is the block for this rather than http.request, because a notification is a
|
|
19
|
+
# delivery: it takes sign_with when the receiver verifies signatures, it caps the response
|
|
20
|
+
# it reads at 1mb because nobody wants a receiver's HTML error page in a step output, and
|
|
21
|
+
# success_status is how a receiver that answers 202 is accommodated.
|
|
22
|
+
#
|
|
23
|
+
# The delivery is a step in the DAG like any other, so a rule decides when it runs. This one
|
|
24
|
+
# is `all_done`, so the notification is sent whether the batch succeeded or not -- a
|
|
25
|
+
# notifier that only fires on success is one that goes quiet exactly when it matters.
|
|
26
|
+
#
|
|
27
|
+
# To change it: -p failures=0 sends the green version, and severity, text and routing all
|
|
28
|
+
# follow from the numbers without another branch in the document.
|
|
29
|
+
#
|
|
30
|
+
# dg run --local examples/recipes/webhook-post-summary.yaml
|
|
31
|
+
# dg run --local examples/recipes/webhook-post-summary.yaml -p failures=0
|
|
32
|
+
|
|
33
|
+
format: dirigent/v1
|
|
34
|
+
kind: pipeline
|
|
35
|
+
code: webhook-post-summary
|
|
36
|
+
name: A run summary as a webhook
|
|
37
|
+
description: Build a notification body of counts and identifiers, derive its human line from those counts, and deliver it on all_done.
|
|
38
|
+
|
|
39
|
+
tags: [recipes, transform, webhook]
|
|
40
|
+
|
|
41
|
+
requires:
|
|
42
|
+
blocks:
|
|
43
|
+
- transform.jq
|
|
44
|
+
- webhook.post
|
|
45
|
+
|
|
46
|
+
connections:
|
|
47
|
+
echo-webhook:
|
|
48
|
+
kind: http
|
|
49
|
+
config:
|
|
50
|
+
base_url: https://postman-echo.com
|
|
51
|
+
timeout: 30s
|
|
52
|
+
health_path: /get
|
|
53
|
+
|
|
54
|
+
params:
|
|
55
|
+
type: object
|
|
56
|
+
properties:
|
|
57
|
+
day:
|
|
58
|
+
type: string
|
|
59
|
+
format: date
|
|
60
|
+
default: "2026-01-01"
|
|
61
|
+
description: The day the load reports on.
|
|
62
|
+
rows:
|
|
63
|
+
type: integer
|
|
64
|
+
minimum: 1
|
|
65
|
+
default: 12
|
|
66
|
+
description: How many records the load handled.
|
|
67
|
+
failures:
|
|
68
|
+
type: integer
|
|
69
|
+
minimum: 0
|
|
70
|
+
default: 3
|
|
71
|
+
description: How many of them were rejected; 0 sends the green version.
|
|
72
|
+
|
|
73
|
+
steps:
|
|
74
|
+
results:
|
|
75
|
+
block: transform.jq
|
|
76
|
+
config:
|
|
77
|
+
input:
|
|
78
|
+
rows: ${params.rows}
|
|
79
|
+
failures: ${params.failures}
|
|
80
|
+
day: ${params.day}
|
|
81
|
+
program: |
|
|
82
|
+
. as {$rows, $failures, $day}
|
|
83
|
+
| [range($rows)
|
|
84
|
+
| {key: "\($day)/rec-\(.)",
|
|
85
|
+
status: (if . < $failures then "rejected" else "loaded" end),
|
|
86
|
+
reason: (if . < $failures then "period is outside the data set" else null end)}]
|
|
87
|
+
|
|
88
|
+
summary:
|
|
89
|
+
block: transform.jq
|
|
90
|
+
depends_on: [results]
|
|
91
|
+
config:
|
|
92
|
+
input:
|
|
93
|
+
rows: ${steps.results.output.value}
|
|
94
|
+
day: ${params.day}
|
|
95
|
+
run: ${run.id}
|
|
96
|
+
program: |
|
|
97
|
+
. as {$rows, $day, $run}
|
|
98
|
+
| {kind: "load.finished",
|
|
99
|
+
day: $day,
|
|
100
|
+
run: $run,
|
|
101
|
+
rows: ($rows | length),
|
|
102
|
+
loaded: ([$rows[] | select(.status == "loaded")] | length),
|
|
103
|
+
rejected: ([$rows[] | select(.status == "rejected")] | length),
|
|
104
|
+
# The keys, so the receiver can look them up; not a count of them, which nobody
|
|
105
|
+
# can act on.
|
|
106
|
+
rejected_keys: [$rows[] | select(.status == "rejected") | .key]}
|
|
107
|
+
| . + {clean: (.rejected == 0),
|
|
108
|
+
severity: (if .rejected == 0 then "info"
|
|
109
|
+
elif .rejected < (.rows / 2) then "warning"
|
|
110
|
+
else "error" end)}
|
|
111
|
+
# The sentence is derived from the counts, last, so it cannot contradict them.
|
|
112
|
+
| . + {text: "\(.day): \(.loaded)/\(.rows) loaded, \(.rejected) rejected"}
|
|
113
|
+
|
|
114
|
+
notify:
|
|
115
|
+
block: webhook.post
|
|
116
|
+
depends_on: [summary]
|
|
117
|
+
# Sent whether the work above passed or failed: a notifier that only fires on success
|
|
118
|
+
# goes quiet exactly when somebody needs it.
|
|
119
|
+
rule: all_done
|
|
120
|
+
config:
|
|
121
|
+
connection: echo-webhook
|
|
122
|
+
path: /post
|
|
123
|
+
body: ${steps.summary.output.value}
|
|
124
|
+
headers:
|
|
125
|
+
# Routing the receiver can dispatch on without parsing the body.
|
|
126
|
+
x-severity: ${steps.summary.output.value.severity}
|
|
127
|
+
# A receiver that queues rather than processes answers 202; both count as delivered.
|
|
128
|
+
success_status: [200, 202, 204]
|
|
129
|
+
|
|
130
|
+
delivered:
|
|
131
|
+
block: transform.jq
|
|
132
|
+
depends_on: [notify]
|
|
133
|
+
config:
|
|
134
|
+
input:
|
|
135
|
+
status: ${steps.notify.output.status}
|
|
136
|
+
signed: ${steps.notify.output.signed}
|
|
137
|
+
echoed: ${steps.notify.output.json_body}
|
|
138
|
+
program: |
|
|
139
|
+
{status, signed,
|
|
140
|
+
severity: .echoed.headers["x-severity"],
|
|
141
|
+
text: .echoed.json.text,
|
|
142
|
+
rejected_keys: .echoed.json.rejected_keys}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# S3 examples
|
|
2
|
+
|
|
3
|
+
Object storage without an S3 block anywhere: `s3://` is a registered URI scheme, so the
|
|
4
|
+
ordinary storage blocks address it the way they address anything else,
|
|
5
|
+
and every document here declares `requires: {storage: [s3]}` so an instance without a
|
|
6
|
+
backend refuses it before storing it.
|
|
7
|
+
|
|
8
|
+
All five name one connection, `artifacts`, and it is the code the
|
|
9
|
+
[compose stack](../../docs/operations.md) already bootstraps for its own artifact bucket. So
|
|
10
|
+
on the stack there is nothing to set up: apply and run, naming the stack's bucket.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
dg run s3-round-trip -p day=2026-01-01 -p bucket=dirigent --watch
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Under `dg dev` there is no S3 server and no connection, and
|
|
17
|
+
[s3-round-trip.yaml](s3-round-trip.yaml) documents both: the rustfs container to start, the
|
|
18
|
+
`artifacts` connection to create, and the `DIRIGENT_STORAGE_CONNECTIONS` line that makes it
|
|
19
|
+
serve the scheme.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
dg apply examples/s3/s3-round-trip.yaml
|
|
23
|
+
dg run s3-round-trip -p day=2026-01-01 --watch
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Pipelines
|
|
27
|
+
|
|
28
|
+
| File | What it teaches |
|
|
29
|
+
| --- | --- |
|
|
30
|
+
| [s3-round-trip.yaml](s3-round-trip.yaml) | The whole seam: an artifact out to a bucket and back, moved by the ordinary storage blocks. |
|
|
31
|
+
| [s3-copy-and-verify.yaml](s3-copy-and-verify.yaml) | Promote-then-consume: a copy between prefixes, and `storage.exists` standing between the copy and the reader. |
|
|
32
|
+
| [s3-csv-report.yaml](s3-csv-report.yaml) | A report delivered to a bucket: the csv-report chain with the converter's `target` one word away from scratch. |
|
|
33
|
+
| [report-to-s3.yaml](report-to-s3.yaml) | A rendered markdown page delivered to a bucket: `report.render` hands its text on and `storage.write` puts it in the object, content type and all. |
|
|
34
|
+
| [s3-parquet-report.yaml](s3-parquet-report.yaml) | A typed parquet dataset written straight to a bucket, for an analysis to open (needs `dirigent-parquet`). |
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# The same rendered page as examples/recipes/report-to-file.yaml, delivered to a bucket.
|
|
2
|
+
#
|
|
3
|
+
# NEEDS AN S3-COMPATIBLE ENDPOINT, set up exactly as s3-round-trip.yaml documents: the same
|
|
4
|
+
# rustfs container, the same artifacts connection, the same DIRIGENT_STORAGE_CONNECTIONS.
|
|
5
|
+
#
|
|
6
|
+
# Three hops, and what each one hands on:
|
|
7
|
+
# summary transform.jq, the numbers the page is about.
|
|
8
|
+
# page report.render, the markdown. Output: text, content_type, text_bytes.
|
|
9
|
+
# deliver storage.write, the text to an s3:// URI. Output: uri, bytes_written,
|
|
10
|
+
# content_type -- and on S3 the content type is stored with the object, so a
|
|
11
|
+
# browser opening the link is told what it is.
|
|
12
|
+
#
|
|
13
|
+
# THE RENDER DOES NOT KNOW WHERE THE PAGE GOES. It renders text and hands it on; only the
|
|
14
|
+
# step below it names a destination, and changing s3:// to file:// is the whole difference
|
|
15
|
+
# between this document and the recipe it was copied from. That is what schemes are for.
|
|
16
|
+
#
|
|
17
|
+
# dg apply examples/s3/report-to-s3.yaml
|
|
18
|
+
# dg run report-to-s3 -p day=2026-01-01 --watch
|
|
19
|
+
#
|
|
20
|
+
# The page is at s3://<bucket>/reports/<day>.md when the run settles.
|
|
21
|
+
|
|
22
|
+
format: dirigent/v1
|
|
23
|
+
kind: pipeline
|
|
24
|
+
code: report-to-s3
|
|
25
|
+
name: A rendered report delivered to a bucket
|
|
26
|
+
description: Render a markdown page from a Jinja template and write it to an object in S3.
|
|
27
|
+
|
|
28
|
+
tags: [s3, report, storage, transform]
|
|
29
|
+
|
|
30
|
+
requires:
|
|
31
|
+
blocks:
|
|
32
|
+
- transform.jq
|
|
33
|
+
- report.render
|
|
34
|
+
- storage.write
|
|
35
|
+
connections:
|
|
36
|
+
- artifacts
|
|
37
|
+
storage:
|
|
38
|
+
- s3
|
|
39
|
+
|
|
40
|
+
params:
|
|
41
|
+
type: object
|
|
42
|
+
required: [day]
|
|
43
|
+
additionalProperties: false
|
|
44
|
+
properties:
|
|
45
|
+
day:
|
|
46
|
+
type: string
|
|
47
|
+
format: date
|
|
48
|
+
description: The day the report is about, and the object it is named as.
|
|
49
|
+
bucket:
|
|
50
|
+
type: string
|
|
51
|
+
default: dirigent-archive
|
|
52
|
+
description: The bucket the page is delivered into.
|
|
53
|
+
|
|
54
|
+
steps:
|
|
55
|
+
summary:
|
|
56
|
+
block: transform.jq
|
|
57
|
+
config:
|
|
58
|
+
input:
|
|
59
|
+
day: ${params.day}
|
|
60
|
+
readings:
|
|
61
|
+
- {station: st-1, region: east, celsius: 4.5}
|
|
62
|
+
- {station: st-3, region: west, celsius: 1.2}
|
|
63
|
+
- {station: st-4, region: north, celsius: -3.4}
|
|
64
|
+
program: |
|
|
65
|
+
{day: .day,
|
|
66
|
+
stations: (.readings | length),
|
|
67
|
+
coldest: (.readings | min_by(.celsius) | .station),
|
|
68
|
+
rows: .readings}
|
|
69
|
+
|
|
70
|
+
page:
|
|
71
|
+
block: report.render
|
|
72
|
+
depends_on: [summary]
|
|
73
|
+
config:
|
|
74
|
+
values:
|
|
75
|
+
summary: ${steps.summary.output.value}
|
|
76
|
+
content_type: text/markdown
|
|
77
|
+
template: |
|
|
78
|
+
# Readings for {{ summary.day }}
|
|
79
|
+
|
|
80
|
+
{{ summary.stations }} stations reported; the coldest was {{ summary.coldest }}.
|
|
81
|
+
|
|
82
|
+
{% for row in summary.rows %}
|
|
83
|
+
- **{{ row.station }}** ({{ row.region }}): {{ row.celsius }}C
|
|
84
|
+
{% endfor %}
|
|
85
|
+
|
|
86
|
+
deliver:
|
|
87
|
+
block: storage.write
|
|
88
|
+
depends_on: [page]
|
|
89
|
+
config:
|
|
90
|
+
target: s3://${params.bucket}/reports/${params.day}.md
|
|
91
|
+
text: ${steps.page.output.text}
|
|
92
|
+
# Stored on the object, so whoever fetches it is told it is markdown.
|
|
93
|
+
content_type: ${steps.page.output.content_type}
|