dirigent-examples 0.15.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. dirigent_examples/__init__.py +22 -0
  2. dirigent_examples/py.typed +0 -0
  3. dirigent_examples/shelves/README.md +299 -0
  4. dirigent_examples/shelves/composition/README.md +18 -0
  5. dirigent_examples/shelves/composition/chained-instances.yaml +97 -0
  6. dirigent_examples/shelves/composition/composition-child.yaml +64 -0
  7. dirigent_examples/shelves/composition/composition-parent.yaml +89 -0
  8. dirigent_examples/shelves/connections.yaml +52 -0
  9. dirigent_examples/shelves/demo/README.md +19 -0
  10. dirigent_examples/shelves/demo/markdown-showcase.yaml +117 -0
  11. dirigent_examples/shelves/demo/params-showcase.yaml +92 -0
  12. dirigent_examples/shelves/demo/requires.yaml +65 -0
  13. dirigent_examples/shelves/demo/weekly-import-malawi.yaml +54 -0
  14. dirigent_examples/shelves/demo/weekly-import-nepal.yaml +75 -0
  15. dirigent_examples/shelves/docker/README.md +29 -0
  16. dirigent_examples/shelves/docker/docker-build-push.yaml +110 -0
  17. dirigent_examples/shelves/docker/docker-build-run.yaml +106 -0
  18. dirigent_examples/shelves/docker/docker-compose-database.yaml +124 -0
  19. dirigent_examples/shelves/docker/docker-compose-failing-up.yaml +83 -0
  20. dirigent_examples/shelves/docker/docker-compose-file.yaml +117 -0
  21. dirigent_examples/shelves/docker/docker-compose-profiles-env.yaml +133 -0
  22. dirigent_examples/shelves/docker/docker-compose-stack.yaml +70 -0
  23. dirigent_examples/shelves/docker/docker-hello.yaml +53 -0
  24. dirigent_examples/shelves/docker/docker-remote-daemon.yaml +92 -0
  25. dirigent_examples/shelves/docker/docker-run-failing-teardown.yaml +94 -0
  26. dirigent_examples/shelves/docker/docker-ticker.yaml +49 -0
  27. dirigent_examples/shelves/execute/README.md +16 -0
  28. dirigent_examples/shelves/execute/long-log.yaml +89 -0
  29. dirigent_examples/shelves/failure/README.md +20 -0
  30. dirigent_examples/shelves/failure/error-handler.yaml +89 -0
  31. dirigent_examples/shelves/failure/optional-step.yaml +82 -0
  32. dirigent_examples/shelves/failure/retries.yaml +75 -0
  33. dirigent_examples/shelves/failure/retry-budget.yaml +82 -0
  34. dirigent_examples/shelves/failure/step-timeout.yaml +96 -0
  35. dirigent_examples/shelves/git/README.md +32 -0
  36. dirigent_examples/shelves/git/git-checkout-build.yaml +125 -0
  37. dirigent_examples/shelves/git/git-checkout-compose.yaml +138 -0
  38. dirigent_examples/shelves/git/git-checkout-public.yaml +84 -0
  39. dirigent_examples/shelves/graph/README.md +22 -0
  40. dirigent_examples/shelves/graph/deep-chain.yaml +119 -0
  41. dirigent_examples/shelves/graph/fan-in.yaml +80 -0
  42. dirigent_examples/shelves/graph/fan-out.yaml +66 -0
  43. dirigent_examples/shelves/graph/linear.yaml +66 -0
  44. dirigent_examples/shelves/graph/parallel-branches.yaml +57 -0
  45. dirigent_examples/shelves/graph/parallel-sleep.yaml +55 -0
  46. dirigent_examples/shelves/graph/skip-diamond.yaml +105 -0
  47. dirigent_examples/shelves/graph/wide-fan.yaml +147 -0
  48. dirigent_examples/shelves/hello-world.yaml +30 -0
  49. dirigent_examples/shelves/open-data/README.md +67 -0
  50. dirigent_examples/shelves/open-data/feeds-composition.yaml +120 -0
  51. dirigent_examples/shelves/open-data/gdacs-disaster-updates.yaml +230 -0
  52. dirigent_examples/shelves/open-data/github-releases-relay.yaml +225 -0
  53. dirigent_examples/shelves/open-data/hdx-dataset-watch.yaml +211 -0
  54. dirigent_examples/shelves/open-data/kobo-submissions-to-csv.yaml +149 -0
  55. dirigent_examples/shelves/open-data/nominatim-geocode-facilities.yaml +169 -0
  56. dirigent_examples/shelves/open-data/odk-central-submissions.yaml +146 -0
  57. dirigent_examples/shelves/open-data/open-meteo-weekly-report.yaml +142 -0
  58. dirigent_examples/shelves/open-data/overpass-health-facilities.yaml +154 -0
  59. dirigent_examples/shelves/open-data/usgs-earthquakes-alert.yaml +208 -0
  60. dirigent_examples/shelves/open-data/who-gho-indicators-to-parquet.yaml +163 -0
  61. dirigent_examples/shelves/open-data/wikidata-country-reference.yaml +146 -0
  62. dirigent_examples/shelves/open-data/world-bank-population-trend.yaml +166 -0
  63. dirigent_examples/shelves/patterns/README.md +144 -0
  64. dirigent_examples/shelves/patterns/concurrency-queue.yaml +89 -0
  65. dirigent_examples/shelves/patterns/concurrency-replace.yaml +92 -0
  66. dirigent_examples/shelves/patterns/concurrency-skip.yaml +97 -0
  67. dirigent_examples/shelves/patterns/connections-referenced-vs-carried.yaml +140 -0
  68. dirigent_examples/shelves/patterns/deadline-on-a-sensor.yaml +106 -0
  69. dirigent_examples/shelves/patterns/fan-out-continue.yaml +88 -0
  70. dirigent_examples/shelves/patterns/fan-out-fail-fast.yaml +80 -0
  71. dirigent_examples/shelves/patterns/fan-out-from-params.yaml +84 -0
  72. dirigent_examples/shelves/patterns/fan-out-item-wise.yaml +111 -0
  73. dirigent_examples/shelves/patterns/fan-out-literal-list.yaml +81 -0
  74. dirigent_examples/shelves/patterns/fan-out-nested-objects.yaml +107 -0
  75. dirigent_examples/shelves/patterns/fan-out-then-join.yaml +86 -0
  76. dirigent_examples/shelves/patterns/log-levels.yaml +119 -0
  77. dirigent_examples/shelves/patterns/outputs-inline-vs-storage.yaml +140 -0
  78. dirigent_examples/shelves/patterns/params-every-type.yaml +259 -0
  79. dirigent_examples/shelves/patterns/params-validation-refuses.yaml +131 -0
  80. dirigent_examples/shelves/patterns/pipeline-run-child.yaml +96 -0
  81. dirigent_examples/shelves/patterns/pipeline-run-fire-and-forget.yaml +99 -0
  82. dirigent_examples/shelves/patterns/pipeline-run-strict.yaml +103 -0
  83. dirigent_examples/shelves/patterns/pipeline-run-wait.yaml +107 -0
  84. dirigent_examples/shelves/patterns/pipeline-run-with-params.yaml +124 -0
  85. dirigent_examples/shelves/patterns/poll-cadence.yaml +102 -0
  86. dirigent_examples/shelves/patterns/priority-layered.yaml +120 -0
  87. dirigent_examples/shelves/patterns/references-cheat-sheet.yaml +186 -0
  88. dirigent_examples/shelves/patterns/retry-budget-exhausted.yaml +92 -0
  89. dirigent_examples/shelves/patterns/retry-exponential-backoff.yaml +88 -0
  90. dirigent_examples/shelves/patterns/retry-only-transient.yaml +108 -0
  91. dirigent_examples/shelves/patterns/retry-with-jitter.yaml +102 -0
  92. dirigent_examples/shelves/patterns/rule-all-done.yaml +83 -0
  93. dirigent_examples/shelves/patterns/rule-all-success.yaml +79 -0
  94. dirigent_examples/shelves/patterns/rule-always.yaml +92 -0
  95. dirigent_examples/shelves/patterns/rule-one-failed.yaml +86 -0
  96. dirigent_examples/shelves/patterns/schedule-at-once.yaml +110 -0
  97. dirigent_examples/shelves/patterns/schedule-cron-timezone.yaml +121 -0
  98. dirigent_examples/shelves/patterns/schedule-interval.yaml +109 -0
  99. dirigent_examples/shelves/patterns/schedule-window-half-open.yaml +105 -0
  100. dirigent_examples/shelves/patterns/sensor-http-ready.yaml +121 -0
  101. dirigent_examples/shelves/patterns/sensor-storage-exists.yaml +134 -0
  102. dirigent_examples/shelves/patterns/step-names-and-keys.yaml +99 -0
  103. dirigent_examples/shelves/patterns/timeout-fails-the-step.yaml +94 -0
  104. dirigent_examples/shelves/patterns/timeout-skips-the-step.yaml +102 -0
  105. dirigent_examples/shelves/patterns/webhook-mapping-nested-payload.yaml +125 -0
  106. dirigent_examples/shelves/patterns/webhook-signed.yaml +144 -0
  107. dirigent_examples/shelves/preview/s3-parquet-to-ingestion.yaml +92 -0
  108. dirigent_examples/shelves/python/README.md +31 -0
  109. dirigent_examples/shelves/python/apply_and_run.py +52 -0
  110. dirigent_examples/shelves/python/ci_gate.py +76 -0
  111. dirigent_examples/shelves/python/connections.py +61 -0
  112. dirigent_examples/shelves/python/error_handling.py +84 -0
  113. dirigent_examples/shelves/python/follow_logs.py +39 -0
  114. dirigent_examples/shelves/python/list_and_filter.py +52 -0
  115. dirigent_examples/shelves/queues/README.md +59 -0
  116. dirigent_examples/shelves/queues/kafka-consume-then-transform.yaml +105 -0
  117. dirigent_examples/shelves/queues/kafka-produce-then-consume.yaml +124 -0
  118. dirigent_examples/shelves/queues/rabbitmq-consume-ack-on-success.yaml +102 -0
  119. dirigent_examples/shelves/queues/report-to-kafka.yaml +105 -0
  120. dirigent_examples/shelves/queues/report-to-rabbitmq.yaml +105 -0
  121. dirigent_examples/shelves/recipes/README.md +130 -0
  122. dirigent_examples/shelves/recipes/csv-header-rules.yaml +147 -0
  123. dirigent_examples/shelves/recipes/csv-to-ndjson.yaml +107 -0
  124. dirigent_examples/shelves/recipes/etl-csv-clean-validate-parquet.yaml +207 -0
  125. dirigent_examples/shelves/recipes/filter-by-predicate.yaml +116 -0
  126. dirigent_examples/shelves/recipes/filter-then-map-then-reduce.yaml +109 -0
  127. dirigent_examples/shelves/recipes/http-fetch-validate-post.yaml +142 -0
  128. dirigent_examples/shelves/recipes/http-follow-redirects.yaml +100 -0
  129. dirigent_examples/shelves/recipes/http-get-with-query.yaml +96 -0
  130. dirigent_examples/shelves/recipes/http-headers-and-auth-connection.yaml +110 -0
  131. dirigent_examples/shelves/recipes/http-post-file-from-storage.yaml +117 -0
  132. dirigent_examples/shelves/recipes/http-post-json-echo.yaml +105 -0
  133. dirigent_examples/shelves/recipes/http-post-report.yaml +183 -0
  134. dirigent_examples/shelves/recipes/http-save-body-to-storage.yaml +106 -0
  135. dirigent_examples/shelves/recipes/http-success-status-list.yaml +80 -0
  136. dirigent_examples/shelves/recipes/http-timeout-override.yaml +104 -0
  137. dirigent_examples/shelves/recipes/jq-dedupe-by-key.yaml +78 -0
  138. dirigent_examples/shelves/recipes/jq-defaults-and-nulls.yaml +91 -0
  139. dirigent_examples/shelves/recipes/jq-group-by-and-sum.yaml +74 -0
  140. dirigent_examples/shelves/recipes/jq-join-two-lists.yaml +77 -0
  141. dirigent_examples/shelves/recipes/jq-long-to-wide.yaml +76 -0
  142. dirigent_examples/shelves/recipes/jq-nested-to-flat.yaml +89 -0
  143. dirigent_examples/shelves/recipes/jq-pivot-wide-to-long.yaml +65 -0
  144. dirigent_examples/shelves/recipes/jq-running-totals.yaml +82 -0
  145. dirigent_examples/shelves/recipes/jq-string-cleaning.yaml +88 -0
  146. dirigent_examples/shelves/recipes/jq-top-n.yaml +85 -0
  147. dirigent_examples/shelves/recipes/jq-validate-in-jq-vs-schema.yaml +124 -0
  148. dirigent_examples/shelves/recipes/jq-window-dates.yaml +82 -0
  149. dirigent_examples/shelves/recipes/json-to-csv-flattening.yaml +141 -0
  150. dirigent_examples/shelves/recipes/large-output-to-storage.yaml +134 -0
  151. dirigent_examples/shelves/recipes/map-enrich-with-lookup.yaml +96 -0
  152. dirigent_examples/shelves/recipes/ndjson-to-parquet.yaml +130 -0
  153. dirigent_examples/shelves/recipes/pagination-by-fan-out.yaml +115 -0
  154. dirigent_examples/shelves/recipes/parquet-round-trip-types.yaml +163 -0
  155. dirigent_examples/shelves/recipes/reconcile-two-sources.yaml +159 -0
  156. dirigent_examples/shelves/recipes/report-built-in.yaml +72 -0
  157. dirigent_examples/shelves/recipes/report-daily-digest.yaml +186 -0
  158. dirigent_examples/shelves/recipes/report-to-file.yaml +131 -0
  159. dirigent_examples/shelves/recipes/report-to-webhook.yaml +136 -0
  160. dirigent_examples/shelves/recipes/schema-carried.yaml +112 -0
  161. dirigent_examples/shelves/recipes/schema-formats.yaml +107 -0
  162. dirigent_examples/shelves/recipes/schema-referenced.yaml +86 -0
  163. dirigent_examples/shelves/recipes/schema-refuses-then-rule.yaml +127 -0
  164. dirigent_examples/shelves/recipes/storage-copy-dated-archive.yaml +114 -0
  165. dirigent_examples/shelves/recipes/storage-exists-gate.yaml +127 -0
  166. dirigent_examples/shelves/recipes/storage-manifest-of-a-fan-out.yaml +104 -0
  167. dirigent_examples/shelves/recipes/storage-write-then-read.yaml +119 -0
  168. dirigent_examples/shelves/recipes/webhook-post-hmac.yaml +132 -0
  169. dirigent_examples/shelves/recipes/webhook-post-summary.yaml +142 -0
  170. dirigent_examples/shelves/s3/README.md +34 -0
  171. dirigent_examples/shelves/s3/report-to-s3.yaml +93 -0
  172. dirigent_examples/shelves/s3/s3-copy-and-verify.yaml +105 -0
  173. dirigent_examples/shelves/s3/s3-csv-report.yaml +87 -0
  174. dirigent_examples/shelves/s3/s3-parquet-report.yaml +77 -0
  175. dirigent_examples/shelves/s3/s3-round-trip.yaml +125 -0
  176. dirigent_examples/shelves/schemas/README.md +36 -0
  177. dirigent_examples/shelves/schemas/echo-reading.json +18 -0
  178. dirigent_examples/shelves/schemas/ou-record.json +13 -0
  179. dirigent_examples/shelves/schemas/station-reading.json +13 -0
  180. dirigent_examples/shelves/sensors/README.md +16 -0
  181. dirigent_examples/shelves/sensors/sensor-gate.yaml +65 -0
  182. dirigent_examples/shelves/sensors/time-window.yaml +61 -0
  183. dirigent_examples/shelves/sql/README.md +52 -0
  184. dirigent_examples/shelves/sql/duckdb-parquet-to-report.yaml +146 -0
  185. dirigent_examples/shelves/sql/sql-postgres-readonly.yaml +111 -0
  186. dirigent_examples/shelves/sql/sql-query-to-storage.yaml +85 -0
  187. dirigent_examples/shelves/sql/sql-sqlite-roundtrip.yaml +114 -0
  188. dirigent_examples/shelves/sql/warehouse.sql +42 -0
  189. dirigent_examples/shelves/transform/README.md +36 -0
  190. dirigent_examples/shelves/transform/csv-report.yaml +55 -0
  191. dirigent_examples/shelves/transform/jq-filter-and-map.yaml +70 -0
  192. dirigent_examples/shelves/transform/jq-group-and-aggregate.yaml +70 -0
  193. dirigent_examples/shelves/transform/jq-join-two-sources.yaml +98 -0
  194. dirigent_examples/shelves/transform/jq-reshape.yaml +91 -0
  195. dirigent_examples/shelves/transform/jq-stream-through-storage.yaml +112 -0
  196. dirigent_examples/shelves/transform/ndjson-round-trip.yaml +56 -0
  197. dirigent_examples/shelves/transform/parquet-round-trip.yaml +68 -0
  198. dirigent_examples/shelves/transform/std-convert-fan-out.yaml +142 -0
  199. dirigent_examples/shelves/transform/xml-feed-to-ndjson.yaml +116 -0
  200. dirigent_examples/shelves/transform/yaml-config-to-json.yaml +104 -0
  201. dirigent_examples/shelves/triggers/README.md +45 -0
  202. dirigent_examples/shelves/triggers/at-one-time.yaml +78 -0
  203. dirigent_examples/shelves/triggers/cron-nightly.yaml +79 -0
  204. dirigent_examples/shelves/triggers/cron-windowed.yaml +86 -0
  205. dirigent_examples/shelves/triggers/document-nightly.yaml +80 -0
  206. dirigent_examples/shelves/triggers/interval-rolling.yaml +88 -0
  207. dirigent_examples/shelves/triggers/managed-and-manual.yaml +109 -0
  208. dirigent_examples/shelves/triggers/webhook-trigger.yaml +75 -0
  209. dirigent_examples/shelves/validate/README.md +31 -0
  210. dirigent_examples/shelves/validate/expects-a-shape.yaml +56 -0
  211. dirigent_examples/shelves/validate/the-shape-is-wrong.yaml +46 -0
  212. dirigent_examples-0.15.0.dist-info/METADATA +21 -0
  213. dirigent_examples-0.15.0.dist-info/RECORD +216 -0
  214. dirigent_examples-0.15.0.dist-info/WHEEL +4 -0
  215. dirigent_examples-0.15.0.dist-info/entry_points.txt +3 -0
  216. dirigent_examples-0.15.0.dist-info/licenses/LICENSE +18 -0
