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.
- dhis2w_ql-0.99.1/PKG-INFO +125 -0
- dhis2w_ql-0.99.1/README.md +115 -0
- dhis2w_ql-0.99.1/pyproject.toml +21 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/__init__.py +84 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/ast.py +341 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/d2path/__init__.py +10 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/d2path/evaluator.py +342 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/d2path/functions.py +382 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/d2path/values.py +111 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/doc_examples.py +1251 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/engine/__init__.py +24 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/engine/datasource.py +80 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/engine/executor.py +446 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/engine/plan.py +72 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/engine/planner.py +207 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/errors.py +39 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/generate.py +255 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/parser.py +561 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/py.typed +0 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/samples.py +557 -0
- dhis2w_ql-0.99.1/src/dhis2w_ql/tokenizer.py +259 -0
|
@@ -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"]
|