Skip to content

Commit ab6a274

Browse files
refactor(codegen, cli): replace theme filtering with tag filtering in CLI commands
Signed-off-by: Roel <75250264+RoelBollens-TomTom@users.noreply.github.com>
1 parent 3e9b61b commit ab6a274

8 files changed

Lines changed: 255 additions & 283 deletions

File tree

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

Lines changed: 47 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,12 @@
1818
from yamlcore import CoreLoader # type: ignore
1919

2020
from overture.schema.core import OvertureFeature
21-
from overture.schema.system.discovery import ModelKey, discover_models, tags_by_key
21+
from overture.schema.system.discovery import (
22+
ModelKey,
23+
discover_models,
24+
filter_models,
25+
tags_by_key,
26+
)
2227
from overture.schema.system.feature import Feature
2328
from overture.schema.system.json_schema import json_schema
2429

@@ -189,72 +194,32 @@ def validate_features(data: list, model_type: UnionType) -> list[BaseModel]:
189194

190195

191196
def resolve_types(
192-
use_overture_types: bool,
193-
namespace: str | None,
194-
theme_names: tuple[str, ...],
197+
tags: tuple[str, ...],
198+
excluded_tags: tuple[str, ...],
195199
type_names: tuple[str, ...],
196200
) -> UnionType:
197201
"""Resolve CLI options into a model type suitable for parse_feature.
198202
199203
Args
200204
----
201-
use_overture_types: Boolean from --overture-types flag
202-
namespace: Namespace to filter by (e.g., "overture", "annex")
203-
theme_names: List of theme names from --theme option
205+
tags: Tags to include (e.g., "feature", "overture:theme=buildings")
206+
excluded_tags: Tags to exclude (e.g., "draft")
204207
type_names: List of type names from --type option
205208
206209
Returns
207210
-------
208211
Model type suitable for passing to parse_feature
209212
"""
210-
# Discover models once with the appropriate namespace
211-
all_models = discover_models()
213+
# Discover models
214+
models: ModelDict = discover_models()
212215

213216
# Filter models based on CLI options
214-
filtered_models: ModelDict = {}
215-
216-
if namespace and namespace != "overture":
217-
filtered_models = {
218-
key: model_class
219-
for key, model_class in all_models.items()
220-
if namespace in key.tags
221-
}
222-
223-
if use_overture_types:
224-
for key, model_class in all_models.items():
225-
if tags_by_key(key.tags, "overture:theme"):
226-
filtered_models[key] = model_class
227-
228-
elif theme_names and not type_names:
229-
# Theme-only mode: all types in specified themes
230-
for key, model_class in all_models.items():
231-
if next(iter(tags_by_key(key.tags, "overture:theme")), None) in theme_names:
232-
filtered_models[key] = model_class
233-
234-
elif type_names and not theme_names:
235-
# Type-only mode: find matching types across all themes
236-
for key, model_class in all_models.items():
237-
if key.name in type_names and tags_by_key(key.tags, "overture:theme"):
238-
filtered_models[key] = model_class
239-
240-
elif type_names and theme_names:
241-
# Both specified: find matching types within specified themes
242-
for key, model_class in all_models.items():
243-
if (
244-
key.name in type_names
245-
and next(iter(tags_by_key(key.tags, "overture:theme")), None)
246-
in theme_names
247-
):
248-
filtered_models[key] = model_class
249-
250-
else:
251-
# No filters specified - use all models
252-
filtered_models = all_models
217+
models = filter_models(models, tags, excluded_tags, type_names)
253218

254-
if not filtered_models:
219+
if not models:
255220
raise ValueError("No models found matching the specified criteria")
256221

257-
return create_union_type_from_models(filtered_models)
222+
return create_union_type_from_models(models)
258223

259224

