Skip to content

Commit 039c907

Browse files
refactor(system): Adds reserved namespaces and logging to tag filtering
Signed-off-by: Roel <75250264+RoelBollens-TomTom@users.noreply.github.com>
1 parent ab6a274 commit 039c907

4 files changed

Lines changed: 269 additions & 15 deletions

File tree

packages/overture-schema-core/src/overture/schema/core/tag_providers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def theme_provider(
3737
model_class: type[BaseModel], key: ModelKey, tags: set[str]
3838
) -> set[str]:
3939
for tp in _extract_types(model_class):
40-
if issubclass(tp, OvertureFeature):
40+
if isinstance(tp, type) and issubclass(tp, OvertureFeature):
4141
tags.add(
4242
"overture:theme=" + get_args(tp.model_fields["theme"].annotation)[0]
4343
)

packages/overture-schema-system/src/overture/schema/system/discovery.py

Lines changed: 73 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import re
66
from collections.abc import Callable
77
from dataclasses import dataclass, replace
8-
from typing import Annotated, Any, Literal, Union, get_args, get_origin
8+
from typing import Annotated, Any, Literal, TypeAlias, Union, get_args, get_origin
99

1010
from pydantic import BaseModel
1111

@@ -18,6 +18,11 @@
1818
"overture": {"overture-schema-core"},
1919
"feature": {"overture-schema-system"},
2020
}
21+
RESERVED_NAMESPACES: dict[str, set[str]] = {
22+
"overture": {"overture-schema-core"},
23+
"system": {"overture-schema-system"},
24+
}
25+
2126
TAG = r"[a-z0-9][a-z0-9_-]*"
2227
NAMESPACE_TAG = r"[a-z0-9]+:[a-z0-9]+(?:=[a-z0-9_.-]+)?"
2328
TAG_RE = re.compile(rf"^(?:{TAG}|{NAMESPACE_TAG})$")
@@ -61,13 +66,19 @@ class TagProviderKey:
6166
package_name: str # distribution package name
6267

6368

64-
TagProvider = Callable[[type[BaseModel], ModelKey, set[str]], set[str]]
69+
TagProvider: TypeAlias = Callable[[type[BaseModel], ModelKey, set[str]], set[str]]
70+
71+
ModelDict: TypeAlias = dict[ModelKey, type[BaseModel]]
72+
73+
TagProviderDict: TypeAlias = dict[TagProviderKey, TagProvider]
74+
75+
ModelKeyFilter: TypeAlias = Callable[[ModelKey], bool]
6576

6677

6778
def generate_tags(
6879
model_class: type[BaseModel],
6980
key: ModelKey,
70-
providers: dict[TagProviderKey, TagProvider],
81+
providers: TagProviderDict,
7182
) -> set[str]:
7283
tags: set[str] = set()
7384

@@ -85,16 +96,53 @@ def generate_tags(
8596

8697

8798
def _filter_tags(tags: set[str], provider: TagProviderKey) -> set[str]:
88-
reserved_tags = tuple(
89-
tag for tag, dist in RESERVED_TAGS.items() if provider.package_name not in dist
90-
)
99+
"""Filter tags, removing invalid, reserved, or namespace-restricted tags for a provider."""
100+
filtered_tags: set[str] = set()
101+
reserved_tags: set[str] = {
102+
tag for tag, pkgs in RESERVED_TAGS.items() if provider.package_name not in pkgs
103+
}
104+
reserved_namespaces: set[str] = {
105+
ns
106+
for ns, pkgs in RESERVED_NAMESPACES.items()
107+
if provider.package_name not in pkgs
108+
}
109+
110+
for tag in tags:
111+
# Validate tag format
112+
if not TAG_RE.fullmatch(tag):
113+
logger.debug(
114+
f"Tag provider '{provider.name}' (package '{provider.package_name}') attempted to set '{tag}' as tag. "
115+
f"This tag does not match the required format."
116+
)
117+
continue
118+
119+
# Reserved tag check
120+
if tag in reserved_tags:
121+
allowed_pkgs = RESERVED_TAGS.get(tag, set())
122+
logger.debug(
123+
f"Tag provider '{provider.name}' (package '{provider.package_name}') attempted to set reserved tag '{tag}'. "
124+
f"This tag can only be set by packages from: {allowed_pkgs}."
125+
)
126+
continue
127+
128+
# Reserved namespace check
129+
tag_ns = namespace(tag)
130+
if tag_ns and tag_ns in reserved_namespaces:
131+
allowed_pkgs = RESERVED_NAMESPACES.get(tag_ns, set())
132+
logger.debug(
133+
f"Tag provider '{provider.name}' (package '{provider.package_name}') attempted to set tag '{tag}' in reserved namespace '{tag_ns}'. "
134+
f"This namespace can only be set by packages from: {allowed_pkgs}."
135+
)
136+
continue
91137

92-
return {tag for tag in tags if TAG_RE.match(tag) and tag not in reserved_tags}
138+
filtered_tags.add(tag)
139+
140+
return filtered_tags
93141

94142

95143
def discover_tag_providers(
96144
tag_providers_group: str = "overture.tag_providers",
97-
) -> dict[TagProviderKey, TagProvider]:
145+
) -> TagProviderDict:
98146
tag_providers = {}
99147

