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,116 @@
|
|
|
1
|
+
# Keep the rows a predicate answers true for, and nothing else about them changes.
|
|
2
|
+
#
|
|
3
|
+
# In: a list of stations with a status, a battery level, and a last-seen timestamp.
|
|
4
|
+
# Out: the subset that is reporting and healthy, elements untouched, in input order.
|
|
5
|
+
#
|
|
6
|
+
# filter.jq is select as a contract. The program answers one question about one element, and
|
|
7
|
+
# the frame appends the element it was given rather than anything the program returned -- so
|
|
8
|
+
# a filter cannot edit a row on the way past, however the program is written. Order is the
|
|
9
|
+
# input's, and the output is always a subset.
|
|
10
|
+
#
|
|
11
|
+
# The rule that catches everybody: jq's truthiness is not applied here. A program that
|
|
12
|
+
# answers 0, "" or null is a mistake surfaced rather than an element quietly dropped, and
|
|
13
|
+
# the step fails saying so:
|
|
14
|
+
#
|
|
15
|
+
# element 0: the program answered 4, and a filter answers true or false; jq's truthiness
|
|
16
|
+
# is not applied, so a program meaning 'has readings' writes '.count > 0'
|
|
17
|
+
#
|
|
18
|
+
# So a predicate is written as a comparison, always. The three below are the shapes most
|
|
19
|
+
# predicates take: a set membership with IN, a numeric threshold, and a presence check that
|
|
20
|
+
# distinguishes absent from null.
|
|
21
|
+
#
|
|
22
|
+
# The count step afterwards is the other half of filtering: a filter that reports nothing
|
|
23
|
+
# leaves "how many did it drop" to somebody reading two artifacts side by side.
|
|
24
|
+
#
|
|
25
|
+
# To change it: -p min_battery=90 tightens the threshold; -p statuses='["active","standby"]'
|
|
26
|
+
# widens the set the membership test reads.
|
|
27
|
+
#
|
|
28
|
+
# dg run --local examples/recipes/filter-by-predicate.yaml
|
|
29
|
+
# dg run --local examples/recipes/filter-by-predicate.yaml -p min_battery=90
|
|
30
|
+
|
|
31
|
+
format: dirigent/v1
|
|
32
|
+
kind: pipeline
|
|
33
|
+
code: filter-by-predicate
|
|
34
|
+
name: Filter with a predicate
|
|
35
|
+
description: Keep the elements a true-or-false program accepts, unmodified and in order, and report how many were dropped.
|
|
36
|
+
|
|
37
|
+
tags: [recipes, transform, filter]
|
|
38
|
+
|
|
39
|
+
requires:
|
|
40
|
+
blocks:
|
|
41
|
+
- value.const
|
|
42
|
+
- transform.jq
|
|
43
|
+
- filter.jq
|
|
44
|
+
|
|
45
|
+
params:
|
|
46
|
+
type: object
|
|
47
|
+
properties:
|
|
48
|
+
min_battery:
|
|
49
|
+
type: integer
|
|
50
|
+
minimum: 0
|
|
51
|
+
default: 20
|
|
52
|
+
description: The battery percentage below which a station is not considered healthy.
|
|
53
|
+
statuses:
|
|
54
|
+
type: array
|
|
55
|
+
default: [active]
|
|
56
|
+
items:
|
|
57
|
+
type: string
|
|
58
|
+
description: The statuses that count as reporting.
|
|
59
|
+
|
|
60
|
+
steps:
|
|
61
|
+
stations:
|
|
62
|
+
block: value.const
|
|
63
|
+
config:
|
|
64
|
+
value:
|
|
65
|
+
- { id: st-1, status: active, battery: 96, last_seen: "2026-01-01T06:00:00Z" }
|
|
66
|
+
- { id: st-2, status: retired, battery: 100, last_seen: "2025-11-02T06:00:00Z" }
|
|
67
|
+
- { id: st-3, status: active, battery: 11, last_seen: "2026-01-01T05:00:00Z" }
|
|
68
|
+
- { id: st-4, status: standby, battery: 80, last_seen: "2026-01-01T04:00:00Z" }
|
|
69
|
+
# No battery reading at all: absent is not the same as flat.
|
|
70
|
+
- { id: st-5, status: active, last_seen: "2026-01-01T06:00:00Z" }
|
|
71
|
+
|
|
72
|
+
attach:
|
|
73
|
+
block: transform.jq
|
|
74
|
+
depends_on: [stations]
|
|
75
|
+
config:
|
|
76
|
+
# The predicate's thresholds are data, so they travel with each element; a filter
|
|
77
|
+
# program sees one element and nothing else.
|
|
78
|
+
input:
|
|
79
|
+
rows: ${steps.stations.output.value}
|
|
80
|
+
min_battery: ${params.min_battery}
|
|
81
|
+
statuses: ${params.statuses}
|
|
82
|
+
program: |
|
|
83
|
+
. as {$rows, $min_battery, $statuses}
|
|
84
|
+
| [$rows[] | . + {_min_battery: $min_battery, _statuses: $statuses}]
|
|
85
|
+
|
|
86
|
+
healthy:
|
|
87
|
+
block: filter.jq
|
|
88
|
+
depends_on: [attach]
|
|
89
|
+
config:
|
|
90
|
+
input: ${steps.attach.output.value}
|
|
91
|
+
# Every clause is a comparison: IN answers a boolean, the threshold answers a boolean,
|
|
92
|
+
# and the presence check answers a boolean. Nothing here relies on truthiness.
|
|
93
|
+
#
|
|
94
|
+
# The element is named first because IN evaluates its argument against whatever was
|
|
95
|
+
# piped into it -- a bare .status | IN(._statuses[]) would look for _statuses on the
|
|
96
|
+
# status string and fail the element.
|
|
97
|
+
program: |
|
|
98
|
+
. as $row
|
|
99
|
+
| ($row.status | IN($row._statuses[]))
|
|
100
|
+
and ($row.battery != null)
|
|
101
|
+
and ($row.battery >= $row._min_battery)
|
|
102
|
+
|
|
103
|
+
report:
|
|
104
|
+
block: transform.jq
|
|
105
|
+
depends_on: [attach, healthy]
|
|
106
|
+
config:
|
|
107
|
+
input:
|
|
108
|
+
seen: ${steps.attach.output.value}
|
|
109
|
+
kept: ${steps.healthy.output.value}
|
|
110
|
+
program: |
|
|
111
|
+
{seen: (.seen | length),
|
|
112
|
+
kept: (.kept | length),
|
|
113
|
+
dropped: [(.seen | map(.id)) - (.kept | map(.id)) | .[]],
|
|
114
|
+
# The bookkeeping fields the predicate needed are removed here rather than in the
|
|
115
|
+
# filter, which is not allowed to modify what it keeps.
|
|
116
|
+
rows: [.kept[] | del(._min_battery, ._statuses)]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# The three verbs in the order a pipeline usually needs them: keep, convert, summarise.
|
|
2
|
+
#
|
|
3
|
+
# In: raw meter readings, some of them from a decommissioned meter or flagged suspect.
|
|
4
|
+
# Out: {kept, converted, summary} -- and a summary computed by reduce over the converted
|
|
5
|
+
# rows.
|
|
6
|
+
#
|
|
7
|
+
# Each step is the verb whose promise matches what it is doing, and the promise is what the
|
|
8
|
+
# document tells a reader without their having to read the program:
|
|
9
|
+
#
|
|
10
|
+
# filter.jq the output is a subset of the input and the elements are untouched. A
|
|
11
|
+
# reader knows no value was edited here without reading the predicate.
|
|
12
|
+
# map.jq the output has one element per input element, in order. A reader knows the
|
|
13
|
+
# batch did not change size here without reading the program.
|
|
14
|
+
# transform.jq anything else. Summarising changes the shape from a list to an object, so
|
|
15
|
+
# it is the only one of the three that can do it.
|
|
16
|
+
#
|
|
17
|
+
# Written as one transform.jq the result would be identical and the document would say
|
|
18
|
+
# nothing. The cost of the split is two more steps; the payoff is that each step's failure
|
|
19
|
+
# names the element it happened on, and that a run's step list reads as what was done.
|
|
20
|
+
#
|
|
21
|
+
# reduce is the summariser here rather than three passes of add, because the accumulator
|
|
22
|
+
# needs to see each row once to keep a count, a total and a maximum together. add over three
|
|
23
|
+
# mapped lists is fine and reads well; reduce is what to reach for once the accumulator
|
|
24
|
+
# holds more than one number.
|
|
25
|
+
#
|
|
26
|
+
# To change it: -p suspect_is_kept=true keeps the flagged rows, which moves both the count
|
|
27
|
+
# and the mean and is the fastest way to see how much a quality rule is worth.
|
|
28
|
+
#
|
|
29
|
+
# dg run --local examples/recipes/filter-then-map-then-reduce.yaml
|
|
30
|
+
# dg run --local examples/recipes/filter-then-map-then-reduce.yaml -p suspect_is_kept=true
|
|
31
|
+
|
|
32
|
+
format: dirigent/v1
|
|
33
|
+
kind: pipeline
|
|
34
|
+
code: filter-then-map-then-reduce
|
|
35
|
+
name: Filter, then map, then reduce
|
|
36
|
+
description: Keep the usable readings, convert each one, and fold the result into a summary with reduce.
|
|
37
|
+
|
|
38
|
+
tags: [recipes, transform, filter, map]
|
|
39
|
+
|
|
40
|
+
requires:
|
|
41
|
+
blocks:
|
|
42
|
+
- value.const
|
|
43
|
+
- transform.jq
|
|
44
|
+
- map.jq
|
|
45
|
+
- filter.jq
|
|
46
|
+
|
|
47
|
+
params:
|
|
48
|
+
type: object
|
|
49
|
+
properties:
|
|
50
|
+
suspect_is_kept:
|
|
51
|
+
type: boolean
|
|
52
|
+
default: false
|
|
53
|
+
description: Keep the readings flagged suspect instead of dropping them.
|
|
54
|
+
|
|
55
|
+
steps:
|
|
56
|
+
readings:
|
|
57
|
+
block: value.const
|
|
58
|
+
config:
|
|
59
|
+
value:
|
|
60
|
+
- { meter: m-1, at: "2026-01-01T06:00:00Z", kwh: 12.5, state: live, suspect: false }
|
|
61
|
+
- { meter: m-2, at: "2026-01-01T06:00:00Z", kwh: 0.0, state: decommissioned, suspect: false }
|
|
62
|
+
- { meter: m-3, at: "2026-01-01T06:00:00Z", kwh: 9.25, state: live, suspect: true }
|
|
63
|
+
- { meter: m-4, at: "2026-01-01T06:00:00Z", kwh: 31.0, state: live, suspect: false }
|
|
64
|
+
- { meter: m-5, at: "2026-01-01T06:00:00Z", kwh: 4.75, state: live, suspect: false }
|
|
65
|
+
|
|
66
|
+
attach:
|
|
67
|
+
block: transform.jq
|
|
68
|
+
depends_on: [readings]
|
|
69
|
+
config:
|
|
70
|
+
input:
|
|
71
|
+
rows: ${steps.readings.output.value}
|
|
72
|
+
keep_suspect: ${params.suspect_is_kept}
|
|
73
|
+
program: |
|
|
74
|
+
. as {$rows, $keep_suspect}
|
|
75
|
+
| [$rows[] | . + {_keep_suspect: $keep_suspect}]
|
|
76
|
+
|
|
77
|
+
kept:
|
|
78
|
+
block: filter.jq
|
|
79
|
+
depends_on: [attach]
|
|
80
|
+
config:
|
|
81
|
+
input: ${steps.attach.output.value}
|
|
82
|
+
program: |
|
|
83
|
+
.state == "live" and (._keep_suspect or (.suspect | not))
|
|
84
|
+
|
|
85
|
+
converted:
|
|
86
|
+
block: map.jq
|
|
87
|
+
depends_on: [kept]
|
|
88
|
+
config:
|
|
89
|
+
input: ${steps.kept.output.value}
|
|
90
|
+
# A megajoule is 3.6 kWh, so this is the unit conversion a sink asked for; the map is
|
|
91
|
+
# where it belongs because it touches one row at a time and changes no row count.
|
|
92
|
+
program: |
|
|
93
|
+
{meter, at, megajoules: (.kwh * 3.6 | . * 100 | round / 100)}
|
|
94
|
+
|
|
95
|
+
summary:
|
|
96
|
+
block: transform.jq
|
|
97
|
+
depends_on: [converted]
|
|
98
|
+
config:
|
|
99
|
+
input: ${steps.converted.output.value}
|
|
100
|
+
program: |
|
|
101
|
+
. as $rows
|
|
102
|
+
| reduce $rows[] as $row
|
|
103
|
+
({meters: 0, megajoules: 0, peak: null};
|
|
104
|
+
{meters: (.meters + 1),
|
|
105
|
+
megajoules: (.megajoules + $row.megajoules),
|
|
106
|
+
peak: (if .peak == null or $row.megajoules > .peak.megajoules
|
|
107
|
+
then $row else .peak end)})
|
|
108
|
+
| . + {mean_megajoules: (if .meters == 0 then null
|
|
109
|
+
else (.megajoules / .meters * 100 | round / 100) end)}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Fetch, check what came back, reshape it, check what you made, and post it on.
|
|
2
|
+
#
|
|
3
|
+
# This is the shape most real pipelines have, and the reason it is written out in full here is
|
|
4
|
+
# the second gate. One gate, at the fetch, is the common half-measure: it catches the day the
|
|
5
|
+
# source changes and catches nothing else. The second gate catches the other failure, which is
|
|
6
|
+
# the pipeline's own -- a jq program edited under time pressure, a field renamed in one place
|
|
7
|
+
# and not the other -- and it catches it before the bad row leaves the building.
|
|
8
|
+
#
|
|
9
|
+
# In: a station and a day, as parameters.
|
|
10
|
+
# Out: one checked row, posted to a public echo service, and a receipt saying what it saw.
|
|
11
|
+
#
|
|
12
|
+
# Five hops, and what each one hands on:
|
|
13
|
+
#
|
|
14
|
+
# fetch http.request. A GET whose query carries the two parameters, answered by Postman
|
|
15
|
+
# Echo, a public request-and-response service, so this runs against something real
|
|
16
|
+
# with nothing to stand up first. Output: status, headers, body, body_bytes.
|
|
17
|
+
# checked validate.schema against `echo-reading`: the source's shape, as this document
|
|
18
|
+
# understands it. The block passes the value through unchanged when it holds, so
|
|
19
|
+
# the next step reads the gate's output rather than reaching back past it -- which
|
|
20
|
+
# is what makes the gate impossible to skip by accident.
|
|
21
|
+
# reading transform.jq. The reshaping: the query the echo service reflected back becomes
|
|
22
|
+
# the row a receiver wants, and the temperature becomes a number, because a query
|
|
23
|
+
# string carries text and a downstream average cannot add text.
|
|
24
|
+
# verified validate.schema again, against `station-reading` this time: the shape this
|
|
25
|
+
# document promises, with additionalProperties false so a field the program leaks
|
|
26
|
+
# is refused rather than shipped.
|
|
27
|
+
# publish http.request POST. The body is the value the gate passed, serialised by the
|
|
28
|
+
# block: nothing here builds JSON out of strings.
|
|
29
|
+
#
|
|
30
|
+
# BOTH SCHEMAS ARE NAMED, NOT CARRIED. `schema:` takes a code and never a shape, and a document
|
|
31
|
+
# that carries its own top-level `schemas:` section is refused by a server, so the two live in
|
|
32
|
+
# examples/schemas/ and `requires.schemas` below says the instance must hold them. That is the
|
|
33
|
+
# preflight: an apply against an instance missing either one is refused up front, naming what
|
|
34
|
+
# is missing, instead of failing on the first run.
|
|
35
|
+
#
|
|
36
|
+
# dg schema create examples/schemas/echo-reading.json
|
|
37
|
+
# dg schema create examples/schemas/station-reading.json
|
|
38
|
+
# dg apply examples/recipes/http-fetch-validate-post.yaml
|
|
39
|
+
# dg run http-fetch-validate-post --watch
|
|
40
|
+
#
|
|
41
|
+
# Locally there is no instance holding them, so hand them over on the command line:
|
|
42
|
+
#
|
|
43
|
+
# dg run --local examples/recipes/http-fetch-validate-post.yaml \
|
|
44
|
+
# --schema examples/schemas/echo-reading.json \
|
|
45
|
+
# --schema examples/schemas/station-reading.json
|
|
46
|
+
#
|
|
47
|
+
# TO MAKE IT YOURS: point `fetch` at your source, rewrite the two schemas for its shape and
|
|
48
|
+
# yours, and point `publish` at your receiver. The five hops do not move; a real pipeline is
|
|
49
|
+
# this with a longer jq program and a connection holding the credential.
|
|
50
|
+
|
|
51
|
+
format: dirigent/v1
|
|
52
|
+
kind: pipeline
|
|
53
|
+
code: http-fetch-validate-post
|
|
54
|
+
name: Fetch, validate, transform, validate, post
|
|
55
|
+
description: |
|
|
56
|
+
The full shape: a GET from a public endpoint, a schema gate on what came back, a jq
|
|
57
|
+
reshaping, a second gate on what was made, and a POST of the result.
|
|
58
|
+
|
|
59
|
+
The second gate is the point. The first one catches the source changing; the second catches
|
|
60
|
+
this document's own program going wrong, before the bad row is delivered.
|
|
61
|
+
|
|
62
|
+
tags: [recipes, http, transform, validate, starter]
|
|
63
|
+
|
|
64
|
+
requires:
|
|
65
|
+
blocks:
|
|
66
|
+
- http.request
|
|
67
|
+
- transform.jq
|
|
68
|
+
- validate.schema
|
|
69
|
+
# The preflight that refuses this document on an instance holding neither shape.
|
|
70
|
+
schemas:
|
|
71
|
+
- echo-reading
|
|
72
|
+
- station-reading
|
|
73
|
+
|
|
74
|
+
params:
|
|
75
|
+
type: object
|
|
76
|
+
properties:
|
|
77
|
+
station:
|
|
78
|
+
type: string
|
|
79
|
+
minLength: 1
|
|
80
|
+
default: st-1
|
|
81
|
+
description: The station the reading is attributed to.
|
|
82
|
+
day:
|
|
83
|
+
type: string
|
|
84
|
+
format: date
|
|
85
|
+
default: "2026-01-01"
|
|
86
|
+
description: The day the reading reports for.
|
|
87
|
+
celsius:
|
|
88
|
+
type: string
|
|
89
|
+
default: "12.5"
|
|
90
|
+
description: The temperature, as the text a query string carries; the transform makes it a number.
|
|
91
|
+
|
|
92
|
+
steps:
|
|
93
|
+
fetch:
|
|
94
|
+
block: http.request
|
|
95
|
+
config:
|
|
96
|
+
url: https://postman-echo.com/get
|
|
97
|
+
method: GET
|
|
98
|
+
query:
|
|
99
|
+
station: ${params.station}
|
|
100
|
+
day: ${params.day}
|
|
101
|
+
celsius: ${params.celsius}
|
|
102
|
+
timeout: 30s
|
|
103
|
+
|
|
104
|
+
checked:
|
|
105
|
+
block: validate.schema
|
|
106
|
+
depends_on: [fetch]
|
|
107
|
+
config:
|
|
108
|
+
input: ${steps.fetch.output.body}
|
|
109
|
+
# A code naming a schema the instance holds, never a shape written here.
|
|
110
|
+
schema: echo-reading
|
|
111
|
+
|
|
112
|
+
reading:
|
|
113
|
+
block: transform.jq
|
|
114
|
+
depends_on: [checked]
|
|
115
|
+
config:
|
|
116
|
+
input: ${steps.checked.output.value}
|
|
117
|
+
program: |
|
|
118
|
+
.args
|
|
119
|
+
| {station: .station,
|
|
120
|
+
day: .day,
|
|
121
|
+
# tonumber, because a query string carried this as text and a receiver that
|
|
122
|
+
# averages it cannot add text.
|
|
123
|
+
celsius: (.celsius | tonumber)}
|
|
124
|
+
|
|
125
|
+
verified:
|
|
126
|
+
block: validate.schema
|
|
127
|
+
depends_on: [reading]
|
|
128
|
+
config:
|
|
129
|
+
input: ${steps.reading.output.value}
|
|
130
|
+
schema: station-reading
|
|
131
|
+
|
|
132
|
+
publish:
|
|
133
|
+
block: http.request
|
|
134
|
+
depends_on: [verified]
|
|
135
|
+
config:
|
|
136
|
+
url: https://postman-echo.com/post
|
|
137
|
+
method: POST
|
|
138
|
+
# The value the gate passed through, serialised by the block.
|
|
139
|
+
body: ${steps.verified.output.value}
|
|
140
|
+
headers:
|
|
141
|
+
x-reading-day: ${params.day}
|
|
142
|
+
timeout: 30s
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Redirects are not followed unless a step says so, and both answers are useful.
|
|
2
|
+
#
|
|
3
|
+
# In: a redirect status and a target, as parameters.
|
|
4
|
+
# Out: the same call made twice -- once returning the 3xx itself, once following it to the
|
|
5
|
+
# end -- so the difference is one output rather than an argument.
|
|
6
|
+
#
|
|
7
|
+
# follow_redirects defaults to false, and the default is the interesting choice. A redirect
|
|
8
|
+
# is information: it says the resource moved, or that a load balancer wants a different
|
|
9
|
+
# host, or that an unauthenticated call is being sent to a login page. Following it
|
|
10
|
+
# silently turns "your URL is wrong" into a 200 that a pipeline treats as data, and the
|
|
11
|
+
# classic version of that is a 302 to an HTML sign-in page parsed as an empty result.
|
|
12
|
+
#
|
|
13
|
+
# So the block returns the 3xx as the answer, with its Location header, and a step that
|
|
14
|
+
# genuinely wants the destination asks for it:
|
|
15
|
+
#
|
|
16
|
+
# follow_redirects: false status is 302 and the body is the redirect's own. Location
|
|
17
|
+
# says where it points, which is a thing a pipeline can report
|
|
18
|
+
# or refuse.
|
|
19
|
+
# follow_redirects: true the client follows, and status is the destination's. Nothing
|
|
20
|
+
# says a redirect happened, so this is the setting for an API
|
|
21
|
+
# that is known to redirect as part of its normal working.
|
|
22
|
+
#
|
|
23
|
+
# A 3xx is not a 2xx, so the un-followed call needs the status listed as success or the
|
|
24
|
+
# step fails -- which is success_status doing exactly what http-success-status-list.yaml
|
|
25
|
+
# describes, on the one status that most deserves it.
|
|
26
|
+
#
|
|
27
|
+
# To change it: -p status=301 makes it a permanent move, which is the one worth reporting
|
|
28
|
+
# rather than following, since the URL in the document is now wrong.
|
|
29
|
+
#
|
|
30
|
+
# dg run --local examples/recipes/http-follow-redirects.yaml
|
|
31
|
+
# dg run --local examples/recipes/http-follow-redirects.yaml -p status=301
|
|
32
|
+
|
|
33
|
+
format: dirigent/v1
|
|
34
|
+
kind: pipeline
|
|
35
|
+
code: http-follow-redirects
|
|
36
|
+
name: Following a redirect, or not
|
|
37
|
+
description: Make the same redirecting call twice, with and without follow_redirects, and compare the status and the Location header.
|
|
38
|
+
|
|
39
|
+
tags: [recipes, http, transform]
|
|
40
|
+
|
|
41
|
+
requires:
|
|
42
|
+
blocks:
|
|
43
|
+
- http.request
|
|
44
|
+
- transform.jq
|
|
45
|
+
|
|
46
|
+
params:
|
|
47
|
+
type: object
|
|
48
|
+
properties:
|
|
49
|
+
status:
|
|
50
|
+
type: integer
|
|
51
|
+
default: 302
|
|
52
|
+
description: The redirect status the endpoint answers with.
|
|
53
|
+
target:
|
|
54
|
+
type: string
|
|
55
|
+
default: https://postman-echo.com/get
|
|
56
|
+
description: Where the redirect points.
|
|
57
|
+
|
|
58
|
+
steps:
|
|
59
|
+
unfollowed:
|
|
60
|
+
block: http.request
|
|
61
|
+
config:
|
|
62
|
+
url: https://postman-echo.com/redirect-to
|
|
63
|
+
method: GET
|
|
64
|
+
query:
|
|
65
|
+
url: ${params.target}
|
|
66
|
+
status_code: ${params.status}
|
|
67
|
+
# The default, written out because this document is about it.
|
|
68
|
+
follow_redirects: false
|
|
69
|
+
# A 3xx is not a 2xx, so the status the call is asking for has to be listed.
|
|
70
|
+
success_status: [301, 302, 307, 308]
|
|
71
|
+
timeout: 20s
|
|
72
|
+
|
|
73
|
+
followed:
|
|
74
|
+
block: http.request
|
|
75
|
+
config:
|
|
76
|
+
url: https://postman-echo.com/redirect-to
|
|
77
|
+
method: GET
|
|
78
|
+
query:
|
|
79
|
+
url: ${params.target}
|
|
80
|
+
status_code: ${params.status}
|
|
81
|
+
follow_redirects: true
|
|
82
|
+
timeout: 20s
|
|
83
|
+
|
|
84
|
+
compare:
|
|
85
|
+
block: transform.jq
|
|
86
|
+
depends_on: [unfollowed, followed]
|
|
87
|
+
config:
|
|
88
|
+
input:
|
|
89
|
+
unfollowed_status: ${steps.unfollowed.output.status}
|
|
90
|
+
unfollowed_headers: ${steps.unfollowed.output.headers}
|
|
91
|
+
followed_status: ${steps.followed.output.status}
|
|
92
|
+
followed_body: ${steps.followed.output.body}
|
|
93
|
+
program: |
|
|
94
|
+
{unfollowed: {status: .unfollowed_status,
|
|
95
|
+
# Where it pointed: the thing a pipeline can report or refuse.
|
|
96
|
+
location: .unfollowed_headers.location},
|
|
97
|
+
followed: {status: .followed_status,
|
|
98
|
+
# The destination answered, and nothing in the response says a redirect
|
|
99
|
+
# happened on the way.
|
|
100
|
+
url: .followed_body.url}}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# A GET with query parameters, and what to do with the answer.
|
|
2
|
+
#
|
|
3
|
+
# In: a station code and a day, as parameters.
|
|
4
|
+
# Out: the echo service's view of the request -- the query it received -- reshaped into a
|
|
5
|
+
# small record, plus the status and how long the call took.
|
|
6
|
+
#
|
|
7
|
+
# https://postman-echo.com/get answers with the request it was given, so a recipe about
|
|
8
|
+
# building a request can show what actually arrived on the wire without standing anything
|
|
9
|
+
# up. Against a real API the only lines that change are url or connection, and the path.
|
|
10
|
+
#
|
|
11
|
+
# The query is a map, not a string, and that is the whole point of the field: the block
|
|
12
|
+
# encodes each value once, correctly, so a station code with a space or an ampersand in it
|
|
13
|
+
# cannot break out of its parameter. A path with ?a=1&b=2 hand-written into it is where
|
|
14
|
+
# injection and double-encoding both come from.
|
|
15
|
+
#
|
|
16
|
+
# Values may be strings, numbers or booleans -- paging=false goes over the wire as the text
|
|
17
|
+
# `false`, because that is what a query string is -- and a reference resolves before the
|
|
18
|
+
# encoding happens, so ${params.day} is a value and never a fragment of URL.
|
|
19
|
+
#
|
|
20
|
+
# The answer is one field. body carries the parsed document when the service answered JSON
|
|
21
|
+
# and the text when it answered anything else, and body_bytes says how big it was.
|
|
22
|
+
# max_response bounds what the step will hold: a body past it is refused rather than read
|
|
23
|
+
# half way. A body worth keeping is handed to a storage.write step, which is what
|
|
24
|
+
# http-save-body-to-storage.yaml does with this same call.
|
|
25
|
+
#
|
|
26
|
+
# duration_ms is on every response, so a pipeline can report what a call cost without
|
|
27
|
+
# timing it itself.
|
|
28
|
+
#
|
|
29
|
+
# To change it: -p station=st-9, or point url at your own endpoint and keep the rest.
|
|
30
|
+
#
|
|
31
|
+
# dg run --local examples/recipes/http-get-with-query.yaml
|
|
32
|
+
# dg run --local examples/recipes/http-get-with-query.yaml -p station=st-9 -p day=2026-02-01
|
|
33
|
+
|
|
34
|
+
format: dirigent/v1
|
|
35
|
+
kind: pipeline
|
|
36
|
+
code: http-get-with-query
|
|
37
|
+
name: A GET with query parameters
|
|
38
|
+
description: Build a GET whose query is a map rather than a hand-written string, and reshape the JSON body it answers with.
|
|
39
|
+
|
|
40
|
+
tags: [recipes, http, transform]
|
|
41
|
+
|
|
42
|
+
requires:
|
|
43
|
+
blocks:
|
|
44
|
+
- http.request
|
|
45
|
+
- transform.jq
|
|
46
|
+
|
|
47
|
+
params:
|
|
48
|
+
type: object
|
|
49
|
+
properties:
|
|
50
|
+
station:
|
|
51
|
+
type: string
|
|
52
|
+
default: st-1
|
|
53
|
+
description: The station the request asks about.
|
|
54
|
+
day:
|
|
55
|
+
type: string
|
|
56
|
+
format: date
|
|
57
|
+
default: "2026-01-01"
|
|
58
|
+
description: The day the request asks about.
|
|
59
|
+
page_size:
|
|
60
|
+
type: integer
|
|
61
|
+
minimum: 1
|
|
62
|
+
default: 50
|
|
63
|
+
description: A numeric query value, sent as its digits.
|
|
64
|
+
|
|
65
|
+
steps:
|
|
66
|
+
fetch:
|
|
67
|
+
block: http.request
|
|
68
|
+
config:
|
|
69
|
+
# An absolute url, for the case where no connection is configured. A pipeline that
|
|
70
|
+
# runs against an instance names a connection instead, so moving from staging to
|
|
71
|
+
# production edits the connection and no document.
|
|
72
|
+
url: https://postman-echo.com/get
|
|
73
|
+
method: GET
|
|
74
|
+
query:
|
|
75
|
+
station: ${params.station}
|
|
76
|
+
day: ${params.day}
|
|
77
|
+
page_size: ${params.page_size}
|
|
78
|
+
# A boolean goes on the wire as the word, because a query string has no types.
|
|
79
|
+
paging: false
|
|
80
|
+
timeout: 20s
|
|
81
|
+
|
|
82
|
+
received:
|
|
83
|
+
block: transform.jq
|
|
84
|
+
depends_on: [fetch]
|
|
85
|
+
config:
|
|
86
|
+
input:
|
|
87
|
+
status: ${steps.fetch.output.status}
|
|
88
|
+
duration_ms: ${steps.fetch.output.duration_ms}
|
|
89
|
+
# The parsed body. The echo service puts what it received under .args.
|
|
90
|
+
body: ${steps.fetch.output.body}
|
|
91
|
+
program: |
|
|
92
|
+
{status, duration_ms,
|
|
93
|
+
url: .body.url,
|
|
94
|
+
# Every query value arrives back as text, because that is what a query string is.
|
|
95
|
+
query: .body.args,
|
|
96
|
+
page_size_is_text: (.body.args.page_size | type)}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Credentials live on a connection, not in the document. Here is what that looks like.
|
|
2
|
+
#
|
|
3
|
+
# In: nothing but parameters.
|
|
4
|
+
# Out: {authenticated: true} from an endpoint that refuses an unauthenticated caller with a
|
|
5
|
+
# 401, proving the connection's credentials were applied.
|
|
6
|
+
#
|
|
7
|
+
# A document is portable precisely because it names a credential rather than carrying one.
|
|
8
|
+
# On a server the connection is created once and the document says only its code:
|
|
9
|
+
#
|
|
10
|
+
# dg connection create http postman-echo \
|
|
11
|
+
# --set base_url=https://postman-echo.com \
|
|
12
|
+
# --set basic_username=postman --set basic_password=password
|
|
13
|
+
#
|
|
14
|
+
# basic_password is a SecretStr: sealed on the way in, encrypted at rest, and redacted in
|
|
15
|
+
# every API response -- an applied connection reports which fields are set, never what they
|
|
16
|
+
# are set to.
|
|
17
|
+
#
|
|
18
|
+
# This document carries its connection inline instead, which is the exception rather than
|
|
19
|
+
# the pattern, and it is labelled below with why: the credentials are Postman's own public
|
|
20
|
+
# demo pair, so the recipe runs standalone with nothing to hand it. Real credentials never
|
|
21
|
+
# go in a file that lives in git. The instance will not store a document that carries a
|
|
22
|
+
# connection, either, which is the format making that hard to get wrong.
|
|
23
|
+
#
|
|
24
|
+
# What the connection contributes, once named:
|
|
25
|
+
#
|
|
26
|
+
# base_url the step gives a path, and the connection decides which host it is
|
|
27
|
+
# resolved against. Staging to production is a connection edit and no
|
|
28
|
+
# document change.
|
|
29
|
+
# basic_username the Authorization header, built by the block. Nothing in the document
|
|
30
|
+
# basic_password ever spells out `Basic ...`, and no secret reaches a header map.
|
|
31
|
+
# timeout the default deadline for every call on this connection, which a step
|
|
32
|
+
# may override for one call.
|
|
33
|
+
#
|
|
34
|
+
# headers is for what the receiver wants that is not a credential: a routing key, an API
|
|
35
|
+
# version, a correlation id. A token in there is a token in the document, which is the
|
|
36
|
+
# mistake this whole surface exists to prevent.
|
|
37
|
+
#
|
|
38
|
+
# To change it: -p path=/get calls an endpoint that takes no credentials at all and echoes
|
|
39
|
+
# the headers it saw, including the Authorization the connection added.
|
|
40
|
+
#
|
|
41
|
+
# dg run --local examples/recipes/http-headers-and-auth-connection.yaml
|
|
42
|
+
# dg run --local examples/recipes/http-headers-and-auth-connection.yaml -p path=/get
|
|
43
|
+
|
|
44
|
+
format: dirigent/v1
|
|
45
|
+
kind: pipeline
|
|
46
|
+
code: http-headers-and-auth-connection
|
|
47
|
+
name: Auth on a connection, headers in the step
|
|
48
|
+
description: Call a basic-auth endpoint through a connection that holds the credentials, with the step contributing only non-secret headers.
|
|
49
|
+
|
|
50
|
+
tags: [recipes, http, transform]
|
|
51
|
+
|
|
52
|
+
requires:
|
|
53
|
+
blocks:
|
|
54
|
+
- http.request
|
|
55
|
+
- transform.jq
|
|
56
|
+
|
|
57
|
+
# Carried inline so this recipe runs standalone. The pair is Postman's own public demo
|
|
58
|
+
# credential, documented at https://postman-echo.com; a real one is created on the instance
|
|
59
|
+
# and named by code, and an instance refuses to store a document that carries a connection
|
|
60
|
+
# at all.
|
|
61
|
+
connections:
|
|
62
|
+
echo-basic:
|
|
63
|
+
kind: http
|
|
64
|
+
config:
|
|
65
|
+
base_url: https://postman-echo.com
|
|
66
|
+
basic_username: postman
|
|
67
|
+
basic_password: password
|
|
68
|
+
timeout: 30s
|
|
69
|
+
# What dg connection check calls to answer green or red.
|
|
70
|
+
health_path: /get
|
|
71
|
+
|
|
72
|
+
params:
|
|
73
|
+
type: object
|
|
74
|
+
properties:
|
|
75
|
+
path:
|
|
76
|
+
type: string
|
|
77
|
+
default: /basic-auth
|
|
78
|
+
description: The path resolved against the connection's base URL.
|
|
79
|
+
correlation_id:
|
|
80
|
+
type: string
|
|
81
|
+
default: recipes-0001
|
|
82
|
+
description: A non-secret header the receiver wants, sent by the step.
|
|
83
|
+
|
|
84
|
+
steps:
|
|
85
|
+
call:
|
|
86
|
+
block: http.request
|
|
87
|
+
config:
|
|
88
|
+
# A code, resolved by the instance. The host, the credentials and the default timeout
|
|
89
|
+
# all come from it.
|
|
90
|
+
connection: echo-basic
|
|
91
|
+
path: ${params.path}
|
|
92
|
+
method: GET
|
|
93
|
+
headers:
|
|
94
|
+
# Not a credential: a routing and tracing header, which is what this map is for.
|
|
95
|
+
x-correlation-id: ${params.correlation_id}
|
|
96
|
+
accept: application/json
|
|
97
|
+
|
|
98
|
+
result:
|
|
99
|
+
block: transform.jq
|
|
100
|
+
depends_on: [call]
|
|
101
|
+
config:
|
|
102
|
+
input:
|
|
103
|
+
status: ${steps.call.output.status}
|
|
104
|
+
body: ${steps.call.output.body}
|
|
105
|
+
program: |
|
|
106
|
+
# /basic-auth answers {"authenticated": true}; /get echoes the request instead, and
|
|
107
|
+
# its .headers shows the Authorization header the connection contributed.
|
|
108
|
+
{status,
|
|
109
|
+
authenticated: (.body.authenticated // null),
|
|
110
|
+
saw_authorization: ((.body.headers.authorization // null) != null)}
|