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 ADDED
@@ -0,0 +1,3 @@
1
+ """Lintel: experimental GraphQL projection from UMF. See release limitations."""
2
+
3
+ __version__ = "0.1.0a1"
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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any