260225
def get_source_name(filename: Path) -> str:
@@ -290,10 +255,10 @@ def cli() -> None:
290255
$ overture-schema list-types
291256
\b
292257
# Generate JSON schema
293-
$ overture-schema json-schema --theme buildings
258+
$ overture-schema json-schema --tag overture:theme=buildings
294259
\b
295260
# Validate specific types
296-
$ overture-schema validate --theme buildings data.json
261+
$ overture-schema validate --tag overture:theme=buildings data.json
297262
"""
298263
pass
299264

@@ -536,7 +501,7 @@ def handle_validation_error(
536501
style="yellow",
537502
)
538503
stderr.print(
539-
" • Consider validating each type separately with --theme or --type",
504+
" • Consider validating each type separately with --tag or --type",
540505
style="dim",
541506
)
542507
stderr.print()
@@ -557,7 +522,7 @@ def handle_validation_error(
557522
style="yellow",
558523
)
559524
stderr.print(
560-
" • Specifying --theme or --type to narrow validation", style="dim"
525+
" • Specifying --tag or --type to narrow validation", style="dim"
561526
)
562527
stderr.print(" • Adding discriminator fields to clarify intent", style="dim")
563528
stderr.print()
@@ -637,18 +602,16 @@ def handle_generic_error(e: Exception, filename: Path, error_type: str) -> None:
637602
@cli.command()
638603
@click.argument("filename", type=click.Path(path_type=Path), required=True)
639604
@click.option(
640-
"--overture-types",
641-
is_flag=True,
642-
help="Validate against all official Overture types (excludes extensions)",
643-
)
644-
@click.option(
645-
"--namespace",
646-
help="Namespace to filter by (e.g., overture, annex)",
605+
"--tag",
606+
"tags",
607+
multiple=True,
608+
help="Tags to include (e.g., overture:theme=addresses)",
647609
)
648610
@click.option(
649-
"--theme",
611+
"--exclude-tag",
612+
"excluded_tags",
650613
multiple=True,
651-
help="Theme to validate against (shorthand for all types in theme)",
614+
help="Tags to exclude (e.g., overture:theme=base)",
652615
)
653616
@click.option(
654617
"--type",
@@ -664,9 +627,8 @@ def handle_generic_error(e: Exception, filename: Path, error_type: str) -> None:
664627
)
665628
def validate(
666629
filename: Path,
667-
overture_types: bool,
668-
namespace: str | None,
669-
theme: tuple[str, ...],
630+
tags: tuple[str, ...],
631+
excluded_tags: tuple[str, ...],
670632
types: tuple[str, ...],
671633
show_fields: tuple[str, ...],
672634
) -> None:
@@ -684,17 +646,17 @@ def validate(
684646
$ overture-schema validate - < data.json
685647
\b
686648
# Validate only buildings
687-
$ overture-schema validate --theme buildings data.json
649+
$ overture-schema validate --tag overture:theme=buildings data.json
688650
\b
689651
# Validate specific type
690652
$ overture-schema validate --type building data.json
691653
\b
692654
# Official Overture types only
693-
$ overture-schema validate --overture-types data.json
655+
$ overture-schema validate --tag overture --tag feature data.json
694656
"""
695657
# Resolve model type first (errors here are ValueErrors, not ValidationErrors)
696658
try:
697-
model_type = resolve_types(overture_types, namespace, theme, types)
659+
model_type = resolve_types(tags, excluded_tags, types)
698660
except ValueError as e:
699661
handle_generic_error(e, filename, "value")
700662
return
@@ -722,18 +684,16 @@ def validate(
722684

723685
@cli.command("json-schema")
724686
@click.option(
725-
"--overture-types",
726-
is_flag=True,
727-
help="Generate schema for all official Overture types (excludes extensions)",
728-
)
729-
@click.option(
730-
"--namespace",
731-
help="Namespace to filter by (e.g., overture, annex)",
687+
"--tag",
688+
"tags",
689+
multiple=True,
690+
help="Tags to include (e.g., overture:theme=addresses)",
732691
)
733692
@click.option(
734-
"--theme",
693+
"--exclude-tag",
694+
"excluded_tags",
735695
multiple=True,
736-
help="Theme to generate schema for (shorthand for all types in theme)",
696+
help="Tags to exclude (e.g., overture:theme=base)",
737697
)
738698
@click.option(
739699
"--type",
@@ -742,9 +702,8 @@ def validate(
742702
help="Specific type to generate schema for (e.g., building, segment)",
743703
)
744704
def json_schema_command(
745-
overture_types: bool,
746-
namespace: str | None,
747-
theme: tuple[str, ...],
705+
tags: tuple[str, ...],
706+
excluded_tags: tuple[str, ...],
748707
types: tuple[str, ...],
749708
) -> None:
750709
r"""Generate JSON schema for Overture Maps types.
@@ -757,17 +716,17 @@ def json_schema_command(
757716
# All types
758717
$ overture-schema json-schema > schema.json
759718
\b
760-
# Buildings theme
761-
$ overture-schema json-schema --theme buildings
719+
# Buildings theme by tag
720+
$ overture-schema json-schema --tag overture:theme=buildings
762721
\b
763722
# Specific types
764723
$ overture-schema json-schema --type building
765724
\b
766725
# Official Overture types only
767-
$ overture-schema json-schema --overture-types
726+
$ overture-schema json-schema --tag overture --tag feature
768727
"""
769728
try:
770-
model_type = resolve_types(overture_types, namespace, theme, types)
729+
model_type = resolve_types(tags, excluded_tags, types)
771730
schema = json_schema(model_type)
772731
# Use plain print for JSON output to avoid Rich formatting
773732
print(json.dumps(schema, indent=2, sort_keys=True))
@@ -786,7 +745,7 @@ def json_schema_command(
786745
"--exclude-tag",
787746
"excluded_tags",
788747
multiple=True,
789-
help="Filter types by tag (e.g., overture:theme=base)",
748+
help="Exclude types by tag (e.g., overture:theme=base)",
790749
)
791750
@click.option(
792751
"--group-by",
@@ -797,8 +756,7 @@ def list_types(
797756
) -> None:
798757
r"""List all available types grouped by theme with descriptions.
799758
800-
Displays all registered Overture Maps types organized by theme,
801-
including model class names and docstrings.
759+
Displays all registered Overture Maps types and can organized by grouping.
802760
803761
\b
804762
Examples:
@@ -807,21 +765,8 @@ def list_types(
807765
"""
808766
try:
809767
models = discover_models()
810-
filters = []
811-
812-
if tags:
813-
filters.append(lambda key: all(tag in key.tags for tag in tags))
814-
if excluded_tags:
815-
filters.append(
816-
lambda key: not any(tag in key.tags for tag in excluded_tags)
817-
)
818768

819-
if filters:
820-
models = {
821-
key: model
822-
for key, model in models.items()
823-
if all(f(key) for f in filters)
824-
}
769+
models = filter_models(models, tags=tags, excluded_tags=excluded_tags)
825770

826771
if group_by:
827772
grouped_models: dict[str, set[ModelKey]] = {}

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

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ def test_list_types_command(self, cli_runner: CliRunner) -> None:
1616
"""Test the list-types command."""
1717
result = cli_runner.invoke(cli, ["list-types"])
1818
assert result.exit_code == 0
19-
# Should show theme names
20-
assert "BUILDINGS" in result.output or "buildings" in result.output
19+
# Should show theme tag
20+
assert "overture:theme=buildings" in result.output
2121
# Should show type names
2222
assert "building" in result.output
2323

@@ -33,7 +33,9 @@ class TestJsonSchemaCommand:
3333

3434
def test_json_schema_generates_valid_output(self, cli_runner: CliRunner) -> None:
3535
"""Test that json-schema command generates valid JSON."""
36-
result = cli_runner.invoke(cli, ["json-schema", "--theme", "buildings"])
36+
result = cli_runner.invoke(
37+
cli, ["json-schema", "--tag", "overture:theme=buildings"]
38+
)
3739
assert result.exit_code == 0
3840

3941
# Should be valid JSON
@@ -57,7 +59,7 @@ def test_validate_flat_format_input(self, cli_runner: CliRunner) -> None:
5759
flat_feature = build_feature(geojson_format=False)
5860
flat_json = json.dumps(flat_feature)
5961
result = cli_runner.invoke(
60-
cli, ["validate", "--theme", "buildings", "-"], input=flat_json
62+
cli, ["validate", "--tag", "overture:theme=buildings", "-"], input=flat_json
6163
)
6264
assert result.exit_code == 0
6365
assert "Successfully validated <stdin>" in result.output
@@ -222,7 +224,7 @@ def test_validate_with_nonexistent_filters_raises_error(
222224
# Try to validate with a nonexistent theme
223225
result = cli_runner.invoke(
224226
cli,
225-
["validate", "--theme", "nonexistent_theme", "-"],
227+
["validate", "--tag", "overture:theme=nonexistent_theme", "-"],
226228
input=building_feature_yaml_content,
227229
)
228230
# UsageError exits with code 2
@@ -254,7 +256,7 @@ def test_validate_with_valid_theme_invalid_type_raises_error(
254256
# Try to validate buildings theme with a type that doesn't exist in that theme
255257
result = cli_runner.invoke(
256258
cli,
257-
["validate", "--theme", "buildings", "--type", "segment", "-"],
259+
["validate", "--tag", "overture:theme=buildings", "--type", "segment", "-"],
258260
input=building_feature_yaml_content,
259261
)
260262
# UsageError exits with code 2

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ class TestPerformValidation:
203203
def test_perform_validation_raises_for_invalid_single_feature(self) -> None:
204204
"""Test that perform_validation raises ValidationError for single invalid feature."""
205205
data = build_feature(id=None) # Missing required 'id'
206-
model_type = resolve_types(False, None, ("buildings",), ())
206+
model_type = resolve_types(("overture:theme=buildings",), (), ())
207207

208208
with pytest.raises(ValidationError) as exc_info:
209209
perform_validation(data, model_type)
@@ -218,7 +218,7 @@ def test_perform_validation_raises_for_invalid_list_item(self) -> None:
218218
id=None, coordinates=[[[2, 2], [3, 2], [3, 3], [2, 3], [2, 2]]]
219219
)
220220
data = [feature1, feature2]
221-
model_type = resolve_types(False, None, ("buildings",), ())
221+
model_type = resolve_types(("overture:theme=buildings",), (), ())
222222

223223
with pytest.raises(ValidationError) as exc_info:
224224
perform_validation(data, model_type)
@@ -230,15 +230,15 @@ def test_perform_validation_raises_for_invalid_list_item(self) -> None:
230230
def test_perform_validation_empty_list(self) -> None:
231231
"""Test validating an empty list (edge case)."""
232232
data: list[dict[str, object]] = []
233-
model_type = resolve_types(False, None, ("buildings",), ())
233+
model_type = resolve_types(("overture:theme=buildings",), (), ())
234234

235235
# Should not raise
236236
perform_validation(data, model_type)
237237

238238
def test_perform_validation_empty_feature_collection(self) -> None:
239239
"""Test validating an empty FeatureCollection (edge case)."""
240240
data = {"type": "FeatureCollection", "features": []}
241-
model_type = resolve_types(False, None, ("buildings",), ())
241+
model_type = resolve_types(("overture:theme=buildings",), (), ())
242242

243243
# Should not raise
244244
perform_validation(data, model_type)
@@ -248,10 +248,10 @@ def test_perform_validation_with_different_themes(self) -> None:
248248
data = build_feature(theme="buildings", type="building")
249249

250250
# Should work with buildings theme
251-
buildings_type = resolve_types(False, None, ("buildings",), ())
251+
buildings_type = resolve_types(("overture:theme=buildings",), (), ())
252252
perform_validation(data, buildings_type)
253253

254254
# Should fail with wrong theme
255-
places_type = resolve_types(False, None, ("places",), ())
255+
places_type = resolve_types(("overture:theme=places",), (), ())
256256
with pytest.raises(ValidationError):
257257
perform_validation(data, places_type)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ def test_ambiguous_data_shows_most_likely_errors(
4040
version: 0
4141
""")
4242

43-
result = cli_runner.invoke(cli, ["validate", "--theme", "buildings", filename])
43+
result = cli_runner.invoke(
44+
cli, ["validate", "--tag", "overture:theme=buildings", filename]
45+
)
4446

4547
assert result.exit_code == 1
4648

0 commit comments

Comments
 (0)