unitysvc-core 0.2.23__tar.gz → 0.2.25__tar.gz
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.
- {unitysvc_core-0.2.23/src/unitysvc_core.egg-info → unitysvc_core-0.2.25}/PKG-INFO +1 -1
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/pyproject.toml +1 -1
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/__init__.py +2 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/base.py +11 -6
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/service_group_data.py +4 -3
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/validators.py +29 -12
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/service_group_v1.json +6 -5
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_service_group.py +27 -4
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_validators.py +21 -4
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/LICENSE +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/README.md +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/setup.cfg +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/pricing.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_pricing.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_validator.py +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "unitysvc-core"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.25"
|
|
8
8
|
description = "Shared data models and validation helpers for UnitySVC packages"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{ name = "Bo Peng", email = "bo.peng@unitysvc.com" }]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from .base import (
|
|
2
|
+
ROUTABLE_GROUP_TYPES,
|
|
2
3
|
AccessMethodEnum,
|
|
3
4
|
AuthMethodEnum,
|
|
4
5
|
ContentFilterEnum,
|
|
@@ -131,6 +132,7 @@ __all__ = [
|
|
|
131
132
|
"QuotaResetCycleEnum",
|
|
132
133
|
"RateLimitUnitEnum",
|
|
133
134
|
"RequestTransformEnum",
|
|
135
|
+
"ROUTABLE_GROUP_TYPES",
|
|
134
136
|
"SellerTypeEnum",
|
|
135
137
|
"ServiceGroupStatusEnum",
|
|
136
138
|
"ServiceTypeEnum",
|
|
@@ -136,16 +136,21 @@ class GroupOwnerTypeEnum(StrEnum):
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
class GroupTypeEnum(StrEnum):
|
|
139
|
-
"""Type of service group.
|
|
139
|
+
"""Type of service group. Derived from members, not authored.
|
|
140
140
|
|
|
141
|
-
- ``
|
|
142
|
-
- ``
|
|
143
|
-
- ``
|
|
141
|
+
- ``category``: Parent group for browsing/organization only.
|
|
142
|
+
- ``collection``: Services-bearing group that is not routable.
|
|
143
|
+
- ``open``: Routable group where keyless requests can fan safely.
|
|
144
|
+
- ``keyed``: Routable group where callers must provide a routing key.
|
|
144
145
|
"""
|
|
145
146
|
|
|
146
|
-
regular = "regular"
|
|
147
147
|
category = "category"
|
|
148
|
-
|
|
148
|
+
collection = "collection"
|
|
149
|
+
open = "open"
|
|
150
|
+
keyed = "keyed"
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
ROUTABLE_GROUP_TYPES = frozenset({GroupTypeEnum.open, GroupTypeEnum.keyed})
|
|
149
154
|
|
|
150
155
|
|
|
151
156
|
class SellerTypeEnum(StrEnum):
|
{unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/service_group_data.py
RENAMED
|
@@ -84,10 +84,11 @@ class ServiceGroupData(BaseModel):
|
|
|
84
84
|
)
|
|
85
85
|
|
|
86
86
|
group_type: GroupTypeEnum = Field(
|
|
87
|
-
default=GroupTypeEnum.
|
|
87
|
+
default=GroupTypeEnum.category,
|
|
88
88
|
description=(
|
|
89
|
-
"
|
|
90
|
-
"
|
|
89
|
+
"Backend-derived group type: category (browse-only parent), "
|
|
90
|
+
"collection (not routable), open (routable keyless fanout), "
|
|
91
|
+
"or keyed (routable by key)."
|
|
91
92
|
),
|
|
92
93
|
)
|
|
93
94
|
|
|
@@ -815,20 +815,39 @@ def validate_mcp_offering(data: dict[str, Any] | None) -> list[str]:
|
|
|
815
815
|
"""
|
|
816
816
|
if not data or not isinstance(data, dict):
|
|
817
817
|
return []
|
|
818
|
-
if data.get("service_type") != "mcp":
|
|
819
|
-
return []
|
|
820
818
|
|
|
819
|
+
service_type = data.get("service_type")
|
|
820
|
+
capabilities = data.get("capabilities")
|
|
821
821
|
channels = data.get("upstream_access_config")
|
|
822
|
+
mcp_channels = (
|
|
823
|
+
sorted(
|
|
824
|
+
name
|
|
825
|
+
for name, config in (channels or {}).items()
|
|
826
|
+
if isinstance(config, dict) and config.get("access_method") == "mcp"
|
|
827
|
+
)
|
|
828
|
+
if isinstance(channels, dict)
|
|
829
|
+
else []
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
if service_type != "mcp":
|
|
833
|
+
declarations: list[str] = []
|
|
834
|
+
if isinstance(capabilities, list) and "mcp-delegation" in capabilities:
|
|
835
|
+
declarations.append("capability 'mcp-delegation'")
|
|
836
|
+
if mcp_channels:
|
|
837
|
+
declarations.append("MCP upstream channel(s) " + ", ".join(repr(name) for name in mcp_channels))
|
|
838
|
+
if declarations:
|
|
839
|
+
return [
|
|
840
|
+
f"service_type {service_type!r} may not declare "
|
|
841
|
+
+ "; ".join(declarations)
|
|
842
|
+
+ "; use service_type 'mcp' instead"
|
|
843
|
+
]
|
|
844
|
+
return []
|
|
845
|
+
|
|
822
846
|
if not channels or not isinstance(channels, dict):
|
|
823
847
|
return ["upstream_access_config: an MCP service must declare at least one channel"]
|
|
824
848
|
|
|
825
|
-
if not
|
|
826
|
-
|
|
827
|
-
):
|
|
828
|
-
return [
|
|
829
|
-
"upstream_access_config: an MCP service must have at least one channel "
|
|
830
|
-
"with access_method 'mcp'"
|
|
831
|
-
]
|
|
849
|
+
if not mcp_channels:
|
|
850
|
+
return ["upstream_access_config: an MCP service must have at least one channel with access_method 'mcp'"]
|
|
832
851
|
|
|
833
852
|
return []
|
|
834
853
|
|
|
@@ -901,9 +920,7 @@ def validate_listing_mcp_base_urls(user_access_interfaces: dict[str, Any] | None
|
|
|
901
920
|
f"a non-empty 'namespace' in routing_key"
|
|
902
921
|
)
|
|
903
922
|
else:
|
|
904
|
-
errors.extend(
|
|
905
|
-
validate_mcp_namespace(namespace, f"{field}.routing_key.namespace")
|
|
906
|
-
)
|
|
923
|
+
errors.extend(validate_mcp_namespace(namespace, f"{field}.routing_key.namespace"))
|
|
907
924
|
|
|
908
925
|
return errors
|
|
909
926
|
|
{unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/service_group_v1.json
RENAMED
|
@@ -11,11 +11,12 @@
|
|
|
11
11
|
"type": "string"
|
|
12
12
|
},
|
|
13
13
|
"GroupTypeEnum": {
|
|
14
|
-
"description": "Type of service group.\n\n- ``
|
|
14
|
+
"description": "Type of service group. Derived from members, not authored.\n\n- ``category``: Parent group for browsing/organization only.\n- ``collection``: Services-bearing group that is not routable.\n- ``open``: Routable group where keyless requests can fan safely.\n- ``keyed``: Routable group where callers must provide a routing key.",
|
|
15
15
|
"enum": [
|
|
16
|
-
"regular",
|
|
17
16
|
"category",
|
|
18
|
-
"
|
|
17
|
+
"collection",
|
|
18
|
+
"open",
|
|
19
|
+
"keyed"
|
|
19
20
|
],
|
|
20
21
|
"title": "GroupTypeEnum",
|
|
21
22
|
"type": "string"
|
|
@@ -69,8 +70,8 @@
|
|
|
69
70
|
},
|
|
70
71
|
"group_type": {
|
|
71
72
|
"$ref": "#/$defs/GroupTypeEnum",
|
|
72
|
-
"default": "
|
|
73
|
-
"description": "
|
|
73
|
+
"default": "category",
|
|
74
|
+
"description": "Backend-derived group type: category (browse-only parent), collection (not routable), open (routable keyless fanout), or keyed (routable by key)."
|
|
74
75
|
},
|
|
75
76
|
"membership_rules": {
|
|
76
77
|
"anyOf": [
|
|
@@ -46,22 +46,44 @@ class TestServiceGroupDataAdminFields:
|
|
|
46
46
|
g = ServiceGroupData(name="ok", display_name="X")
|
|
47
47
|
assert g.owner_type is GroupOwnerTypeEnum.platform
|
|
48
48
|
assert g.owner_id is None
|
|
49
|
-
assert g.group_type is GroupTypeEnum.
|
|
49
|
+
assert g.group_type is GroupTypeEnum.category
|
|
50
50
|
assert g.access_interface_data_template is None
|
|
51
51
|
|
|
52
|
+
@pytest.mark.parametrize(
|
|
53
|
+
"group_type",
|
|
54
|
+
[
|
|
55
|
+
GroupTypeEnum.category,
|
|
56
|
+
GroupTypeEnum.collection,
|
|
57
|
+
GroupTypeEnum.open,
|
|
58
|
+
GroupTypeEnum.keyed,
|
|
59
|
+
],
|
|
60
|
+
)
|
|
61
|
+
def test_backend_group_types_set(self, group_type: GroupTypeEnum):
|
|
62
|
+
g = ServiceGroupData(
|
|
63
|
+
name="ok",
|
|
64
|
+
display_name="X",
|
|
65
|
+
group_type=group_type,
|
|
66
|
+
)
|
|
67
|
+
assert g.group_type is group_type
|
|
68
|
+
|
|
52
69
|
def test_admin_fields_set(self):
|
|
53
70
|
g = ServiceGroupData(
|
|
54
71
|
name="ok",
|
|
55
72
|
display_name="X",
|
|
56
73
|
owner_type=GroupOwnerTypeEnum.seller,
|
|
57
74
|
owner_id="00000000-0000-0000-0000-000000000001",
|
|
58
|
-
group_type=GroupTypeEnum.
|
|
75
|
+
group_type=GroupTypeEnum.open,
|
|
59
76
|
access_interface_data_template='{"base_url": "https://x"}',
|
|
60
77
|
)
|
|
61
78
|
assert g.owner_type is GroupOwnerTypeEnum.seller
|
|
62
|
-
assert g.group_type is GroupTypeEnum.
|
|
79
|
+
assert g.group_type is GroupTypeEnum.open
|
|
63
80
|
assert g.access_interface_data_template == '{"base_url": "https://x"}'
|
|
64
81
|
|
|
82
|
+
@pytest.mark.parametrize("legacy_type", ["regular", "misc"])
|
|
83
|
+
def test_legacy_group_types_rejected(self, legacy_type: str):
|
|
84
|
+
with pytest.raises(ValidationError):
|
|
85
|
+
ServiceGroupData(name="ok", display_name="X", group_type=legacy_type)
|
|
86
|
+
|
|
65
87
|
|
|
66
88
|
class TestServiceGroupV1FileModel:
|
|
67
89
|
"""``extra='forbid'`` keeps file-level validation strict."""
|
|
@@ -78,10 +100,11 @@ class TestServiceGroupV1FileModel:
|
|
|
78
100
|
name="my-group",
|
|
79
101
|
display_name="X",
|
|
80
102
|
owner_type="platform",
|
|
81
|
-
group_type="
|
|
103
|
+
group_type="keyed",
|
|
82
104
|
status="active",
|
|
83
105
|
)
|
|
84
106
|
assert g.status is ServiceGroupStatusEnum.active
|
|
107
|
+
assert g.group_type is GroupTypeEnum.keyed
|
|
85
108
|
|
|
86
109
|
|
|
87
110
|
class TestValidateServiceGroupDict:
|
|
@@ -583,9 +583,28 @@ class TestValidateMcpOffering:
|
|
|
583
583
|
def test_valid_mcp_offering_passes(self) -> None:
|
|
584
584
|
assert validate_mcp_offering(self._offering()) == []
|
|
585
585
|
|
|
586
|
-
def
|
|
586
|
+
def test_non_mcp_offering_without_mcp_declarations_is_ignored(self) -> None:
|
|
587
587
|
assert validate_mcp_offering({"service_type": "llm"}) == []
|
|
588
588
|
|
|
589
|
+
def test_non_mcp_offering_cannot_declare_mcp_delegation(self) -> None:
|
|
590
|
+
errors = validate_mcp_offering({"service_type": "proxy", "capabilities": ["mcp-delegation"]})
|
|
591
|
+
assert errors
|
|
592
|
+
assert "mcp-delegation" in errors[0]
|
|
593
|
+
assert "service_type 'mcp'" in errors[0]
|
|
594
|
+
|
|
595
|
+
def test_non_mcp_offering_cannot_declare_an_mcp_channel(self) -> None:
|
|
596
|
+
errors = validate_mcp_offering(
|
|
597
|
+
{
|
|
598
|
+
"service_type": "proxy",
|
|
599
|
+
"upstream_access_config": {
|
|
600
|
+
"upstream": {"access_method": "mcp", "base_url": "https://mcp.example.test"}
|
|
601
|
+
},
|
|
602
|
+
}
|
|
603
|
+
)
|
|
604
|
+
assert errors
|
|
605
|
+
assert "MCP upstream channel" in errors[0]
|
|
606
|
+
assert "service_type 'mcp'" in errors[0]
|
|
607
|
+
|
|
589
608
|
def test_requires_a_channel(self) -> None:
|
|
590
609
|
errors = validate_mcp_offering(self._offering(upstream_access_config={}))
|
|
591
610
|
assert errors
|
|
@@ -593,9 +612,7 @@ class TestValidateMcpOffering:
|
|
|
593
612
|
|
|
594
613
|
def test_requires_an_mcp_access_method_channel(self) -> None:
|
|
595
614
|
errors = validate_mcp_offering(
|
|
596
|
-
self._offering(
|
|
597
|
-
upstream_access_config={"x": {"access_method": "http", "base_url": "https://x"}}
|
|
598
|
-
)
|
|
615
|
+
self._offering(upstream_access_config={"x": {"access_method": "http", "base_url": "https://x"}})
|
|
599
616
|
)
|
|
600
617
|
assert errors
|
|
601
618
|
assert "access_method 'mcp'" in errors[0]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|