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 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"]