@@ -0,0 +1,120 @@
1
+ # Two of the feeds on this shelf, started by a third: refresh the reference, then read the data.
2
+ #
3
+ # `pipeline.run` starts another pipeline on the same instance and, by default, waits for it. So
4
+ # this document is an orchestration layer over documents that each stand alone: the Wikidata
5
+ # reference table and the WHO GHO extract know nothing about each other or about this file, and
6
+ # each is still runnable, testable and schedulable on its own.
7
+ #
8
+ # WHAT CROSSES THE BOUNDARY, AND WHAT DOES NOT. Parameters go down: the parent hands each child
9
+ # exactly the ones that child's own schema declares, validated against that schema when the step
10
+ # executes rather than when this document is applied. What comes back up is a status and a run
11
+ # id -- not the child's data. So the country list below is written here rather than read out of
12
+ # the reference the first child just built: a child's rows reach a parent only through storage,
13
+ # at a path both sides agree on, and under `--local` each run's scratch is its own. On an
14
+ # instance, giving both children a shared prefix parameter is what turns "refresh the reference"
15
+ # into something the next child can actually read.
16
+ #
17
+ # `requires.pipelines` names both children, so an instance that does not hold them refuses this
18
+ # document at apply time with the list of what to apply first, instead of failing at the step at
19
+ # six in the morning.
20
+ #
21
+ # What happens, hop by hop:
22
+ #
23
+ # reference the Wikidata country table, rebuilt. It takes no parameters: what it produces is
24
+ # the same for everybody, which is what makes it a reference.
25
+ # indicators the GHO extract for one indicator over the country list, started after the
26
+ # reference finished. It fans out internally with items: continue, so a country the
27
+ # endpoint refuses leaves that child completed_with_errors -- and `strict` is left
28
+ # at its default here, so that outcome is reported rather than treated as failure.
29
+ # Setting strict: true is how a parent says a partial child is not good enough.
30
+ # briefing the receipt: which children ran, under which run ids, and how each ended. A run
31
+ # id is what somebody opens to see what actually happened inside a child.
32
+ #
33
+ # To make it yours: add a child. A briefing is a list of pipeline.run steps and a transform that
34
+ # reads their statuses, and adding a feed is one step plus one line in requires.
35
+ #
36
+ # dg run --local examples/open-data/feeds-composition.yaml \
37
+ # --also-apply examples/open-data/wikidata-country-reference.yaml \
38
+ # --also-apply examples/open-data/who-gho-indicators-to-parquet.yaml
39
+ #
40
+ # Against an instance the children are applied first, and then the parent is just a pipeline:
41
+ #
42
+ # dg apply examples/open-data/wikidata-country-reference.yaml
43
+ # dg apply examples/open-data/who-gho-indicators-to-parquet.yaml
44
+ # dg apply examples/open-data/feeds-composition.yaml
45
+ # dg run feeds-composition --watch
46
+
47
+ format: dirigent/v1
48
+ kind: pipeline
49
+ code: feeds-composition
50
+ name: Open data briefing
51
+ description: |
52
+ One document that runs two others: the **Wikidata** country reference and the **WHO GHO**
53
+ indicator extract, then reports how each child ended.
54
+
55
+ Parameters go down, statuses come back up, and data crosses between pipelines through
56
+ storage rather than through a step output.
57
+
58
+ tags: [open-data, pipeline, transform, briefing, composition]
59
+
60
+ requires:
61
+ blocks:
62
+ - pipeline.run
63
+ - transform.jq
64
+ pipelines:
65
+ - wikidata-country-reference
66
+ - who-gho-indicators-to-parquet
67
+
68
+ params:
69
+ type: object
70
+ additionalProperties: false
71
+ properties:
72
+ indicator:
73
+ type: string
74
+ default: WHOSIS_000001
75
+ description: Handed to the GHO extract as its own indicator parameter.
76
+ countries:
77
+ type: array
78
+ default: [MWI, NPL]
79
+ items:
80
+ type: string
81
+ description: The countries the extract fans out over.
82
+
83
+ steps:
84
+ reference:
85
+ block: pipeline.run
86
+ config:
87
+ pipeline: wikidata-country-reference
88
+ # No params: the child's defaults are the whole answer, and a parent that repeats them
89
+ # here would be a second place to change them.
90
+
91
+ indicators:
92
+ block: pipeline.run
93
+ depends_on: [reference]
94
+ config:
95
+ pipeline: who-gho-indicators-to-parquet
96
+ # Only what the child's schema declares. A parameter it does not have is refused when
97
+ # this step executes, which is the same check a person typing -p would get.
98
+ params:
99
+ indicator: ${params.indicator}
100
+ countries: ${params.countries}
101
+
102
+ briefing:
103
+ block: transform.jq
104
+ depends_on: [indicators]
105
+ config:
106
+ input:
107
+ indicator: ${params.indicator}
108
+ reference: ${steps.reference.output}
109
+ indicators: ${steps.indicators.output}
110
+ # A child hands back three things and no data: which pipeline ran, which run it was, and
111
+ # how it ended.
112
+ program: |
113
+ {
114
+ indicator,
115
+ children: [
116
+ {feed: .reference.pipeline, run: .reference.run_id, status: .reference.status},
117
+ {feed: .indicators.pipeline, run: .indicators.run_id, status: .indicators.status}
118
+ ],
119
+ all_green: ([.reference.status, .indicators.status] | all(. == "succeeded"))
120
+ }
@@ -0,0 +1,230 @@
1
+ # Yesterday's disaster alerts, with the ones you already saw taken out.
2
+ #
3
+ # GDACS -- the Global Disaster Alert and Coordination System, run by the JRC and OCHA -- is
4
+ # keyless and anonymous: no account, no token, no registered application. One GET answers
5
+ # GeoJSON, {"features": [{"properties": {...}}]}, and everything an event has is under
6
+ # `properties`.
7
+ #
8
+ # The window does not make the query non-overlapping. GDACS filters on the event's own active
9
+ # range, so a cyclone that opened last week and has not closed comes back in today's answer and
10
+ # in tomorrow's, and an event that is revised keeps its `eventid` while `datemodified` moves.
11
+ # Both are the same problem: the query cannot tell you what is new. The fix is a list of what
12
+ # yesterday's run saw, and the honest consequence is visible in the graph: yesterday's file is
13
+ # read behind a sensor, so on the very first day there is nothing to compare against, `seen`
14
+ # skips, and everything behind it skips too. Day one posts nothing and ends succeeded; day two
15
+ # posts what day one did not have. There is no state block, so a marker file is the state, and a
16
+ # skipped read is how a pipeline says "there was no marker".
17
+ #
18
+ # What happens, hop by hop:
19
+ #
20
+ # dates the window turned into the two dates -- the ones the query filters between, and
21
+ # the ones the files below are named by. GDACS wants a plain day here, not the
22
+ # instant a window edge carries. It settles the prefix the lists are kept under
23
+ # as well, since the default one is only known once the run has started.
24
+ # fetch one GET. The two list filters are semicolon-joined in a single parameter, which is
25
+ # this API's own convention rather than a general one.
26
+ # today the events flattened to rows, carried as a value because the comparison needs them
27
+ # as data.
28
+ # record storage.write, this day's list, put down before anything is compared so tomorrow's
29
+ # run has something to read even if the post below fails.
30
+ # seen yesterday's list, if there is one. Five seconds, then skip: an absent file is the
31
+ # expected answer on day one and after any gap in the schedule.
32
+ # recalled storage.read, yesterday's file brought back into the run as a value. A value
33
+ # goes out to storage only through a write and comes in only through a read, so
34
+ # the marker costs a step at each end.
35
+ # previous yesterday's ids, taken out of what that read produced.
36
+ # fresh the difference. The comparison is over ids alone, because a revised event keeps
37
+ # its id and moves its date.
38
+ # notify what is actually new, posted. Postman Echo by default so the shape of the post is
39
+ # visible without a receiver; point it at yours.
40
+ #
41
+ # To make it yours: narrow `eventlist` and `alertlevel` to what you act on, and give `store` a
42
+ # prefix that outlives one run. The default is `${run.scratch}`, which is deleted with the run,
43
+ # so under it every day is day one.
44
+ #
45
+ # Rehearsing both days from the CLI takes one directory holding the instance, and a `store`
46
+ # under that directory's artifact root so the second run finds what the first wrote:
47
+ #
48
+ # dg run --local --root /tmp/dirigent-gdacs examples/open-data/gdacs-disaster-updates.yaml \
49
+ # -p store=file:///tmp/dirigent-gdacs/artifacts/gdacs --window 2026-09-05..2026-09-06
50
+ # dg run --local --root /tmp/dirigent-gdacs examples/open-data/gdacs-disaster-updates.yaml \
51
+ # -p store=file:///tmp/dirigent-gdacs/artifacts/gdacs --window 2026-09-06..2026-09-07
52
+ #
53
+ # Day one skips at `seen` and posts nothing; day two posts the difference.
54
+
55
+ format: dirigent/v1
56
+ kind: pipeline
57
+ code: gdacs-disaster-updates
58
+ name: GDACS disaster updates
59
+ description: |
60
+ Disaster events active inside the run's window, read from the **GDACS** event list,
61
+ deduplicated against the previous day's list and posted onward.
62
+
63
+ Keyless and anonymous: nothing to register, nothing to hold.
64
+
65
+ tags: [open-data, http, sensor, storage, transform, webhook, schedule]
66
+
67
+ requires:
68
+ blocks:
69
+ - transform.jq
70
+ - http.request
71
+ - storage.write
72
+ - storage.exists
73
+ - storage.read
74
+ - webhook.post
75
+
76
+ params:
77
+ type: object
78
+ additionalProperties: false
79
+ properties:
80
+ eventlist:
81
+ type: string
82
+ default: EQ;TC;FL
83
+ description: >-
84
+ Event types, semicolon-separated as GDACS reads them: EQ earthquake, TC tropical
85
+ cyclone, FL flood, VO volcano, DR drought, WF wildfire.
86
+ alertlevel:
87
+ type: string
88
+ default: Green;Orange;Red
89
+ description: Alert levels, semicolon-separated. Orange;Red is the pair worth waking for.
90
+ store:
91
+ type: string
92
+ default: ""
93
+ description: >-
94
+ A prefix the daily lists are written under, such as
95
+ file:///var/lib/dirigent/gdacs. Empty keeps them in the run's own scratch, where
96
+ tomorrow's run cannot read them.
97
+ webhook_url:
98
+ type: string
99
+ default: https://postman-echo.com/post
100
+ description: Where the digest goes.
101
+
102
+ steps:
103
+ dates:
104
+ block: transform.jq
105
+ config:
106
+ input:
107
+ start: ${run.window.start}
108
+ end: ${run.window.end}
109
+ store: ${params.store}
110
+ scratch: ${run.scratch}
111
+ # The two days the query filters between, and the two file names. Yesterday is the day
112
+ # before the window opened, which is the file the previous firing wrote. The prefix is
113
+ # settled here too: a reference resolves once, so a default that names another one has
114
+ # to be worked out in a step rather than written in the parameter.
115
+ program: |
116
+ def as_date: .[0:19] + "Z" | fromdateiso8601;
117
+ {
118
+ day: .start[0:10],
119
+ from_date: .start[0:10],
120
+ to_date: .end[0:10],
121
+ previous_day: (.start | as_date | . - 86400 | strftime("%Y-%m-%d")),
122
+ store: (if .store == "" then .scratch + "/gdacs" else .store end)
123
+ }
124
+
125
+ fetch:
126
+ block: http.request
127
+ depends_on: [dates]
128
+ config:
129
+ url: https://www.gdacs.org/gdacsapi/api/events/geteventlist/SEARCH
130
+ query:
131
+ # Both filters are lists written into one parameter, joined with semicolons. This is
132
+ # GDACS's own convention, not a general one.
133
+ eventlist: ${params.eventlist}
134
+ alertlevel: ${params.alertlevel}
135
+ # A plain day at each edge. The filter is an overlap against the event's own from/to
136
+ # range, so an event still running when the window opened is included.
137
+ fromDate: ${steps.dates.output.value.from_date}
138
+ toDate: ${steps.dates.output.value.to_date}
139
+ # The API's own page ceiling. A single day worldwide is far short of it; a longer
140
+ # window is not, and would need pageNumber walked.
141
+ pageSize: 100
142
+
143
+ today:
144
+ block: transform.jq
145
+ depends_on: [fetch]
146
+ config:
147
+ input: ${steps.fetch.output.body}
148
+ # GeoJSON, so the event is under properties. The id is the type and the number together:
149
+ # GDACS's own detail URL takes both, and the number alone is a sequence per event type.
150
+ program: |
151
+ [.features[]
152
+ | .properties as $p
153
+ | {
154
+ id: "\($p.eventtype)-\($p.eventid)",
155
+ title: $p.name,
156
+ url: $p.url.report,
157
+ event_type: $p.eventtype,
158
+ alert_level: $p.alertlevel,
159
+ country: $p.country,
160
+ from_date: $p.fromdate,
161
+ modified_at: $p.datemodified
162
+ }]
163
+
164
+ record:
165
+ block: storage.write
166
+ depends_on: [dates, today]
167
+ config:
168
+ # Written before anything is compared, so tomorrow's run has a list to read even if the
169
+ # post below fails. The day names the file, which is what makes "yesterday" a path.
170
+ target: ${steps.dates.output.value.store}/${steps.dates.output.value.day}.json
171
+ value: ${steps.today.output.value}
172
+
173
+ seen:
174
+ block: storage.exists
175
+ depends_on: [dates, record]
176
+ poll: 1s
177
+ deadline: 5s
178
+ # No file for yesterday means no previous run, or a gap in the schedule. Both are quiet
179
+ # days rather than failures, and everything downstream skips with this step.
180
+ on_timeout: skip
181
+ config:
182
+ uri: ${steps.dates.output.value.store}/${steps.dates.output.value.previous_day}.json
183
+
184
+ recalled:
185
+ block: storage.read
186
+ depends_on: [seen]
187
+ config:
188
+ # The path the sensor above just found, read as the value yesterday's run wrote.
189
+ source: ${steps.seen.output.uri}
190
+
191
+ previous:
192
+ block: transform.jq
193
+ depends_on: [recalled]
194
+ config:
195
+ input: ${steps.recalled.output.value}
196
+ # Only the ids: the comparison is about identity, and yesterday's alert levels are
197
+ # yesterday's problem.
198
+ program: |
199
+ [.[].id]
200
+
201
+ fresh:
202
+ block: transform.jq
203
+ depends_on: [today, previous]
204
+ config:
205
+ input:
206
+ today: ${steps.today.output.value}
207
+ seen: ${steps.previous.output.value}
208
+ # A revised event keeps its id and moves its datemodified, so identity is the only safe
209
+ # key. An escalation from Green to Red is therefore not news here; a new eventid is.
210
+ program: |
211
+ . as {$today, $seen}
212
+ | [$today[] | select(.id as $id | $seen | index($id) | not)]
213
+
214
+ notify:
215
+ block: webhook.post
216
+ depends_on: [dates, fresh]
217
+ config:
218
+ url: ${params.webhook_url}
219
+ body:
220
+ kind: gdacs-updates
221
+ day: ${steps.dates.output.value.day}
222
+ events: ${steps.fresh.output.value}
223
+
224
+ triggers:
225
+ schedules:
226
+ - code: daily
227
+ name: Every morning, for the day that closed
228
+ description: Reads the previous day, which is the window this firing carries.
229
+ cron: "0 7 * * *"
230
+ timezone: UTC
@@ -0,0 +1,225 @@
1
+ # New releases of a repository, relayed onward, with the rate limit treated as a fact of life.
2
+ #
3
+ # GitHub's REST API serves public repositories without a token: GET /repos/{owner}/{repo}/releases
4
+ # answers a list, newest first, each release carrying `tag_name`, `name`, `published_at`,
5
+ # `prerelease`, `draft` and `html_url`. Unauthenticated calls are limited to 60 requests per hour
6
+ # per IP, and that is the whole budget -- shared with everything else on that address. A schedule
7
+ # firing every ten minutes spends 6 of them; one firing hourly spends 1, which is the right
8
+ # cadence for something that publishes a release a month. A token raises the limit to 5000, and
9
+ # is the only reason to give this pipeline a connection.
10
+ #
11
+ # The floor is a parameter here, and the marker is written but not read. `since` says what has
12
+ # already been relayed, the last step records the newest stamp this run saw, and pointing the
13
+ # next firing's `since` at that marker is the read half of the pattern -- the same sensor-plus-
14
+ # transform shape any marker read has. Splitting it this way keeps the two halves legible: this
15
+ # document is about what "new" means and what a rate limit costs, not about state.
16
+ #
17
+ # What happens, hop by hop:
18
+ #
19
+ # releases one GET, with the API version pinned by the Accept header. GitHub asks every
20
+ # client for a User-Agent and refuses requests without one.
21
+ # candidates the releases flattened, each row carrying the floor and the two exclusions it
22
+ # will be judged by. A jq program never has values spliced into its text, so
23
+ # anything a filter compares against has to arrive as data on the row.
24
+ # fresh filter.jq: published after the floor, not a draft, and not a prerelease unless
25
+ # the run asked for them. Three comparisons, each between two fields of the row.
26
+ # digest the rows a person would read, with the carried limits dropped again.
27
+ # ordered the digest in publication order, which is the order the file below holds.
28
+ # staged storage.write, the digest as one json object. A converter reads a URI and writes
29
+ # a URI, so a value the run is holding is put down before it can be converted.
30
+ # written json to ndjson, because an empty list has to encode as an empty file for the
31
+ # gate below to see the difference.
32
+ # matched the gate: nothing new is a skip, not a failure, and the relay skips with it.
33
+ # relay the digest, posted. Postman Echo by default so this runs for real.
34
+ # marker the newest stamp seen, plus what the rate limit had left, computed under
35
+ # rule: all_done so a quiet run still records where it got to. GitHub reports the
36
+ # remaining budget in a response header, which is worth keeping beside the stamp:
37
+ # when the relay goes quiet, that number says whether it is the repo or the quota.
38
+ # remember that marker written to storage, which is the half the next firing would read.
39
+ #
40
+ # To make it yours: set repo, move `since` forward to when you started caring, and point
41
+ # webhook_url at your receiver. For a private repository or a heavier cadence, put a token in
42
+ # a connection and give the step `connection:` instead of `url:`.
43
+ #
44
+ # dg run --local examples/open-data/github-releases-relay.yaml
45
+ # dg run --local examples/open-data/github-releases-relay.yaml -p repo=jqlang/jq -p since=2030-01-01T00:00:00Z
46
+
47
+ format: dirigent/v1
48
+ kind: pipeline
49
+ code: github-releases-relay
50
+ name: GitHub releases relay
51
+ description: |
52
+ Releases of a public repository published since a floor, read from **GitHub's REST API**
53
+ without a token, and relayed onward -- only when there are any.
54
+
55
+ Unauthenticated calls share a budget of 60 requests an hour per IP, which is what decides
56
+ the cadence a schedule can have.
57
+
58
+ tags: [open-data, http, sensor, storage, transform, webhook, filter, relay, starter]
59
+
60
+ requires:
61
+ blocks:
62
+ - http.request
63
+ - transform.jq
64
+ - filter.jq
65
+ - map.jq
66
+ - convert.std
67
+ - storage.write
68
+ - storage.exists
69
+ - webhook.post
70
+
71
+ params:
72
+ type: object
73
+ additionalProperties: false
74
+ properties:
75
+ repo:
76
+ type: string
77
+ default: jqlang/jq
78
+ description: owner/name of a public repository.
79
+ since:
80
+ type: string
81
+ default: "2020-01-01T00:00:00Z"
82
+ description: Only releases published after this instant are relayed.
83
+ include_prereleases:
84
+ type: boolean
85
+ default: false
86
+ per_page:
87
+ type: integer
88
+ default: 30
89
+ maximum: 100
90
+ description: One page is one request against a 60-per-hour budget; ask for enough.
91
+ webhook_url:
92
+ type: string
93
+ default: https://postman-echo.com/post
94
+
95
+ steps:
96
+ releases:
97
+ block: http.request
98
+ config:
99
+ url: https://api.github.com/repos/${params.repo}/releases
100
+ headers:
101
+ # Pinning the API version here is what stops a future default from reshaping the
102
+ # payload under a pipeline that has been running for a year.
103
+ Accept: application/vnd.github+json
104
+ X-GitHub-Api-Version: "2022-11-28"
105
+ User-Agent: dirigent-example
106
+ query:
107
+ per_page: ${params.per_page}
108
+
109
+ candidates:
110
+ block: transform.jq
111
+ depends_on: [releases]
112
+ config:
113
+ input:
114
+ releases: ${steps.releases.output.body}
115
+ since: ${params.since}
116
+ include_prereleases: ${params.include_prereleases}
117
+ # A draft has no published_at at all, so the null is replaced with a stamp that loses
118
+ # every comparison rather than being compared as null.
119
+ program: |
120
+ . as {$since, $include_prereleases}
121
+ | [.releases[]
122
+ | {
123
+ tag: .tag_name,
124
+ title: (.name // .tag_name),
125
+ published_at: (.published_at // "0000-01-01T00:00:00Z"),
126
+ draft: .draft,
127
+ prerelease: .prerelease,
128
+ url: .html_url,
129
+ since: $since,
130
+ include_prereleases: $include_prereleases
131
+ }]
132
+
133
+ fresh:
134
+ block: filter.jq
135
+ depends_on: [candidates]
136
+ config:
137
+ input: ${steps.candidates.output.value}
138
+ # ISO 8601 stamps in the same zone compare correctly as strings, which is the one thing
139
+ # that format is designed to make true.
140
+ program: |
141
+ .published_at > .since
142
+ and (.draft | not)
143
+ and (.include_prereleases or (.prerelease | not))
144
+
145
+ digest:
146
+ block: map.jq
147
+ depends_on: [fresh]
148
+ config:
149
+ input: ${steps.fresh.output.value}
150
+ program: |
151
+ del(.since, .include_prereleases)
152
+
153
+ ordered:
154
+ block: transform.jq
155
+ depends_on: [digest]
156
+ config:
157
+ input: ${steps.digest.output.value}
158
+ program: |
159
+ sort_by(.published_at)
160
+
161
+ staged:
162
+ block: storage.write
163
+ depends_on: [ordered]
164
+ config:
165
+ target: ${run.scratch}/fresh.json
166
+ value: ${steps.ordered.output.value}
167
+
168
+ written:
169
+ block: convert.std
170
+ depends_on: [staged]
171
+ config:
172
+ source: ${steps.staged.output.uri}
173
+ target: ${run.scratch}/fresh.ndjson
174
+ from: json
175
+ to: ndjson
176
+
177
+ matched:
178
+ block: storage.exists
179
+ depends_on: [written]
180
+ poll: 1s
181
+ deadline: 3s
182
+ on_timeout: skip
183
+ config:
184
+ uri: ${steps.written.output.target}
185
+ min_size: 1b
186
+
187
+ relay:
188
+ block: webhook.post
189
+ depends_on: [digest, matched]
190
+ config:
191
+ url: ${params.webhook_url}
192
+ body:
193
+ kind: github-releases
194
+ repo: ${params.repo}
195
+ since: ${params.since}
196
+ releases: ${steps.digest.output.value}
197
+
198
+ marker:
199
+ block: transform.jq
200
+ depends_on: [releases, digest, matched]
201
+ # all_done: a run that relayed nothing still saw the repository, and still knows what the
202
+ # budget had left.
203
+ rule: all_done
204
+ config:
205
+ input:
206
+ repo: ${params.repo}
207
+ releases: ${steps.digest.output.value}
208
+ # Header names arrive lower-cased, which is what HTTP/2 makes of them.
209
+ rate_limit_remaining: ${steps.releases.output.headers.x-ratelimit-remaining}
210
+ # The newest stamp seen, which is what the next run's `since` should be. `max` over an
211
+ # empty list is null, so a quiet run reports null rather than inventing a floor.
212
+ program: |
213
+ {
214
+ repo,
215
+ rate_limit_remaining,
216
+ relayed: (.releases | length),
217
+ newest_published_at: ([.releases[].published_at] | max)
218
+ }
219
+
220
+ remember:
221
+ block: storage.write
222
+ depends_on: [marker]
223
+ config:
224
+ target: ${run.scratch}/markers/releases.json
225
+ value: ${steps.marker.output.value}