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