100148
try:
@@ -123,7 +171,7 @@ def discover_tag_providers(
123171

124172
def discover_models(
125173
model_group: str = "overture.models",
126-
) -> dict[ModelKey, type[BaseModel]]:
174+
) -> ModelDict:
127175
"""Discover all registered Overture models via entry points.
128176
129177
Parameters
@@ -173,13 +221,13 @@ def discover_models(
173221

174222

175223
def filter_models(
176-
models: dict[ModelKey, type[BaseModel]],
224+
models: ModelDict,
177225
tags: tuple[str, ...] = (),
178226
excluded_tags: tuple[str, ...] = (),
179227
type_names: tuple[str, ...] = (),
180-
) -> dict[ModelKey, type[BaseModel]]:
228+
) -> ModelDict:
181229
"""Filter models to those that contain all required tags."""
182-
filters = []
230+
filters: list[ModelKeyFilter] = []
183231

184232
if tags:
185233
filters.append(lambda key: all(tag in key.tags for tag in tags))
@@ -221,6 +269,15 @@ def get_registered_model(feature_type: str) -> type[BaseModel] | None:
221269
return None
222270

223271

272+
def namespace(tag: str) -> str:
273+
"""Extract the namespace from a tag, or return an empty string if there is no namespace."""
274+
if not TAG_RE.fullmatch(tag):
275+
raise ValueError(f"Invalid tag format: {tag}")
276+
if ":" in tag:
277+
return tag.split(":")[0]
278+
return ""
279+
280+
224281
def tags_by_key(tags: frozenset[str] | set[str], key: str) -> set[str]:
225282
"""Extract values for k/v tags with the given key.
226283
@@ -244,7 +301,10 @@ def tags_by_namespace(tags: frozenset[str] | set[str], namespace: str) -> set[st
244301
def feature_provider(
245302
model_class: type[BaseModel], key: ModelKey, tags: set[str]
246303
) -> set[str]:
247-
if any(issubclass(tp, Feature) for tp in _extract_types(model_class)):
304+
if any(
305+
isinstance(tp, type) and issubclass(tp, Feature)
306+
for tp in _extract_types(model_class)
307+
):
248308
tags.add("feature")
249309
return tags
250310

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import pytest
2+
from pydantic import BaseModel
3+
4+
from overture.schema.system.discovery import (
5+
ModelKey,
6+
TagProviderKey,
7+
_filter_tags,
8+
feature_provider,
9+
)
10+
from overture.schema.system.feature import Feature
11+
12+
13+
@pytest.fixture
14+
def core_tag_provider() -> TagProviderKey:
15+
return TagProviderKey(
16+
name="core", entry_point="core:Provider", package_name="overture-schema-core"
17+
)
18+
19+
20+
@pytest.fixture
21+
def system_tag_provider() -> TagProviderKey:
22+
return TagProviderKey(
23+
name="system",
24+
entry_point="system:Provider",
25+
package_name="overture-schema-system",
26+
)
27+
28+
29+
@pytest.fixture
30+
def other_tag_provider() -> TagProviderKey:
31+
return TagProviderKey(
32+
name="other", entry_point="other:Provider", package_name="other-package"
33+
)
34+
35+
36+
@pytest.fixture
37+
def feature() -> type[Feature]:
38+
class SomeFeature(Feature):
39+
pass
40+
41+
return SomeFeature
42+
43+
44+
@pytest.fixture
45+
def not_a_feature() -> type[BaseModel]:
46+
class NotAFeature(BaseModel):
47+
pass
48+
49+
return NotAFeature
50+
51+
52+
def test_valid_tags(other_tag_provider: TagProviderKey) -> None:
53+
tags = {"valid", "other:valid", "other:valid=true"}
54+
filtered = _filter_tags(tags, other_tag_provider)
55+
assert filtered == tags
56+
57+
58+
def test_invalid_tag(other_tag_provider: TagProviderKey) -> None:
59+
tags = {"InvalidTag"}
60+
filtered = _filter_tags(tags, other_tag_provider)
61+
assert filtered == set()
62+
63+
64+
def test_reserved_tag(other_tag_provider: TagProviderKey) -> None:
65+
tags = {"overture", "feature", "valid"}
66+
filtered = _filter_tags(tags, other_tag_provider)
67+
assert "valid" in filtered
68+
assert "overture" not in filtered
69+
assert "feature" not in filtered
70+
71+
72+
def test_allowed_reserved_tag(
73+
core_tag_provider: TagProviderKey, system_tag_provider: TagProviderKey
74+
) -> None:
75+
assert "overture" in _filter_tags({"overture"}, core_tag_provider)
76+
assert "feature" in _filter_tags({"feature"}, system_tag_provider)
77+
78+
79+
def test_reserved_namespace(other_tag_provider: TagProviderKey) -> None:
80+
tags = {"overture:feature", "system:feature", "valid:tag"}
81+
filtered = _filter_tags(tags, other_tag_provider)
82+
assert "valid:tag" in filtered
83+
assert "overture:feature" not in filtered
84+
assert "system:feature" not in filtered
85+
86+
87+
def test_allowed_reserved_namespace(
88+
core_tag_provider: TagProviderKey, system_tag_provider: TagProviderKey
89+
) -> None:
90+
assert "overture:feature" in _filter_tags({"overture:feature"}, core_tag_provider)
91+
assert "system:feature" in _filter_tags({"system:feature"}, system_tag_provider)
92+
93+
94+
def test_empty_tags(other_tag_provider: TagProviderKey) -> None:
95+
assert _filter_tags(set(), other_tag_provider) == set()
96+
97+
98+
def test_mixed_tags(other_tag_provider: TagProviderKey) -> None:
99+
tags = {"valid", "feature", "overture:feature", "InvalidTag"}
100+
filtered = _filter_tags(tags, other_tag_provider)
101+
assert filtered == {"valid"}
102+
103+
104+
def test_feature_provider_adds_feature_tag(feature: type[Feature]) -> None:
105+
key = ModelKey(name="feature", entry_point="system:Feature", tags=frozenset())
106+
result = feature_provider(feature, key, set())
107+
assert "feature" in result
108+
109+
110+
def test_feature_provider_does_not_add_feature_tag(
111+
not_a_feature: type[BaseModel],
112+
) -> None:
113+
key = ModelKey(
114+
name="notafeature", entry_point="system:NotAFeature", tags=frozenset()
115+
)
116+
result = feature_provider(not_a_feature, key, set())
117+
assert "feature" not in result

packages/overture-schema-system/tests/test_tags.py

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,13 @@
1-
from overture.schema.system.discovery import tags_by_key, tags_by_namespace
1+
import re
2+
import unittest
3+
4+
from overture.schema.system.discovery import (
5+
NAMESPACE_TAG,
6+
TAG,
7+
TAG_RE,
8+
tags_by_key,
9+
tags_by_namespace,
10+
)
211

312

413
def test_tags_by_key_returns_correct_values() -> None:
@@ -41,3 +50,71 @@ def test_tags_by_namespace_handles_empty_tags() -> None:
4150
namespace = "system"
4251
result = tags_by_namespace(tags, namespace)
4352
assert result == set()
53+
54+
55+
class TestSimpleTagRegex(unittest.TestCase):
56+
def test_valid_simple_tags(self) -> None:
57+
valid_tags = [
58+
"v",
59+
"valid",
60+
"valid1",
61+
"valid_tag",
62+
"valid-tag",
63+
"0valid",
64+
"42",
65+
]
66+
for tag in valid_tags:
67+
self.assertTrue(re.fullmatch(TAG, tag), f"Should match: {tag}")
68+
self.assertTrue(TAG_RE.fullmatch(tag), f"TAG_RE should match: {tag}")
69+
70+
def test_invalid_simple_tags(self) -> None:
71+
invalid_tags = [
72+
"",
73+
"_invalid",
74+
"-invalid",
75+
"Invalid",
76+
"invalid!",
77+
"invalid ",
78+
"in.valid",
79+
"3.14",
80+
]
81+
for tag in invalid_tags:
82+
self.assertFalse(re.fullmatch(TAG, tag), f"Should not match: {tag}")
83+
self.assertFalse(TAG_RE.fullmatch(tag), f"TAG_RE should not match: {tag}")
84+
85+
86+
class TestNamespaceTagRegex(unittest.TestCase):
87+
def test_valid_namespace_tags(self) -> None:
88+
valid_tags = [
89+
"ns:predicate",
90+
"ns:predicate1",
91+
"ns:predicate=value",
92+
"ns:predicate=value_0",
93+
"ns:predicate=value-0",
94+
"ns:predicate=value.0",
95+
"ns:predicate=value_2-3.4",
96+
"ns:predicate=42",
97+
"ns:predicate=3.14",
98+
]
99+
for tag in valid_tags:
100+
self.assertTrue(re.fullmatch(NAMESPACE_TAG, tag), f"Should match: {tag}")
101+
self.assertTrue(TAG_RE.fullmatch(tag), f"TAG_RE should match: {tag}")
102+
103+
def test_invalid_namespace_tags(self) -> None:
104+
invalid_tags = [
105+
"ns:",
106+
":predicate",
107+
"ns:predicate=",
108+
"ns:predicate=Value",
109+
"ns:predicate=value ",
110+
"ns:predicate=value!",
111+
"ns:predicate=ns:value",
112+
"ns:predicate=predicate=value",
113+
"Ns:predicate",
114+
"ns:Predicate",
115+
]
116+
for tag in invalid_tags:
117+
self.assertFalse(
118+
re.fullmatch(NAMESPACE_TAG, tag), f"Should not match: {tag}"
119+
)
120+
self.assertFalse(TAG_RE.fullmatch(tag), f"TAG_RE should not match: {tag}")

0 commit comments

Comments
 (0)