Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8029c48
fix(core): switch to relative import
mojodna Jan 28, 2026
537b36f
fix(core): fix __name__ reference
mojodna Jan 28, 2026
cb8b8db
chore: add install make target
mojodna Jan 28, 2026
e7771dc
Remove pytest-subtests dependency
mojodna Feb 11, 2026
6f7cb5c
Quiet pytest output for dev workflow
mojodna Feb 10, 2026
abb24f5
Attach docstrings to NewTypes at runtime
mojodna Feb 11, 2026
0edb552
fix(core): add missing f-prefix to string continuation lines
mojodna Feb 25, 2026
f969ffc
fix(system): use dict instead of Mapping in test util type hints
mojodna Feb 25, 2026
b11b8c2
fix(cli): discover discriminator fields at runtime
mojodna Feb 25, 2026
b4237b5
refactor(cli): tighten type analysis contracts
mojodna Feb 25, 2026
910e128
refactor(core,cli): rename ModelKey.class_name to entry_point
mojodna Feb 25, 2026
28ce953
feat(codegen): add overture-schema-codegen package
mojodna Feb 25, 2026
35fbd31
feat(codegen): add type analysis, specs, and type registry
mojodna Feb 25, 2026
7c6a670
feat(codegen): add extraction modules
mojodna Feb 25, 2026
86ef93d
feat(codegen): add constraint description modules
mojodna Feb 25, 2026
065fef5
feat(codegen): add output layout modules
mojodna Feb 25, 2026
1e0ce22
feat(codegen): add example data to theme pyproject.toml files
mojodna Feb 25, 2026
92c656c
feat(codegen): add markdown renderers
mojodna Feb 25, 2026
3823350
feat(codegen): add CLI and integration tests
mojodna Feb 25, 2026
8b0d396
docs(codegen): add design doc, walkthrough, and README
mojodna Feb 25, 2026
75ce84c
fix(codegen): store all Literal args in TypeInfo
mojodna Feb 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

default: test-all

install: uv-sync

uv-sync:
@uv sync --all-packages 2> /dev/null

Expand All @@ -14,7 +16,7 @@ test-all: uv-sync
@uv run pytest -W error packages/

test: uv-sync
@uv run pytest -W error packages/ -x
@uv run pytest -W error packages/ -x -q --tb=short

coverage: uv-sync
@uv run pytest packages/ --cov overture.schema --cov-report=term --cov-report=html && open htmlcov/index.html
Expand Down
33 changes: 33 additions & 0 deletions packages/overture-schema-addresses-theme/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,36 @@ testpaths = ["tests"]

[project.entry-points."overture.models"]
"overture:addresses:address" = "overture.schema.addresses:Address"

[[examples.Address]]
id = "416ab01c-d836-4c4f-aedc-2f30941ce94d"
geometry = "POINT (-176.5637854 -43.9471955)"
country = "NZ"
postcode = "null"
street = "Tikitiki Hill Road"
number = "54"
unit = "null"
postal_city = "null"
version = 1
theme = "addresses"
type = "address"

[examples.Address.bbox]
xmin = -176.56381225585938
xmax = -176.56378173828125
ymin = -43.94719696044922
ymax = -43.94718933105469

[[examples.Address.address_levels]]
value = "Chatham Islands"

[[examples.Address.address_levels]]
value = "Chatham Island"

[[examples.Address.sources]]
property = ""
dataset = "OpenAddresses/LINZ"
record_id = "null"
update_time = "null"
confidence = "null"
between = "null"
207 changes: 207 additions & 0 deletions packages/overture-schema-base-theme/pyproject.toml

Large diffs are not rendered by default.

78 changes: 78 additions & 0 deletions packages/overture-schema-buildings-theme/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,81 @@ packages = ["src/overture"]
[project.entry-points."overture.models"]
"overture:buildings:building" = "overture.schema.buildings:Building"
"overture:buildings:building_part" = "overture.schema.buildings:BuildingPart"

[[examples.Building]]
id = "148f35b1-7bc1-4180-9280-10d39b13883b"
geometry = "POLYGON ((-176.6435004 -43.9938042, -176.6435738 -43.9937107, -176.6437726 -43.9937913, -176.6436992 -43.9938849, -176.6435004 -43.9938042))"
version = 1
level = "null"
subtype = "null"
class = "null"
height = "null"
names = "null"
has_parts = false
is_underground = false
num_floors = "null"
num_floors_underground = "null"
min_height = "null"
min_floor = "null"
facade_color = "null"
facade_material = "null"
roof_material = "null"
roof_shape = "null"
roof_direction = "null"
roof_orientation = "null"
roof_color = "null"
roof_height = "null"
theme = "buildings"
type = "building"

[examples.Building.bbox]
xmin = -176.643798828125
xmax = -176.64349365234375
ymin = -43.9938850402832
ymax = -43.993709564208984

[[examples.Building.sources]]
property = ""
dataset = "OpenStreetMap"
record_id = "w519166507@1"
update_time = "2017-08-27T21:39:50.000Z"
confidence = "null"
between = "null"

