dhis2w-ql 0.99.1__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.
- dhis2w_ql/__init__.py +84 -0
- dhis2w_ql/ast.py +341 -0
- dhis2w_ql/d2path/__init__.py +10 -0
- dhis2w_ql/d2path/evaluator.py +342 -0
- dhis2w_ql/d2path/functions.py +382 -0
- dhis2w_ql/d2path/values.py +111 -0
- dhis2w_ql/doc_examples.py +1251 -0
- dhis2w_ql/engine/__init__.py +24 -0
- dhis2w_ql/engine/datasource.py +80 -0
- dhis2w_ql/engine/executor.py +446 -0
- dhis2w_ql/engine/plan.py +72 -0
- dhis2w_ql/engine/planner.py +207 -0
- dhis2w_ql/errors.py +39 -0
- dhis2w_ql/generate.py +255 -0
- dhis2w_ql/parser.py +561 -0
- dhis2w_ql/py.typed +0 -0
- dhis2w_ql/samples.py +557 -0
- dhis2w_ql/tokenizer.py +259 -0
- dhis2w_ql-0.99.1.dist-info/METADATA +125 -0
- dhis2w_ql-0.99.1.dist-info/RECORD +21 -0
- dhis2w_ql-0.99.1.dist-info/WHEEL +4 -0
dhis2w_ql/__init__.py
ADDED
|
@@ -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
|
+
]
|
dhis2w_ql/ast.py
ADDED
|
@@ -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"]
|