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.
Files changed (43) hide show
  1. {unitysvc_core-0.2.23/src/unitysvc_core.egg-info → unitysvc_core-0.2.25}/PKG-INFO +1 -1
  2. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/pyproject.toml +1 -1
  3. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/__init__.py +2 -0
  4. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/base.py +11 -6
  5. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/service_group_data.py +4 -3
  6. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/validators.py +29 -12
  7. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/service_group_v1.json +6 -5
  8. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
  9. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_service_group.py +27 -4
  10. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_validators.py +21 -4
  11. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/LICENSE +0 -0
  12. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/README.md +0 -0
  13. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/setup.cfg +0 -0
  14. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/__init__.py +0 -0
  15. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/documents.py +0 -0
  16. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/listing_data.py +0 -0
  17. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/listing_v1.py +0 -0
  18. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/offering_data.py +0 -0
  19. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/offering_v1.py +0 -0
  20. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/pricing.py +0 -0
  21. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/promotion_data.py +0 -0
  22. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/provider_data.py +0 -0
  23. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/provider_v1.py +0 -0
  24. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/models/service.py +0 -0
  25. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/py.typed +0 -0
  26. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/base.json +0 -0
  27. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/listing_v1.json +0 -0
  28. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/offering_v1.json +0 -0
  29. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  30. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/schema/provider_v1.json +0 -0
  31. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/utils.py +0 -0
  32. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core/validator.py +0 -0
  33. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
  34. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  35. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/requires.txt +0 -0
  36. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  37. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_access_interface_routing.py +0 -0
  38. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_hidden_path_skip.py +0 -0
  39. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_pricing.py +0 -0
  40. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_provider_account_rate_limits.py +0 -0
  41. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_service_status.py +0 -0
  42. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_utils.py +0 -0
  43. {unitysvc_core-0.2.23 → unitysvc_core-0.2.25}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.23
3
+ Version: 0.2.25
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "unitysvc-core"
7
- version = "0.2.23"
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
- - ``regular``: Enrollable, services-bearing group (the common case).
142
- - ``category``: Non-enrollable parent that organizes child groups.
143
- - ``misc``: System-generated catch-all for uncategorized services.
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
- misc = "misc"
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):
@@ -84,10 +84,11 @@ class ServiceGroupData(BaseModel):
84
84
  )
85
85
 
86
86
  group_type: GroupTypeEnum = Field(
87
- default=GroupTypeEnum.regular,
87
+ default=GroupTypeEnum.category,
88
88
  description=(
89
- "Type of group: regular (enrollable), category (non-enrollable "
90
- "parent), or misc (system-generated catch-all)."
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 any(
826
- isinstance(cfg, dict) and cfg.get("access_method") == "mcp" for cfg in channels.values()
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
 
@@ -11,11 +11,12 @@
11
11
  "type": "string"
12
12
  },
13
13
  "GroupTypeEnum": {
14
- "description": "Type of service group.\n\n- ``regular``: Enrollable, services-bearing group (the common case).\n- ``category``: Non-enrollable parent that organizes child groups.\n- ``misc``: System-generated catch-all for uncategorized services.",
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
- "misc"
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": "regular",
73
- "description": "Type of group: regular (enrollable), category (non-enrollable parent), or misc (system-generated catch-all)."
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": [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.23
3
+ Version: 0.2.25
4
4
  Summary: Shared data models and validation helpers for UnitySVC packages
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
@@ -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.regular
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.category,
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.category
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="regular",
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 test_non_mcp_offering_is_ignored(self) -> None:
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