lintel-api 0.1.0a1__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.
- lintel/__init__.py +3 -0
- lintel/experimental/__init__.py +0 -0
- lintel/experimental/projection.py +121 -0
- lintel/experimental/schema.py +105 -0
- lintel/py.typed +0 -0
- lintel_api-0.1.0a1.dist-info/METADATA +83 -0
- lintel_api-0.1.0a1.dist-info/RECORD +8 -0
- lintel_api-0.1.0a1.dist-info/WHEEL +4 -0
lintel/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""S-01: read UMF itself, project a deliberately small scalar-only subset.
|
|
2
|
+
|
|
3
|
+
Not an alternative UMF validator: core validity/completeness belongs to umf-core.
|
|
4
|
+
This spike has no production activation path and no executor.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
# UMF generates Document dynamically; confine its incomplete static annotations here.
|
|
15
|
+
from umf import read_document, validate_document # pyright: ignore[reportUnknownVariableType]
|
|
16
|
+
|
|
17
|
+
Ref = tuple[str, str]
|
|
18
|
+
RESERVED = {"Node", "Query", "Mutation", "PageInfo", "Decimal", "UUID", "PresenceProbe"}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProjectionError(ValueError):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(frozen=True)
|
|
26
|
+
class Field:
|
|
27
|
+
source: Ref
|
|
28
|
+
name: str
|
|
29
|
+
scalar: str
|
|
30
|
+
optional: bool
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class Projection:
|
|
35
|
+
source_text: str
|
|
36
|
+
record: Ref
|
|
37
|
+
name: str
|
|
38
|
+
fields: tuple[Field, ...]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _name(name: str) -> None:
|
|
42
|
+
if not re.fullmatch(r"[_A-Za-z][_0-9A-Za-z]*", name) or name.startswith("__"):
|
|
43
|
+
raise ProjectionError("NAME_COLLISION: invalid or reserved GraphQL name")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def project(source_text: str, record: Ref, name: str, fields: dict[Ref, str]) -> Projection:
|
|
47
|
+
index = _read_index(source_text)
|
|
48
|
+
_name(name)
|
|
49
|
+
if name in RESERVED or name + "Input" in RESERVED:
|
|
50
|
+
raise ProjectionError("NAME_COLLISION: generated or reserved type")
|
|
51
|
+
owner = index.get(record)
|
|
52
|
+
if owner is None or owner["kind"] != "record":
|
|
53
|
+
raise ProjectionError("INVALID_EXPOSURE: expected UMF record")
|
|
54
|
+
_subset(owner, {"id", "name", "kind", "members", "keys", "extensions"})
|
|
55
|
+
members = {(ref["module"], ref["element"]) for ref in owner["members"]}
|
|
56
|
+
projected: list[Field] = []
|
|
57
|
+
names: set[str] = set()
|
|
58
|
+
for ref, api_name in fields.items():
|
|
59
|
+
_name(api_name)
|
|
60
|
+
if api_name in names or api_name == "id":
|
|
61
|
+
raise ProjectionError("NAME_COLLISION: duplicate or Relay id field")
|
|
62
|
+
names.add(api_name)
|
|
63
|
+
field = index.get(ref)
|
|
64
|
+
if ref not in members or field is None or field["kind"] != "field":
|
|
65
|
+
raise ProjectionError("INVALID_EXPOSURE: field is not a record member")
|
|
66
|
+
_subset(
|
|
67
|
+
field,
|
|
68
|
+
{
|
|
69
|
+
"id",
|
|
70
|
+
"name",
|
|
71
|
+
"kind",
|
|
72
|
+
"scalarType",
|
|
73
|
+
"cardinality",
|
|
74
|
+
"nullability",
|
|
75
|
+
"extensions",
|
|
76
|
+
"facets",
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
_check_scalar(field)
|
|
80
|
+
projected.append(
|
|
81
|
+
Field(ref, api_name, field["scalarType"], field["nullability"] == "absent-allowed")
|
|
82
|
+
)
|
|
83
|
+
if not projected:
|
|
84
|
+
raise ProjectionError("INVALID_EXPOSURE: empty selection")
|
|
85
|
+
return Projection(source_text, record, name, tuple(sorted(projected, key=lambda f: f.name)))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _check_scalar(field: dict[str, Any]) -> None:
|
|
89
|
+
if field.get("scalarType") not in {"string", "boolean", "decimal", "uuid"}:
|
|
90
|
+
raise ProjectionError("UNSUPPORTED: scalar outside spike subset")
|
|
91
|
+
if field.get("cardinality") != "one":
|
|
92
|
+
raise ProjectionError("UNSUPPORTED: cardinality outside spike subset")
|
|
93
|
+
if field.get("nullability") not in {"required", "absent-allowed"}:
|
|
94
|
+
raise ProjectionError("UNSUPPORTED: availability outside spike subset")
|
|
95
|
+
if field.get("facets", {}) and (
|
|
96
|
+
field["scalarType"] != "decimal" or set(field["facets"]) - {"precision", "scale"}
|
|
97
|
+
):
|
|
98
|
+
raise ProjectionError("UNSUPPORTED: facet outside spike subset")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_index(source_text: str) -> dict[Ref, dict[str, Any]]:
|
|
102
|
+
read_public = cast(Callable[[str], Any], read_document)
|
|
103
|
+
document = read_public(source_text)
|
|
104
|
+
validation = validate_document(document)
|
|
105
|
+
if not validation.valid or not validation.complete:
|
|
106
|
+
raise ProjectionError("UNSUPPORTED: UMF validation is invalid or incomplete")
|
|
107
|
+
# Keep generated UMF types at this public parsing boundary; no private imports.
|
|
108
|
+
data = cast(dict[str, Any], document.to_dict())
|
|
109
|
+
index: dict[Ref, dict[str, Any]] = {}
|
|
110
|
+
for module in data["modules"]:
|
|
111
|
+
for element in module["elements"]:
|
|
112
|
+
ref = (module["id"], element["id"])
|
|
113
|
+
if ref in index:
|
|
114
|
+
raise ProjectionError("AMBIGUOUS_IDENTITY: duplicate UMF reference")
|
|
115
|
+
index[ref] = element
|
|
116
|
+
return index
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _subset(element: dict[str, Any], supported: set[str]) -> None:
|
|
120
|
+
if element.get("extensions") or set(element) - supported:
|
|
121
|
+
raise ProjectionError("UNSUPPORTED: selected meaning outside spike subset")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Dynamic Strawberry construction from a direct UMF scalar projection.
|
|
2
|
+
|
|
3
|
+
The query and echo mutation are laboratory probes, not admitted UMF operations.
|
|
4
|
+
They must never be mounted as a production API.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from typing import Any, NewType
|
|
12
|
+
from uuid import UUID
|
|
13
|
+
|
|
14
|
+
import strawberry
|
|
15
|
+
from graphql import lexicographic_sort_schema, print_schema
|
|
16
|
+
from strawberry.schema.config import StrawberryConfig
|
|
17
|
+
|
|
18
|
+
from lintel.experimental.projection import Projection
|
|
19
|
+
|
|
20
|
+
DecimalText = NewType("DecimalText", str)
|
|
21
|
+
UUIDText = NewType("UUIDText", str)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def decimal_text(value: object) -> DecimalText:
|
|
25
|
+
if not isinstance(value, str) or not re.fullmatch(
|
|
26
|
+
r"-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?", value
|
|
27
|
+
):
|
|
28
|
+
raise ValueError("Decimal requires exact finite JSON-number text")
|
|
29
|
+
return DecimalText(value)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def uuid_text(value: object) -> UUIDText:
|
|
33
|
+
if not isinstance(value, str) or str(UUID(value)) != value:
|
|
34
|
+
raise ValueError("UUID requires canonical lowercase hyphenated text")
|
|
35
|
+
return UUIDText(value)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SCALARS: dict[str, Any] = {
|
|
39
|
+
"string": str,
|
|
40
|
+
"boolean": bool,
|
|
41
|
+
"decimal": DecimalText,
|
|
42
|
+
"uuid": UUIDText,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_probe_schema(plan: Projection, row: Mapping[str, object]) -> strawberry.Schema:
|
|
47
|
+
"""No native I/O or executable imports selected by UMF content."""
|
|
48
|
+
annotations: dict[str, Any] = {}
|
|
49
|
+
input_body: dict[str, Any] = {"__annotations__": {}}
|
|
50
|
+
output_body: dict[str, Any] = {"__annotations__": annotations}
|
|
51
|
+
slots = [("field_" + str(i), field) for i, field in enumerate(plan.fields)]
|
|
52
|
+
for slot, field in slots:
|
|
53
|
+
value_type = SCALARS[field.scalar]
|
|
54
|
+
annotation = value_type | None if field.optional else value_type
|
|
55
|
+
annotations[slot] = annotation
|
|
56
|
+
input_body["__annotations__"][slot] = annotation
|
|
57
|
+
output_body[slot] = strawberry.field(name=field.name)
|
|
58
|
+
input_body[slot] = (
|
|
59
|
+
strawberry.field(name=field.name, default=strawberry.UNSET)
|
|
60
|
+
if field.optional
|
|
61
|
+
else strawberry.field(name=field.name)
|
|
62
|
+
)
|
|
63
|
+
output_type = strawberry.type(type(plan.name, (), output_body))
|
|
64
|
+
input_type = strawberry.input(type(plan.name + "Input", (), input_body))
|
|
65
|
+
|
|
66
|
+
def probe() -> Any:
|
|
67
|
+
return output_type(
|
|
68
|
+
**{slot: row.get(f.name) if f.optional else row[f.name] for slot, f in slots}
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def echo(input: Any) -> PresenceProbe:
|
|
72
|
+
# Omission remains distinct from explicit null before the executor boundary.
|
|
73
|
+
return PresenceProbe(
|
|
74
|
+
omitted=any(getattr(input, slot) is strawberry.UNSET for slot, _ in slots),
|
|
75
|
+
explicit_null=any(getattr(input, slot) is None for slot, _ in slots),
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
probe.__annotations__["return"] = output_type
|
|
79
|
+
echo.__annotations__["input"] = input_type
|
|
80
|
+
query = strawberry.type(type("Query", (), {"probe": strawberry.field(resolver=probe)}))
|
|
81
|
+
mutation = strawberry.type(type("Mutation", (), {"echo": strawberry.mutation(resolver=echo)}))
|
|
82
|
+
return strawberry.Schema(
|
|
83
|
+
query,
|
|
84
|
+
mutation=mutation,
|
|
85
|
+
config=StrawberryConfig(auto_camel_case=False),
|
|
86
|
+
scalar_overrides={
|
|
87
|
+
DecimalText: strawberry.scalar(
|
|
88
|
+
name="Decimal", serialize=decimal_text, parse_value=decimal_text
|
|
89
|
+
),
|
|
90
|
+
UUIDText: strawberry.scalar(name="UUID", serialize=uuid_text, parse_value=uuid_text),
|
|
91
|
+
},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@strawberry.type
|
|
96
|
+
class PresenceProbe:
|
|
97
|
+
omitted: bool
|
|
98
|
+
explicit_null: bool
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def export_sdl(schema: strawberry.Schema) -> str:
|
|
102
|
+
# Public graphql-core schema construction from public Strawberry SDL.
|
|
103
|
+
from graphql import build_schema
|
|
104
|
+
|
|
105
|
+
return print_schema(lexicographic_sort_schema(build_schema(schema.as_str()))).rstrip() + "\n"
|
lintel/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: lintel-api
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: UMF-driven GraphQL interoperability spikes (not a production runtime)
|
|
5
|
+
Project-URL: Repository, https://github.com/DocumentDrivenDX/lintel
|
|
6
|
+
Project-URL: Issues, https://github.com/DocumentDrivenDX/lintel/issues
|
|
7
|
+
Classifier: Development Status :: 3 - Alpha
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Requires-Dist: graphql-core==3.3.1
|
|
13
|
+
Requires-Dist: strawberry-graphql==0.339.1
|
|
14
|
+
Requires-Dist: umf-core==0.10.0
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# Lintel
|
|
18
|
+
|
|
19
|
+
Dynamic GraphQL + Relay APIs from UMF, with an embeddable Python/FastAPI runtime
|
|
20
|
+
and generated TypeScript operation/fragment artifacts for Batten applications.
|
|
21
|
+
|
|
22
|
+
## Install the experimental alpha
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pip install lintel-api==0.1.0a1
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Import `project` from `lintel.experimental.projection` and
|
|
29
|
+
`build_probe_schema` / `export_sdl` from `lintel.experimental.schema`.
|
|
30
|
+
These expose the bounded S-01 scalar projection: string, boolean, exact decimal
|
|
31
|
+
text and canonical UUID, with optional-field omission preserved. Supply UMF JSON
|
|
32
|
+
and explicit record/field references; the example below uses the task-board model
|
|
33
|
+
in the source distribution.
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from lintel.experimental.projection import project
|
|
38
|
+
from lintel.experimental.schema import build_probe_schema
|
|
39
|
+
|
|
40
|
+
plan = project(
|
|
41
|
+
Path("docs/helix/02-design/examples/task-board.umf.json").read_text(),
|
|
42
|
+
("board", "Task"), "Task", {("board", "title"): "title"},
|
|
43
|
+
)
|
|
44
|
+
schema = build_probe_schema(plan, {"title": "Fabricated task"})
|
|
45
|
+
assert schema.execute_sync("{ probe { title } }").data == {
|
|
46
|
+
"probe": {"title": "Fabricated task"}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The alpha contains laboratory `probe` and `echo` roots. Do not mount them as a
|
|
51
|
+
production API. Native execution/admission, Relay identity/pagination, TypeScript
|
|
52
|
+
clients and Batten embedding are not provided by this wheel. Native qualification
|
|
53
|
+
harnesses and governed specifications are included in the source archive, without
|
|
54
|
+
installing PostgreSQL or its tools as library dependencies. Experimental APIs may
|
|
55
|
+
change between alpha releases. Python 3.11 is the initial qualified interpreter.
|
|
56
|
+
|
|
57
|
+
Lintel is a provisional name. This repository currently contains the HELIX
|
|
58
|
+
specifications and a bounded [direct UMF interoperability spike](docs/helix/02-design/spikes/SPIKE-001-direct-umf.md).
|
|
59
|
+
The spike reads UMF JSON through the public umf-core reader/validator and builds
|
|
60
|
+
a dynamic Strawberry schema. It is not a production API service.
|
|
61
|
+
|
|
62
|
+
Run `uv sync --locked --group spike --python 3.11`, then
|
|
63
|
+
`uv run --locked --group spike python -m spikes.direct_umf.run`.
|
|
64
|
+
[Verification and limits](docs/helix/04-build/direct-umf-spike.md) distinguish
|
|
65
|
+
this proof from native execution, Relay clients and Batten embedding.
|
|
66
|
+
|
|
67
|
+
Start with [the project packet](docs/helix/README.md),
|
|
68
|
+
[scope and MVP](docs/helix/01-frame/prd.md), and
|
|
69
|
+
[architecture](docs/helix/02-design/architecture.md).
|
|
70
|
+
|
|
71
|
+
The intended family documentation location is
|
|
72
|
+
[documentdrivendx.github.io/lintel](https://documentdrivendx.github.io/lintel/).
|
|
73
|
+
The [source repository](https://github.com/DocumentDrivenDX/lintel) is created;
|
|
74
|
+
family-catalog registration and documentation-site publication remain pending.
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
The [native read spike](docs/helix/02-design/spikes/SPIKE-002-native-reads.md)
|
|
78
|
+
measures pagination, declared-key batching and request consistency on disposable
|
|
79
|
+
PostgreSQL 18. Run `uv sync --locked --group spike --group native-spike`, then
|
|
80
|
+
`uv run --locked --group spike --group native-spike python -m spikes.native_reads.run`.
|
|
81
|
+
Use both dependency groups for `python scripts/check_quality.py` on the current tree.
|
|
82
|
+
[Results and gaps](docs/helix/04-build/native-read-spike.md) keep unsupported
|
|
83
|
+
execution guarantees explicit; no upstream schema API is introduced.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
lintel/__init__.py,sha256=wMUmapWR_AZgKY0svcgD-anKRrmDVn44cVdNceUyA64,106
|
|
2
|
+
lintel/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
lintel/experimental/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
lintel/experimental/projection.py,sha256=uYxe5t-oY94cgzM2IsYdSFvX90wgMDOuEqaEkY_a1iI,4556
|
|
5
|
+
lintel/experimental/schema.py,sha256=bGPg67BbR8XA9MGm0KU-KhZNQq_sOCMEUpJALvU90tk,3812
|
|
6
|
+
lintel_api-0.1.0a1.dist-info/METADATA,sha256=X80LTYQMDJxf_sFBoQH1C0TcMPrYGyQP1ETa240pFYM,3898
|
|
7
|
+
lintel_api-0.1.0a1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
8
|
+
lintel_api-0.1.0a1.dist-info/RECORD,,
|