Skip to content

Commit b8ca2fe

Browse files
Refactor model discovery and introduce tags.
1 parent 75e15ad commit b8ca2fe

19 files changed

Lines changed: 533 additions & 243 deletions

File tree

README.pydantic.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,26 +149,26 @@ Registration is done in the `[project.entry-points."overture.models"]` section:
149149

150150
```toml
151151
[project.entry-points."overture.models"]
152-
"buildings.building" = "overture.schema.buildings.building.models:Building"
153-
"buildings.building_part" = "overture.schema.buildings.building_part.models:BuildingPart"
152+
building = "overture.schema.buildings.building.models:Building"
153+
building_part = "overture.schema.buildings.building_part.models:BuildingPart"
154154
```
155155

156156
The discovery system provides programmatic access to registered models:
157157

158158
```python
159-
from overture.schema.core.discovery import discover_models, get_registered_model
159+
from overture.schema.system.discovery import discover_models, get_registered_model
160160

161161
# Discover all registered models
162162
all_models = discover_models()
163163
# Returns:
164164
# {
165-
# ("buildings", "building"): BuildingModel,
166-
# ("places", "place"): PlaceModel,
165+
# ("building", "acme:Building", {"building_tag"}): BuildingModel,
166+
# ("place", "acme:Place", {"place_tag"}): PlaceModel,
167167
# ...
168168
# }
169169

170-
# Get a specific model by theme and type
171-
building_model = get_registered_model("buildings", "building")
170+
# Get a specific model by type
171+
building_model = get_registered_model("building")
172172
if building_model:
173173
# Use the model class
174174
building = building_model.model_validate(building_data)

packages/overture-schema-addresses-theme/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,4 @@ pythonpath = ["src"]
3737
testpaths = ["tests"]
3838

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

packages/overture-schema-annex/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ path = "src/overture/schema/__about__.py"
2929
packages = ["src/overture"]
3030

3131
[project.entry-points."overture.models"]
32-
"annex:sources" = "overture.schema.annex:Sources"
32+
sources = "overture.schema.annex:Sources"
3333

3434
[tool.pytest.ini_options]
3535
pythonpath = ["src"]

packages/overture-schema-base-theme/pyproject.toml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ path = "src/overture/schema/base/__about__.py"
3535
packages = ["src/overture"]
3636

3737
[project.entry-points."overture.models"]
38-
"overture:base:bathymetry" = "overture.schema.base:Bathymetry"
39-
"overture:base:infrastructure" = "overture.schema.base:Infrastructure"
40-
"overture:base:land" = "overture.schema.base:Land"
41-
"overture:base:land_cover" = "overture.schema.base:LandCover"
42-
"overture:base:land_use" = "overture.schema.base:LandUse"
43-
"overture:base:water" = "overture.schema.base:Water"
38+
bathymetry = "overture.schema.base:Bathymetry"
39+
infrastructure = "overture.schema.base:Infrastructure"
40+
land = "overture.schema.base:Land"
41+
land_cover = "overture.schema.base:LandCover"
42+
land_use = "overture.schema.base:LandUse"
43+
water = "overture.schema.base:Water"

packages/overture-schema-buildings-theme/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,5 @@ path = "src/overture/schema/buildings/__about__.py"
3535
packages = ["src/overture"]
3636

3737
[project.entry-points."overture.models"]
38-
"overture:buildings:building" = "overture.schema.buildings:Building"
39-
"overture:buildings:building_part" = "overture.schema.buildings:BuildingPart"
38+
building = "overture.schema.buildings:Building"
39+
building_part = "overture.schema.buildings:BuildingPart"

packages/overture-schema-cli/src/overture/schema/cli/commands.py

Lines changed: 81 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,20 @@
1313
import yaml
1414
from pydantic import BaseModel, Field, Tag, TypeAdapter, ValidationError
1515
from rich.console import Console
16+
from rich.text import Text
1617
from yamlcore import CoreLoader # type: ignore
1718

1819
from overture.schema.core import OvertureFeature
19-
from overture.schema.core.discovery import ModelKey, discover_models
20+
from overture.schema.system.discovery import ModelKey, discover_models, tags_by_key
2021
from overture.schema.system.feature import Feature
2122
from overture.schema.system.json_schema import json_schema
2223

23-
from .docstrings import get_model_docstring, get_theme_module_docstring
2424
from .error_formatting import (
2525
format_validation_error,
2626
format_validation_errors_verbose,
2727
group_errors_by_discriminator,
2828
select_most_likely_errors,
2929
)
30-
from .output import rewrap
3130
from .type_analysis import StructuralTuple, get_item_index, introspect_union
3231
from .types import ErrorLocation, ModelDict, UnionType
3332

@@ -207,34 +206,40 @@ def resolve_types(
207206
-------
208207
Model type suitable for passing to parse_feature
209208
"""
210-
# Determine effective namespace
211-
effective_namespace = "overture" if use_overture_types else namespace
212-
213209
# Discover models once with the appropriate namespace
214-
all_models = discover_models(namespace=effective_namespace)
210+
all_models = discover_models()
215211

216212
# Filter models based on CLI options
217213
filtered_models: ModelDict = {}
218214

215+
if namespace and namespace != "overture":
216+
filtered_models = {
217+
key: model_class
218+
for key, model_class in all_models.items()
219+
if namespace in key.tags
220+
}
221+
219222
if use_overture_types:
220-
filtered_models = all_models
223+
for key, model_class in all_models.items():
224+
if tags_by_key(key.tags, "overture:theme"):
225+
filtered_models[key] = model_class
221226

222227
elif theme_names and not type_names:
223228
# Theme-only mode: all types in specified themes
224229
for key, model_class in all_models.items():
225-
if key.theme in theme_names:
230+
if next(iter(tags_by_key(key.tags, "overture:theme")),None) in theme_names:
226231
filtered_models[key] = model_class
227232

228233
elif type_names and not theme_names:
229234
# Type-only mode: find matching types across all themes
230235
for key, model_class in all_models.items():
231-
if key.type in type_names:
236+
if key.name in type_names and tags_by_key(key.tags, "overture:theme"):
232237
filtered_models[key] = model_class
233238

234239
elif type_names and theme_names:
235240
# Both specified: find matching types within specified themes
236241
for key, model_class in all_models.items():
237-
if key.theme in theme_names and key.type in type_names:
242+
if key.name in type_names and next(iter(tags_by_key(key.tags, "overture:theme")),None) in theme_names:
238243
filtered_models[key] = model_class
239244

240245
else:
@@ -767,49 +772,28 @@ def json_schema_command(
767772
raise click.UsageError(str(e)) from e
768773

769774

770-
def dump_namespace(
771-
theme_types: dict[str | None, list[tuple[ModelKey, type[BaseModel]]]],
772-
) -> None:
773-
"""Print all themes and types for a namespace.
774-
775-
Displays themes in alphabetical order with their types and docstrings.
776-
Each type includes its model class name and description.
777-
778-
Args
779-
----
780-
theme_types : dict[str | None, list[tuple[ModelKey, type[BaseModel]]]]
781-
Dict mapping theme name to list of (ModelKey, model_class) tuples
782-
"""
783-
for theme in sorted(theme_types.keys(), key=lambda x: (x is None, x)):
784-
if theme:
785-
stdout.print(
786-
f"[bold green underline]{theme.upper()}[/bold green underline]"
787-
)
788-
789-
theme_docstring = get_theme_module_docstring(theme)
790-
if theme_docstring:
791-
stdout.print(
792-
rewrap(theme_docstring, stdout, padding_right=4), style="dim"
793-
)
794-
795-
stdout.print()
796-
797-
# Add types to the tree
798-
sorted_types = sorted(theme_types[theme], key=lambda x: x[0].type)
799-
for key, model_class in sorted_types:
800-
stdout.print(
801-
f" [bright_black]→[/bright_black] [bold cyan]{key.type}[/bold cyan] [dim magenta]({key.class_name})[/dim magenta]"
802-
)
803-
docstring = get_model_docstring(model_class)
804-
if docstring:
805-
stdout.print(
806-
rewrap(docstring, stdout, indent=4, padding_right=12), style="dim"
807-
)
808-
stdout.print()
809-
810-
811775
@cli.command("list-types")
812-
def list_types() -> None:
776+
@click.option(
777+
"--tag",
778+
"tags",
779+
multiple=True,
780+
help="Filter types by tag (e.g., overture:theme=addresses)",
781+
)
782+
@click.option(
783+
"--exclude-tag",
784+
"excluded_tags",
785+
multiple=True,
786+
help="Filter types by tag (e.g., overture:theme=base)",
787+
)
788+
@click.option(
789+
"--group-by",
790+
help="Group types by tag prefix (e.g., 'overture:theme')",
791+
)
792+
def list_types(
793+
tags: tuple[str, ...],
794+
excluded_tags: tuple[str, ...],
795+
group_by: str | None
796+
) -> None:
813797
r"""List all available types grouped by theme with descriptions.
814798
815799
Displays all registered Overture Maps types organized by theme,
@@ -822,35 +806,51 @@ def list_types() -> None:
822806
"""
823807
try:
824808
models = discover_models()
809+
filters = []
810+
811+
if tags:
812+
filters.append(lambda key: all(tag in key.tags for tag in tags))
813+
if excluded_tags:
814+
filters.append(lambda key: not any(tag in key.tags for tag in excluded_tags))
815+
816+
if filters:
817+
models = {
818+
key: model
819+
for key, model in models.items()
820+
if all(f(key) for f in filters)
821+
}
822+
823+
if group_by:
824+
grouped_models: dict[str, set[ModelKey]] = {}
825+
826+
for key, model_class in models.items():
827+
if groups := tags_by_key(key.tags, group_by):
828+
for group in groups:
829+
grouped_models.setdefault(group, set()).add(key)
830+
831+
padding = max((len(key.name) for keys in grouped_models.values() for key in keys), default=0) + 2
832+
833+
for group, keys in sorted(grouped_models.items()):
834+
stdout.print(f"[green bold]{group_by}={group} ({len(keys)})[/green bold]")
835+
for key in sorted(keys, key=lambda k: k.name):
836+
model = Text()
837+
model.append("→ ", style="bright_black")
838+
model.append(key.name, style="bold cyan")
839+
model.pad_right(max(1, padding - len(key.name)))
840+
model.append_text(Text().append(" ".join(sorted(key.tags))))
841+
stdout.print(model)
842+
stdout.print()
843+
844+
else:
845+
padding = max((len(key.name) for key in models.keys()), default=0) + 2
846+
847+
for key in sorted(models.keys(), key=lambda k: k.name):
848+
model = Text()
849+
model.append(key.name, style="bold cyan")
850+
model.pad_right(max(1, padding - len(key.name)))
851+
model.append_text(Text().append(" ".join(sorted(key.tags))))
852+
stdout.print(model)
825853

826-
# Group models by namespace and theme
827-
namespaces: dict[
828-
str, dict[str | None, list[tuple[ModelKey, type[BaseModel]]]]
829-
] = {}
830-
for key, model_class in models.items():
831-
if key.namespace not in namespaces:
832-
namespaces[key.namespace] = {}
833-
if key.theme not in namespaces[key.namespace]:
834-
namespaces[key.namespace][key.theme] = []
835-
836-
namespaces[key.namespace][key.theme].append((key, model_class))
837-
838-
# display Overture themes first
839-
if "overture" in namespaces:
840-
stdout.print("[bold red]OVERTURE THEMES[/bold red]", justify="center")
841-
stdout.print()
842-
843-
dump_namespace(namespaces["overture"])
844-
845-
stdout.print("[bold red]ADDITIONAL TYPES[/bold red]", justify="center")
846-
stdout.print()
847-
848-
for namespace in sorted(namespaces.keys()):
849-
if namespace == "overture":
850-
continue
851-
852-
stdout.print(f"[bold blue]{namespace.upper()}[/bold blue]")
853-
dump_namespace(namespaces[namespace])
854854

855855
except Exception as e:
856856
click.echo(f"Error listing types: {e}", err=True)

packages/overture-schema-cli/src/overture/schema/cli/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from pydantic import BaseModel
66
from pydantic_core import ErrorDetails
77

8-
from overture.schema.core.discovery import ModelKey
8+
from overture.schema.system.discovery import ModelKey
99

1010
# Type alias for union types created from Pydantic models
1111
# This represents either a single model or a discriminated union of models

packages/overture-schema-cli/tests/test_resolve_types.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import pytest
44
from overture.schema.cli.commands import resolve_types
5+
from overture.schema.system.discovery import tags_by_key
56

67

78
class TestResolveTypes:
@@ -124,10 +125,10 @@ def test_resolve_types_returns_expected_themes(
124125
expected_themes: set[str],
125126
) -> None:
126127
"""Test that resolve_types returns models from expected themes."""
127-
from overture.schema.core.discovery import discover_models
128+
from overture.schema.system.discovery import discover_models
128129

129-
models = discover_models(namespace=namespace)
130-
actual_themes = {key.theme for key in models.keys()}
130+
models = discover_models()
131+
actual_themes = {next(iter(tags_by_key(key.tags, "overture:theme")),None) for key in models.keys()}
131132

132133
# Check that we have at least the expected themes (may have more)
133134
assert expected_themes.issubset(actual_themes), (

packages/overture-schema-core/pyproject.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,8 @@ dev = [
3636
"types-pyyaml>=6.0.12.20250516",
3737
"types-shapely>=2.1.0.20250710",
3838
]
39+
40+
[project.entry-points."overture.tag_providers"]
41+
overture = "overture.schema.core.tag_providers:overture_provider"
42+
authority = "overture.schema.core.tag_providers:authority_provider"
43+
theme = "overture.schema.core.tag_providers:theme_provider"

0 commit comments

Comments
 (0)