[[examples.BuildingPart]]
id = "19412d64-51ac-3d6a-ac2f-8a8c8b91bb60"
geometry = "POLYGON ((-73.2462509 -39.8108937, -73.2462755 -39.8109047, -73.246291 -39.8109182, -73.2463022 -39.8109382, -73.2463039 -39.810959, -73.2462962 -39.81098, -73.2462796 -39.8109977, -73.2462674 -39.8110052, -73.2462281 -39.8110153, -73.2461998 -39.811013, -73.2461743 -39.8110034, -73.2461566 -39.8109898, -73.246144 -39.8109702, -73.2461418 -39.8109427, -73.2461511 -39.8109221, -73.2461669 -39.8109066, -73.2461908 -39.8108947, -73.2462184 -39.8108898, -73.2462509 -39.8108937))"
version = 0
level = 3
height = "null"
names = "null"
is_underground = false
num_floors = "null"
num_floors_underground = "null"
min_height = "null"
min_floor = "null"
facade_color = "null"
facade_material = "null"
roof_material = "null"
roof_shape = "null"
roof_direction = "null"
roof_orientation = "null"
roof_color = "null"
roof_height = "null"
building_id = "bd663bd4-1844-4d7d-a400-114de051cf49"
theme = "buildings"
type = "building_part"

[examples.BuildingPart.bbox]
xmin = -73.24630737304688
xmax = -73.24613952636719
ymin = -39.81101608276367
ymax = -39.81088638305664

[[examples.BuildingPart.sources]]
property = ""
dataset = "OpenStreetMap"
record_id = "w223076787@2"
update_time = "2014-10-31T22:55:36.000Z"
confidence = "null"
between = "null"
Original file line number Diff line number Diff line change
Expand Up @@ -798,7 +798,7 @@ def dump_namespace(
sorted_types = sorted(theme_types[theme], key=lambda x: x[0].type)
for key, model_class in sorted_types:
stdout.print(
f" [bright_black]→[/bright_black] [bold cyan]{key.type}[/bold cyan] [dim magenta]({key.class_name})[/dim magenta]"
f" [bright_black]→[/bright_black] [bold cyan]{key.type}[/bold cyan] [dim magenta]({key.entry_point})[/dim magenta]"
)
docstring = get_model_docstring(model_class)
if docstring:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from pydantic import BaseModel
from pydantic.fields import FieldInfo

from overture.schema.system.feature import resolve_discriminator_field_name

from .types import ErrorLocation, ValidationErrorDict

# Type aliases for structural tuple elements
Expand All @@ -29,11 +31,23 @@ class UnionMetadata:
nested_unions: dict[str, "UnionMetadata"]


def _extract_literal_value(model: type[BaseModel], field_name: str) -> str | None:
"""Extract the single Literal value from a model field as a string, if present."""
field_info = model.model_fields.get(field_name)
if field_info is None or field_info.annotation is None:
return None
if get_origin(field_info.annotation) is Literal:
args = get_args(field_info.annotation)
return str(args[0]) if args else None
return None


def _process_union_member(
member: Any, # noqa: ANN401
discriminator_to_model: dict[str, type[BaseModel]],
model_name_to_model: dict[str, type[BaseModel]],
nested_unions: dict[str, UnionMetadata],
discriminator_field: str | None = None,
) -> None:
"""Process a single union member, handling nesting recursively.

Expand All @@ -43,6 +57,7 @@ def _process_union_member(
discriminator_to_model: Dict to populate with discriminator value mappings
model_name_to_model: Dict to populate with model name mappings
nested_unions: Dict to populate with nested union metadata
discriminator_field: The discriminator field name from the parent union annotation
"""
member_origin = get_origin(member)

Expand All @@ -63,30 +78,35 @@ def _process_union_member(
nested_metadata = introspect_union(member)
nested_unions[str(member)] = nested_metadata
discriminator_to_model.update(nested_metadata.discriminator_to_model)
# The nested union's discriminator_to_model uses the nested discriminator
# field (e.g. "subtype"). Re-extract using the parent discriminator field
# (e.g. "type") so leaf models are also reachable by the parent's values.
if discriminator_field is not None:
for model in nested_metadata.model_name_to_model.values():
value = _extract_literal_value(model, discriminator_field)
if value is not None:
discriminator_to_model[value] = model
return

# Unwrap Annotated to get the actual type (e.g., Annotated[Building, Tag('building')])
# and process it recursively
_process_union_member(
member_args[0], discriminator_to_model, model_name_to_model, nested_unions
member_args[0],
discriminator_to_model,
model_name_to_model,
nested_unions,
discriminator_field,
)
return

# Case 2: BaseModel class
if inspect.isclass(member) and issubclass(member, BaseModel):
model_name_to_model[member.__name__] = member

# Extract discriminator values from known discriminator fields only
# Restrict to known discriminator names to avoid false positives from other Literal fields
discriminator_fields = ("type", "theme", "subtype")
for field_name, field_info in member.model_fields.items():
if field_name not in discriminator_fields:
continue
annotation = field_info.annotation
if get_origin(annotation) is Literal:
literal_args = get_args(annotation)
if literal_args:
discriminator_to_model[literal_args[0]] = member
if discriminator_field is not None:
value = _extract_literal_value(member, discriminator_field)
if value is not None:
discriminator_to_model[value] = member


def introspect_union(union_type: Any) -> UnionMetadata: # noqa: ANN401
Expand Down Expand Up @@ -163,9 +183,9 @@ def introspect_union(union_type: Any) -> UnionMetadata: # noqa: ANN401
if isinstance(metadata, FieldInfo) and hasattr(
metadata, "discriminator"
):
disc = metadata.discriminator
# discriminator can be a string or Discriminator object
discriminator_field = str(disc) if disc is not None else None
discriminator_field = resolve_discriminator_field_name(
metadata.discriminator
)
break

# Get union members
Expand All @@ -183,7 +203,11 @@ def introspect_union(union_type: Any) -> UnionMetadata: # noqa: ANN401
# Process each union member
for member in union_members:
_process_union_member(
member, discriminator_to_model, model_name_to_model, nested_unions
member,
discriminator_to_model,
model_name_to_model,
nested_unions,
discriminator_field,
)

return UnionMetadata(
Expand Down
Loading
Loading