dhis2w-ql 0.99.1__tar.gz

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.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.3
2
+ Name: dhis2w-ql
3
+ Version: 0.99.1
4
+ Summary: d2ql: a pipeline query + transform language over JSON-shaped data, with an embedded expression core (d2path). Pure engine (tokenizer, parser, evaluator, planner) with no DHIS2 or FHIR runtime dependency.
5
+ Author: Morten Hansen
6
+ Author-email: Morten Hansen <morten@winterop.com>
7
+ Requires-Dist: pydantic>=2.13
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
11
+ # dhis2w-ql
12
+
13
+ `d2ql` — a pipeline query and transform language with an embedded expression language, `d2path`.
14
+ Pure engine, no DHIS2 required: it queries any JSON-shaped data — lists of dicts, Pydantic models,
15
+ local `.json`/`.ndjson` files — with a pushdown seam for backends that can answer parts of a query
16
+ natively.
17
+
18
+ This package is the language engine alone: tokenizer, recursive-descent parser, Pydantic AST,
19
+ expression evaluator, query planner, and execution engine over a source-agnostic `DataSource`
20
+ protocol. Its only dependency is `pydantic`. The DHIS2 binding (live `DataSource`, pushdown
21
+ compiler, CLI, MCP tools) lives in the `query` plugin in `dhis2w-core`; FHIR is a consumer of the
22
+ engine (`d2path` evaluates over any JSON, and the `transform` stage can emit FHIR resources)
23
+ rather than a dependency.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ uv add dhis2w-ql # or: pip install dhis2w-ql
29
+ ```
30
+
31
+ ## Quickstart — d2path over your own JSON
32
+
33
+ `d2path` is the expression layer: path navigation, operators, and ~40 functions with collection
34
+ semantics.
35
+
36
+ ```python
37
+ from dhis2w_ql import Evaluator, parse_expression
38
+
39
+ facilities = [
40
+ {"name": "Ngelehun CHC", "level": 4, "tags": ["chc", "rural"]},
41
+ {"name": "Kailahun MCHP", "level": 4, "tags": ["mchp"]},
42
+ {"name": "Bo District", "level": 2, "tags": []},
43
+ ]
44
+
45
+ expression = parse_expression('where(level = 4 and tags.count() > 0).name.select(upper())')
46
+ print(Evaluator().evaluate(expression, facilities))
47
+ # ['NGELEHUN CHC', 'KAILAHUN MCHP']
48
+ ```
49
+
50
+ ## Quickstart — a full pipeline over in-memory rows
51
+
52
+ The pipeline layer adds stages (`where`, `select`, `transform`, `order`, paging, `group by`,
53
+ `fold`), named definitions, and sinks. `InMemoryBinder` maps resource names to row lists; any
54
+ backend can implement the same `DataSource` protocol and advertise which filters/ordering/paging
55
+ it can execute natively — the planner pushes that prefix down and runs the rest locally.
56
+
57
+ ```python
58
+ import asyncio
59
+
60
+ from dhis2w_ql import InMemoryBinder, QueryEngine, parse
61
+
62
+ program = parse("""
63
+ facilities
64
+ | where level = 4
65
+ | select name, level
66
+ | order name asc
67
+ | limit 10
68
+ """)
69
+
70
+ engine = QueryEngine(program, InMemoryBinder({"facilities": facilities}))
71
+ result = asyncio.run(engine.run_terminal())
72
+ print(result.rows)
73
+ # [{'name': 'Kailahun MCHP', 'level': 4}, {'name': 'Ngelehun CHC', 'level': 4}]
74
+ ```
75
+
76
+ Programs can also read local files directly — `read("facilities.json")` or
77
+ `read("events.ndjson")` as the source — and end in a sink (`>> "out.csv"`,
78
+ `>> stdout as ndjson`).
79
+
80
+ ## Language shape
81
+
82
+ ```
83
+ define ActiveAggregates:
84
+ dataElements | where domainType = "AGGREGATE"
85
+
86
+ ActiveAggregates
87
+ | where name ~ "ANC"
88
+ | select id, name, categoryCombo.name as combo
89
+ | transform { code: id, label: name }
90
+ | order name asc
91
+ | limit 20
92
+ >> "elements.csv"
93
+ ```
94
+
95
+ - **Expression layer** (`d2path`): path navigation, operators, and functions with collection
96
+ semantics — used inside `where`, `select`, `order`, and `transform`.
97
+ - **Pipeline layer**: stages separated by `|`, optionally ending in a `>>` sink.
98
+ - **Definitions**: `define NAME: ...` and `define function NAME(args): ...` make a `.d2ql` file
99
+ a reusable library of named queries and helpers.
100
+
101
+ The pipeline `|` is the stage separator; collection union is the `union()` function (not the `|`
102
+ operator) to keep the two unambiguous.
103
+
104
+ ## Collection semantics in one minute
105
+
106
+ - Every expression evaluates to a collection; navigation (`a.b`) flattens one level per hop.
107
+ - A single-element collection collapses to its scalar in results.
108
+ - String functions (`upper()`, `lower()`, `length()`, `trim()`, `toChars()`) operate on a
109
+ singleton focus; map them over a collection with `select(...)` — `name.select(upper())`.
110
+ - Comparisons are existential over collections: `tags = "chc"` is true when any element matches.
111
+
112
+ ## Documentation
113
+
114
+ - [d2ql tutorial](https://winterop-com.github.io/dhis2w-utils/guides/d2ql-tutorial/) and
115
+ [reference](https://winterop-com.github.io/dhis2w-utils/guides/d2ql/)
116
+ - [d2path reference](https://winterop-com.github.io/dhis2w-utils/guides/d2path/) and the
117
+ [generated example catalog](https://winterop-com.github.io/dhis2w-utils/query/d2path-examples/)
118
+ — 140 examples covering every function, each parsed and evaluated in CI against this engine
119
+ - [Language semantics](https://winterop-com.github.io/dhis2w-utils/query/semantics/) and the
120
+ [cookbook](https://winterop-com.github.io/dhis2w-utils/query/cookbook/)
121
+ - Runnable sample programs: [`examples/d2ql/`](https://github.com/winterop-com/dhis2w-utils/tree/main/examples/d2ql)
122
+ in the repository — every file is parse-tested in CI
123
+
124
+ The curated catalogs ship in the package: `dhis2w_ql.SAMPLES` (sample programs) and
125
+ `dhis2w_ql.DOC_EXAMPLES` (the evaluator-verified example catalog behind the docs page).
@@ -0,0 +1,115 @@
1
+ # dhis2w-ql
2
+
3
+ `d2ql` — a pipeline query and transform language with an embedded expression language, `d2path`.
4
+ Pure engine, no DHIS2 required: it queries any JSON-shaped data — lists of dicts, Pydantic models,
5
+ local `.json`/`.ndjson` files — with a pushdown seam for backends that can answer parts of a query
6
+ natively.
7
+
8
+ This package is the language engine alone: tokenizer, recursive-descent parser, Pydantic AST,
9
+ expression evaluator, query planner, and execution engine over a source-agnostic `DataSource`
10
+ protocol. Its only dependency is `pydantic`. The DHIS2 binding (live `DataSource`, pushdown
11
+ compiler, CLI, MCP tools) lives in the `query` plugin in `dhis2w-core`; FHIR is a consumer of the
12
+ engine (`d2path` evaluates over any JSON, and the `transform` stage can emit FHIR resources)
13
+ rather than a dependency.
14
+
15
+ ## Install
16
+
17
+ ```bash
18
+ uv add dhis2w-ql # or: pip install dhis2w-ql
19
+ ```
20
+
21
+ ## Quickstart — d2path over your own JSON
22
+
23
+ `d2path` is the expression layer: path navigation, operators, and ~40 functions with collection
24
+ semantics.
25
+
26
+ ```python
27
+ from dhis2w_ql import Evaluator, parse_expression
28
+
29
+ facilities = [
30
+ {"name": "Ngelehun CHC", "level": 4, "tags": ["chc", "rural"]},
31
+ {"name": "Kailahun MCHP", "level": 4, "tags": ["mchp"]},
32
+ {"name": "Bo District", "level": 2, "tags": []},
33
+ ]
34
+
35
+ expression = parse_expression('where(level = 4 and tags.count() > 0).name.select(upper())')
36
+ print(Evaluator().evaluate(expression, facilities))
37
+ # ['NGELEHUN CHC', 'KAILAHUN MCHP']
38
+ ```
39
+
40
+ ## Quickstart — a full pipeline over in-memory rows
41
+
42
+ The pipeline layer adds stages (`where`, `select`, `transform`, `order`, paging, `group by`,
43
+ `fold`), named definitions, and sinks. `InMemoryBinder` maps resource names to row lists; any
44
+ backend can implement the same `DataSource` protocol and advertise which filters/ordering/paging
45
+ it can execute natively — the planner pushes that prefix down and runs the rest locally.
46
+
47
+ ```python
48
+ import asyncio
49
+
50
+ from dhis2w_ql import InMemoryBinder, QueryEngine, parse
51
+
52
+ program = parse("""
53
+ facilities
54
+ | where level = 4
55
+ | select name, level
56
+ | order name asc
57
+ | limit 10
58
+ """)
59
+
60
+ engine = QueryEngine(program, InMemoryBinder({"facilities": facilities}))
61
+ result = asyncio.run(engine.run_terminal())
62
+ print(result.rows)
63
+ # [{'name': 'Kailahun MCHP', 'level': 4}, {'name': 'Ngelehun CHC', 'level': 4}]
64
+ ```
65
+
66
+ Programs can also read local files directly — `read("facilities.json")` or
67
+ `read("events.ndjson")` as the source — and end in a sink (`>> "out.csv"`,
68
+ `>> stdout as ndjson`).
69
+
70
+ ## Language shape
71
+
72
+ ```
73
+ define ActiveAggregates:
74
+ dataElements | where domainType = "AGGREGATE"
75
+
76
+ ActiveAggregates
77
+ | where name ~ "ANC"
78
+ | select id, name, categoryCombo.name as combo
79
+ | transform { code: id, label: name }
80
+ | order name asc
81
+ | limit 20
82
+ >> "elements.csv"
83
+ ```
84
+
85
+ - **Expression layer** (`d2path`): path navigation, operators, and functions with collection
86
+ semantics — used inside `where`, `select`, `order`, and `transform`.
87
+ - **Pipeline layer**: stages separated by `|`, optionally ending in a `>>` sink.
88
+ - **Definitions**: `define NAME: ...` and `define function NAME(args): ...` make a `.d2ql` file
89
+ a reusable library of named queries and helpers.
90
+
91
+ The pipeline `|` is the stage separator; collection union is the `union()` function (not the `|`
92
+ operator) to keep the two unambiguous.
93
+
94
+ ## Collection semantics in one minute
95
+
96
+ - Every expression evaluates to a collection; navigation (`a.b`) flattens one level per hop.
97
+ - A single-element collection collapses to its scalar in results.
98
+ - String functions (`upper()`, `lower()`, `length()`, `trim()`, `toChars()`) operate on a
99
+ singleton focus; map them over a collection with `select(...)` — `name.select(upper())`.
100
+ - Comparisons are existential over collections: `tags = "chc"` is true when any element matches.
101
+
102
+ ## Documentation
103
+
104
+ - [d2ql tutorial](https://winterop-com.github.io/dhis2w-utils/guides/d2ql-tutorial/) and
105
+ [reference](https://winterop-com.github.io/dhis2w-utils/guides/d2ql/)
106
+ - [d2path reference](https://winterop-com.github.io/dhis2w-utils/guides/d2path/) and the
107
+ [generated example catalog](https://winterop-com.github.io/dhis2w-utils/query/d2path-examples/)
108
+ — 140 examples covering every function, each parsed and evaluated in CI against this engine
109
+ - [Language semantics](https://winterop-com.github.io/dhis2w-utils/query/semantics/) and the
110
+ [cookbook](https://winterop-com.github.io/dhis2w-utils/query/cookbook/)
111
+ - Runnable sample programs: [`examples/d2ql/`](https://github.com/winterop-com/dhis2w-utils/tree/main/examples/d2ql)
112
+ in the repository — every file is parse-tested in CI
113
+
114
+ The curated catalogs ship in the package: `dhis2w_ql.SAMPLES` (sample programs) and
115
+ `dhis2w_ql.DOC_EXAMPLES` (the evaluator-verified example catalog behind the docs page).
@@ -0,0 +1,21 @@
1
+ [project]
2
+ name = "dhis2w-ql"
3
+ version = "0.99.1"
4
+ description = "d2ql: a pipeline query + transform language over JSON-shaped data, with an embedded expression core (d2path). Pure engine (tokenizer, parser, evaluator, planner) with no DHIS2 or FHIR runtime dependency."
5
+ readme = "README.md"
6
+ authors = [{ name = "Morten Hansen", email = "morten@winterop.com" }]
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "pydantic>=2.13",
10
+ ]
11
+
12
+ # Domain-neutral: depends only on pydantic, no dhis2w-* or FHIR imports. The DHIS2 binding
13
+ # (DataSource, pushdown compiler, CLI, MCP) lives in the `query` plugin in dhis2w-core; FHIR is
14
+ # a consumer of the engine, never a dependency.
15
+ #
16
+ # Published: in the pypi-publish matrix (.github/workflows/pypi-publish.yml) — dhis2w-core
17
+ # declares it as a runtime dependency, so PyPI installs of dhis2w-core resolve it from PyPI.
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.11.26,<0.12.0"]
21
+ build-backend = "uv_build"
@@ -0,0 +1,84 @@
1
+ """d2ql: a pipeline query + transform language with the d2path expression core.
2
+
3
+ - `d2ql` is the pipeline language (`source | where | select | transform | order | limit >> sink`),
4
+ with `define` / `define function` library definitions.
5
+ - `d2path` (in `dhis2w_ql.d2path`) is the embedded expression language: path navigation, operators,
6
+ and functions with collection semantics.
7
+ - `dhis2w_ql.engine` runs a parsed library against a `ResourceBinder`, pushing supported work down
8
+ to the source and evaluating the rest locally.
9
+ """
10
+
11
+ from dhis2w_ql.ast import AggregateStage, CallSource, Define, DefineFunction, FoldStage, Library, Pipeline
12
+ from dhis2w_ql.d2path import EvalContext, Evaluator, Resolver
13
+ from dhis2w_ql.doc_examples import DOC_EXAMPLES, FunctionExample, doc_examples_by_category
14
+ from dhis2w_ql.engine import (
15
+ DataSource,
16
+ InMemoryBinder,
17
+ InMemoryDataSource,
18
+ NativeFilter,
19
+ NativeOrder,
20
+ NativeQuery,
21
+ QueryEngine,
22
+ QueryPlan,
23
+ QueryResult,
24
+ ResourceBinder,
25
+ SourceCapabilities,
26
+ plan_pipeline,
27
+ serialize_rows,
28
+ to_jsonable,
29
+ write_rows,
30
+ )
31
+ from dhis2w_ql.errors import D2qlError, EvaluationError, LexError, ParseError, SemanticError
32
+ from dhis2w_ql.generate import DEFAULT_SCHEMA, GeneratedExample, SchemaSpec, generate
33
+ from dhis2w_ql.parser import parse, parse_expression, parse_pipeline
34
+ from dhis2w_ql.samples import SAMPLES, Sample, samples_by_category
35
+ from dhis2w_ql.tokenizer import Token, TokenKind, tokenize
36
+
37
+ __all__ = [
38
+ "DEFAULT_SCHEMA",
39
+ "AggregateStage",
40
+ "CallSource",
41
+ "DOC_EXAMPLES",
42
+ "D2qlError",
43
+ "DataSource",
44
+ "Define",
45
+ "DefineFunction",
46
+ "EvalContext",
47
+ "FunctionExample",
48
+ "doc_examples_by_category",
49
+ "EvaluationError",
50
+ "Evaluator",
51
+ "FoldStage",
52
+ "GeneratedExample",
53
+ "SchemaSpec",
54
+ "generate",
55
+ "InMemoryBinder",
56
+ "InMemoryDataSource",
57
+ "LexError",
58
+ "Library",
59
+ "NativeFilter",
60
+ "NativeOrder",
61
+ "NativeQuery",
62
+ "ParseError",
63
+ "Pipeline",
64
+ "QueryEngine",
65
+ "QueryPlan",
66
+ "QueryResult",
67
+ "SAMPLES",
68
+ "Resolver",
69
+ "ResourceBinder",
70
+ "Sample",
71
+ "SemanticError",
72
+ "SourceCapabilities",
73
+ "Token",
74
+ "serialize_rows",
75
+ "TokenKind",
76
+ "parse",
77
+ "parse_expression",
78
+ "parse_pipeline",
79
+ "plan_pipeline",
80
+ "samples_by_category",
81
+ "to_jsonable",
82
+ "tokenize",
83
+ "write_rows",
84
+ ]
@@ -0,0 +1,341 @@
1
+ """Pydantic AST for d2ql: expressions, pipeline stages, sources, sinks, and library definitions.
2
+
3
+ Every node is a frozen `BaseModel` tagged with a `kind` discriminator so the recursive
4
+ expression and stage unions validate and round-trip without a separate transform layer.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated, Literal
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ # --------------------------------------------------------------------------- expressions
14
+
15
+
16
+ class _Node(BaseModel):
17
+ """Common config for every AST node."""
18
+
19
+ model_config = ConfigDict(frozen=True)
20
+
21
+
22
+ class LiteralExpr(_Node):
23
+ """A literal value: string, number, boolean, date/datetime, or null."""
24
+
25
+ kind: Literal["literal"] = "literal"
26
+ value: str | int | float | bool | None
27
+ literal_type: Literal["string", "integer", "decimal", "boolean", "date", "datetime", "null"]
28
+
29
+
30
+ class NameExpr(_Node):
31
+ """A bare identifier — a navigation root or a leading member name (e.g. `name`, `categoryCombo`)."""
32
+
33
+ kind: Literal["name"] = "name"
34
+ name: str
35
+
36
+
37
+ class VariableExpr(_Node):
38
+ """A `$`-prefixed variable: `$this`, `$index`, a function parameter, or a define reference."""
39
+
40
+ kind: Literal["variable"] = "variable"
41
+ name: str
42
+
43
+
44
+ class MemberExpr(_Node):
45
+ """Member navigation `target.name` (e.g. `categoryCombo.name`)."""
46
+
47
+ kind: Literal["member"] = "member"
48
+ target: Expr
49
+ name: str
50
+
51
+
52
+ class IndexExpr(_Node):
53
+ """Indexing `target[index]` (e.g. `dataSetElements[0]`)."""
54
+
55
+ kind: Literal["index"] = "index"
56
+ target: Expr
57
+ index: Expr
58
+
59
+
60
+ class CallExpr(_Node):
61
+ """A function or method call; `target` is None for a free function like `today()`."""
62
+
63
+ kind: Literal["call"] = "call"
64
+ target: Expr | None
65
+ name: str
66
+ args: list[Expr] = Field(default_factory=list)
67
+
68
+
69
+ class UnaryExpr(_Node):
70
+ """A prefix unary operation (`-x`, `not x`)."""
71
+
72
+ kind: Literal["unary"] = "unary"
73
+ op: str
74
+ operand: Expr
75
+
76
+
77
+ class BinaryExpr(_Node):
78
+ """A binary operation (`a = b`, `a ~ b`, `a and b`, `a + b`, `a in b`)."""
79
+
80
+ kind: Literal["binary"] = "binary"
81
+ op: str
82
+ left: Expr
83
+ right: Expr
84
+
85
+
86
+ class ObjectField(_Node):
87
+ """One `key: value` entry inside an object constructor / transform template."""
88
+
89
+ name: str
90
+ value: Expr
91
+
92
+
93
+ class ObjectExpr(_Node):
94
+ """An object constructor `{ key: expr, ... }` — also the body of a `transform` stage."""
95
+
96
+ kind: Literal["object"] = "object"
97
+ fields: list[ObjectField] = Field(default_factory=list)
98
+
99
+
100
+ class ArrayExpr(_Node):
101
+ """An array constructor `[ expr, ... ]`."""
102
+
103
+ kind: Literal["array"] = "array"
104
+ items: list[Expr] = Field(default_factory=list)
105
+
106
+
107
+ Expr = Annotated[
108
+ LiteralExpr
109
+ | NameExpr
110
+ | VariableExpr
111
+ | MemberExpr
112
+ | IndexExpr
113
+ | CallExpr
114
+ | UnaryExpr
115
+ | BinaryExpr
116
+ | ObjectExpr
117
+ | ArrayExpr,
118
+ Field(discriminator="kind"),
119
+ ]
120
+
121
+ # --------------------------------------------------------------------------- sources / stages / sinks
122
+
123
+
124
+ class NameSource(_Node):
125
+ """A pipeline source named by identifier — a DHIS2 resource or a `define` reference.
126
+
127
+ `inline_filter` carries an optional `[predicate]` retrieve shorthand
128
+ (e.g. `dataElements[domainType = AGGREGATE]`).
129
+ """
130
+
131
+ kind: Literal["name"] = "name"
132
+ name: str
133
+ inline_filter: Expr | None = None
134
+
135
+
136
+ class ReadSource(_Node):
137
+ """A pipeline source reading items from a JSON/NDJSON file: `read("path.json")`."""
138
+
139
+ kind: Literal["read"] = "read"
140
+ path: str
141
+
142
+
143
+ class ExprSource(_Node):
144
+ """A pipeline source that is a scalar expression (e.g. `define Total: 1 + 2`)."""
145
+
146
+ kind: Literal["expr"] = "expr"
147
+ expr: Expr
148
+
149
+
150
+ class CallSource(_Node):
151
+ """A source written as a named call with keyword arguments (e.g. `analytics(dx: "...", pe: "...")`)."""
152
+
153
+ kind: Literal["call"] = "call"
154
+ name: str
155
+ args: list[ObjectField] = Field(default_factory=list)
156
+
157
+
158
+ Source = Annotated[NameSource | ReadSource | ExprSource | CallSource, Field(discriminator="kind")]
159
+
160
+
161
+ class SelectItem(_Node):
162
+ """One projected column in a `select` stage, with an optional `as` alias."""
163
+
164
+ expr: Expr
165
+ alias: str | None = None
166
+
167
+
168
+ class OrderKey(_Node):
169
+ """One sort key in an `order` stage."""
170
+
171
+ expr: Expr
172
+ descending: bool = False
173
+
174
+
175
+ class WhereStage(_Node):
176
+ """Filter the stream to items where the predicate is truthy."""
177
+
178
+ kind: Literal["where"] = "where"
179
+ predicate: Expr
180
+
181
+
182
+ class SelectStage(_Node):
183
+ """Project each item to the listed expressions, keyed by name or alias."""
184
+
185
+ kind: Literal["select"] = "select"
186
+ items: list[SelectItem]
187
+
188
+
189
+ class TransformStage(_Node):
190
+ """Reshape each item via the template — an object literal `{ … }` or an expression yielding one."""
191
+
192
+ kind: Literal["transform"] = "transform"
193
+ template: Expr
194
+
195
+
196
+ class OrderStage(_Node):
197
+ """Sort the stream by one or more keys."""
198
+
199
+ kind: Literal["order"] = "order"
200
+ keys: list[OrderKey]
201
+
202
+
203
+ class LimitStage(_Node):
204
+ """Keep at most `count` items."""
205
+
206
+ kind: Literal["limit"] = "limit"
207
+ count: int
208
+
209
+
210
+ class SkipStage(_Node):
211
+ """Drop the first `count` items."""
212
+
213
+ kind: Literal["skip"] = "skip"
214
+ count: int
215
+
216
+
217
+ class CountStage(_Node):
218
+ """Replace the stream with a single integer: its length."""
219
+
220
+ kind: Literal["count"] = "count"
221
+
222
+
223
+ class AggregateStage(_Node):
224
+ """Group rows by a key expression and reduce each group with aggregate expressions.
225
+
226
+ `group by <group> { total: sum(value), n: count() }` — each aggregation expression is
227
+ evaluated against the group's rows, so `value` gathers that field across the group.
228
+ """
229
+
230
+ kind: Literal["aggregate"] = "aggregate"
231
+ group: Expr
232
+ aggregations: ObjectExpr
233
+
234
+
235
+ class FoldStage(_Node):
236
+ """Collapse the whole stream into a single object (e.g. a FHIR Bundle or GeoJSON FeatureCollection).
237
+
238
+ `fold { resourceType: "Bundle", entry: $rows }` — the template is built once with the entire
239
+ stream in focus; `$rows` is the stream as a list, and aggregate/`select` functions see all rows.
240
+ """
241
+
242
+ kind: Literal["fold"] = "fold"
243
+ template: ObjectExpr
244
+
245
+
246
+ Stage = Annotated[
247
+ WhereStage
248
+ | SelectStage
249
+ | TransformStage
250
+ | OrderStage
251
+ | LimitStage
252
+ | SkipStage
253
+ | CountStage
254
+ | AggregateStage
255
+ | FoldStage,
256
+ Field(discriminator="kind"),
257
+ ]
258
+
259
+
260
+ class StdoutSink(_Node):
261
+ """Sink rendering the result to stdout; `format` (from `as`) picks json/ndjson/csv, else the default."""
262
+
263
+ kind: Literal["stdout"] = "stdout"
264
+ format: Literal["json", "ndjson", "csv"] | None = None
265
+
266
+
267
+ class FileSink(_Node):
268
+ """Sink writing the result to a file; format inferred from extension when None."""
269
+
270
+ kind: Literal["file"] = "file"
271
+ path: str
272
+ format: Literal["json", "ndjson", "csv"] | None = None
273
+
274
+
275
+ Sink = Annotated[StdoutSink | FileSink, Field(discriminator="kind")]
276
+
277
+
278
+ class Pipeline(_Node):
279
+ """A source feeding a chain of stages, optionally ending in a sink."""
280
+
281
+ source: Source
282
+ stages: list[Stage] = Field(default_factory=list)
283
+ sink: Sink | None = None
284
+
285
+
286
+ # --------------------------------------------------------------------------- library
287
+
288
+
289
+ class Define(_Node):
290
+ """A named query definition: `define NAME: <pipeline>`."""
291
+
292
+ kind: Literal["define"] = "define"
293
+ name: str
294
+ body: Pipeline
295
+
296
+
297
+ class DefineFunction(_Node):
298
+ """A named function definition: `define function NAME(params): <expr>`."""
299
+
300
+ kind: Literal["define_function"] = "define_function"
301
+ name: str
302
+ params: list[str] = Field(default_factory=list)
303
+ body: Expr
304
+
305
+
306
+ Definition = Annotated[Define | DefineFunction, Field(discriminator="kind")]
307
+
308
+
309
+ class Library(_Node):
310
+ """A whole d2ql program: zero or more definitions plus an optional terminal pipeline."""
311
+
312
+ definitions: list[Definition] = Field(default_factory=list)
313
+ terminal: Pipeline | None = None
314
+
315
+
316
+ for _model in (
317
+ MemberExpr,
318
+ IndexExpr,
319
+ CallExpr,
320
+ UnaryExpr,
321
+ BinaryExpr,
322
+ ObjectField,
323
+ ObjectExpr,
324
+ ArrayExpr,
325
+ NameSource,
326
+ ExprSource,
327
+ CallSource,
328
+ SelectItem,
329
+ OrderKey,
330
+ WhereStage,
331
+ SelectStage,
332
+ TransformStage,
333
+ OrderStage,
334
+ AggregateStage,
335
+ FoldStage,
336
+ Pipeline,
337
+ Define,
338
+ DefineFunction,
339
+ Library,
340
+ ):
341
+ _model.model_rebuild()
@@ -0,0 +1,10 @@
1
+ """d2path: a path + expression language with collection semantics.
2
+
3
+ d2path is the expression sub-language embedded in d2ql (used inside `where`, `select`, `order`,
4
+ and `transform`). It navigates plain dicts (any JSON, including FHIR) or pydantic models (DHIS2 wire
5
+ models) uniformly and evaluates a documented set of operators and functions.
6
+ """
7
+
8
+ from dhis2w_ql.d2path.evaluator import EvalContext, Evaluator, Resolver
9
+
10
+ __all__ = ["EvalContext", "Evaluator", "Resolver"]