unitysvc-core 0.2.0__tar.gz → 0.2.2__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 (40) hide show
  1. {unitysvc_core-0.2.0/src/unitysvc_core.egg-info → unitysvc_core-0.2.2}/PKG-INFO +1 -1
  2. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/pyproject.toml +1 -1
  3. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/__init__.py +2 -4
  4. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/base.py +5 -0
  5. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/listing_data.py +4 -8
  6. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/promotion_data.py +0 -5
  7. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/service.py +73 -0
  8. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/service_group_data.py +0 -5
  9. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/schema/listing_v1.json +0 -14
  10. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/utils.py +0 -36
  11. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
  12. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core.egg-info/SOURCES.txt +1 -0
  13. unitysvc_core-0.2.2/tests/test_service_data.py +49 -0
  14. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/tests/test_utils.py +0 -26
  15. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/LICENSE +0 -0
  16. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/README.md +0 -0
  17. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/setup.cfg +0 -0
  18. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/__init__.py +0 -0
  19. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/documents.py +0 -0
  20. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/listing_v1.py +0 -0
  21. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/offering_data.py +0 -0
  22. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/offering_v1.py +0 -0
  23. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/pricing.py +0 -0
  24. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/provider_data.py +0 -0
  25. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/provider_v1.py +0 -0
  26. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/models/validators.py +0 -0
  27. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/py.typed +0 -0
  28. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/schema/base.json +0 -0
  29. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/schema/offering_v1.json +0 -0
  30. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  31. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/schema/provider_v1.json +0 -0
  32. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/schema/service_group_v1.json +0 -0
  33. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core/validator.py +0 -0
  34. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  35. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core.egg-info/requires.txt +0 -0
  36. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  37. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/tests/test_pricing.py +0 -0
  38. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/tests/test_service_group.py +0 -0
  39. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/tests/test_validator.py +0 -0
  40. {unitysvc_core-0.2.0 → unitysvc_core-0.2.2}/tests/test_validators.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
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.0"
7
+ version = "0.2.2"
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" }]
@@ -61,7 +61,6 @@ from .promotion_data import (
61
61
  PromotionData,
62
62
  PromotionV1,
63
63
  describe_scope,
64
- is_promotion_file,
65
64
  strip_schema_field,
66
65
  validate_promotion,
67
66
  )
@@ -71,13 +70,13 @@ from .service import (
71
70
  AccessInterfaceData,
72
71
  RateLimit,
73
72
  ServiceConstraints,
73
+ ServiceData,
74
74
  UpstreamAccessConfigData,
75
75
  )
76
76
  from .service_group_data import (
77
77
  SERVICE_GROUP_SCHEMA_VERSION,
78
78
  ServiceGroupData,
79
79
  ServiceGroupV1,
80
- is_service_group_file,
81
80
  validate_service_group,
82
81
  )
83
82
  from .validators import (
@@ -111,6 +110,7 @@ __all__ = [
111
110
  "UpstreamAccessConfigData",
112
111
  "RateLimit",
113
112
  "ServiceConstraints",
113
+ "ServiceData",
114
114
  # Enums
115
115
  "AccessMethodEnum",
116
116
  "AuthMethodEnum",
@@ -176,12 +176,10 @@ __all__ = [
176
176
  "suggest_valid_name",
177
177
  # Promotions
178
178
  "PROMOTION_SCHEMA_VERSION",
179
- "is_promotion_file",
180
179
  "describe_scope",
181
180
  "strip_schema_field",
182
181
  "validate_promotion",
183
182
  # Service Groups
184
183
  "SERVICE_GROUP_SCHEMA_VERSION",
185
- "is_service_group_file",
186
184
  "validate_service_group",
187
185
  ]
@@ -241,6 +241,11 @@ class RequestTransformEnum(StrEnum):
241
241
  body_transformer = "body_transformer"
242
242
  # Simple body replacement from rendered enrollment data
243
243
  set_body = "set_body"
244
+ # Translate the request/response between the client's LLM API format and the
245
+ # format the upstream provider accepts (e.g. Anthropic Messages -> OpenAI
246
+ # Chat). Config carries ``upstream_format``; the gateway performs the
247
+ # conversion in-process (#923/#929).
248
+ llm_translate = "llm_translate"
244
249
 
245
250
 
246
251
  class ServiceTypeEnum(StrEnum):
@@ -10,7 +10,6 @@ and `time_created` for data file validation.
10
10
  """
11
11
 
12
12
  from typing import Any
13
- from uuid import UUID
14
13
 
15
14
  from pydantic import BaseModel, Field
16
15
 
@@ -43,13 +42,10 @@ class ServiceListingData(BaseModel):
43
42
 
44
43
  model_config = {"extra": "ignore"}
45
44
 
46
- # Service ID for updates (set by SDK after first publish)
47
- # When provided, updates the existing service. When absent, creates a new service.
48
- service_id: UUID | None = Field(
49
- default=None,
50
- description="Service ID from previous publish. If provided, updates existing service. "
51
- "Stored in override file (e.g., listing.override.json) by SDK after first publish.",
52
- )
45
+ # NOTE: a listing carries no service_id. Service identity is backend-
46
+ # assigned provenance, not authored listing content — it lives in
47
+ # ``service.json`` and travels as the top-level ``ServiceDataInput.service_id``
48
+ # field on upload/revision, never embedded in the listing.
53
49
 
54
50
  # Listing identification
55
51
  name: str | None = Field(
@@ -100,11 +100,6 @@ class PromotionData(BaseModel):
100
100
  )
101
101
 
102
102
 
103
- def is_promotion_file(data: dict[str, Any]) -> bool:
104
- """Check if a data dict is a promotion file (by schema version)."""
105
- return data.get("schema") == PROMOTION_SCHEMA_VERSION
106
-
107
-
108
103
  def strip_schema_field(data: dict[str, Any]) -> dict[str, Any]:
109
104
  """Return a copy of the data dict without the ``schema`` field.
110
105
 
@@ -2,7 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from datetime import datetime
5
6
  from typing import Any
7
+ from uuid import UUID
6
8
 
7
9
  from pydantic import BaseModel, ConfigDict, Field
8
10
 
@@ -140,3 +142,74 @@ class UpstreamAccessConfigData(AccessInterfaceData):
140
142
  "credential on the source header."
141
143
  ),
142
144
  )
145
+
146
+
147
+ class ServiceData(BaseModel):
148
+ """Backend-assigned service identity record (the ``service.json`` file).
149
+
150
+ A service's ``provider_data`` / ``offering_data`` / ``listing_data`` are
151
+ authored by the seller; this is the *other* half — the record the backend
152
+ materializes once a service exists. It is the **round-trip** structure:
153
+ the ingest task returns it, the seller stores it in ``service.json`` beside
154
+ the spec files, and it travels back as the top-level ``service_data`` field
155
+ on the next upload/revision so the backend can match the upload to the
156
+ existing service.
157
+
158
+ Of these fields, only ``service_id`` is consumed on the way *in* (it
159
+ targets revise/replace vs create); the rest are populated on the way *out*
160
+ and are informational provenance for the seller. ``extra="ignore"`` keeps
161
+ unknown keys from breaking either direction.
162
+ """
163
+
164
+ model_config = ConfigDict(extra="ignore")
165
+
166
+ service_id: UUID | None = Field(
167
+ default=None,
168
+ description=(
169
+ "Backend-assigned service id from a previous publish. When present "
170
+ "on upload, the request targets the existing service (revise/"
171
+ "replace) instead of creating a new one."
172
+ ),
173
+ )
174
+
175
+ revision_of: UUID | None = Field(
176
+ default=None,
177
+ description=(
178
+ "Set when this record describes a revision: the canonical service "
179
+ "id the revision derives from. ``service_id`` is then the revision's "
180
+ "own id."
181
+ ),
182
+ )
183
+
184
+ status: str | None = Field(
185
+ default=None,
186
+ description="Service identity status as resolved by the last ingest.",
187
+ )
188
+
189
+ name: str | None = Field(
190
+ default=None,
191
+ description="Backend-derived service name (from listing/offering).",
192
+ )
193
+
194
+ display_name: str | None = Field(
195
+ default=None,
196
+ description="Backend-derived human-readable service name.",
197
+ )
198
+
199
+ time_created: datetime | None = Field(
200
+ default=None,
201
+ description="When the service was first created (informational provenance).",
202
+ )
203
+
204
+ template_instance_id: UUID | None = Field(
205
+ default=None,
206
+ description=(
207
+ "Set when the service was published from a seller TemplateInstance "
208
+ "(the seller-instances flow). Like ``service_id`` it is consumed on "
209
+ "the way *in* to declare the operation — with no ``service_id`` it "
210
+ "creates a service from the template (the backend pins the form's "
211
+ "service_id); with one it updates an existing template-generated "
212
+ "service — and echoed on the way *out* so ``service.json`` records "
213
+ "the template association. Absent for plain (non-template) services."
214
+ ),
215
+ )
@@ -131,11 +131,6 @@ class ServiceGroupData(BaseModel):
131
131
  SERVICE_GROUP_SCHEMA_VERSION = "service_group_v1"
132
132
 
133
133
 
134
- def is_service_group_file(data: dict[str, Any]) -> bool:
135
- """Check if a data dict is a service group file (by schema version)."""
136
- return data.get("schema") == SERVICE_GROUP_SCHEMA_VERSION
137
-
138
-
139
134
  def strip_schema_field(data: dict[str, Any]) -> dict[str, Any]:
140
135
  """Return a copy of the data dict without the ``schema`` field."""
141
136
  return {k: v for k, v in data.items() if k != "schema"}
@@ -1953,20 +1953,6 @@
1953
1953
  "title": "Name",
1954
1954
  "type": "string"
1955
1955
  },
1956
- "service_id": {
1957
- "anyOf": [
1958
- {
1959
- "format": "uuid",
1960
- "type": "string"
1961
- },
1962
- {
1963
- "type": "null"
1964
- }
1965
- ],
1966
- "default": null,
1967
- "description": "Service ID from previous publish. If provided, updates existing service. Stored in override file (e.g., listing.override.json) by SDK after first publish.",
1968
- "title": "Service Id"
1969
- },
1970
1956
  "service_options": {
1971
1957
  "anyOf": [
1972
1958
  {
@@ -408,42 +408,6 @@ def schema_for_path(path: Path) -> str | None:
408
408
  return STEM_TO_SCHEMA.get(path.stem)
409
409
 
410
410
 
411
- def find_file_by_schema_and_name(
412
- data_dir: Path, schema: str, name_field: str, name_value: str
413
- ) -> tuple[Path, str, dict[str, Any]] | None:
414
- """
415
- Find a data file by schema type and name field value.
416
-
417
- Args:
418
- data_dir: Directory to search
419
- schema: Schema identifier (e.g., "offering_v1", "listing_v1")
420
- name_field: Field name to match (e.g., "name", "seller_name")
421
- name_value: Value to match in the name field
422
-
423
- Returns:
424
- Tuple of (file_path, format, data) if found, None otherwise
425
- """
426
- data_files = find_data_files(data_dir)
427
-
428
- for data_file in data_files:
429
- try:
430
- data, file_format = load_data_file(data_file)
431
- except Exception as exc:
432
- _warn_skipped(data_file, exc)
433
- continue
434
- # Discovery walks every ``*.json`` / ``*.toml`` under ``data_dir``,
435
- # which sweeps in things like ``package-lock.json`` (top-level dict
436
- # but no ``schema`` field) and openapi/mypy-cache files (top-level
437
- # list). The schema check ignores the former, but the latter
438
- # crashes ``.get`` — skip non-dict roots instead.
439
- if not isinstance(data, dict):
440
- continue
441
- if data_file.stem == SCHEMA_TO_STEM.get(schema) and data.get(name_field) == name_value:
442
- return data_file, file_format, data
443
-
444
- return None
445
-
446
-
447
411
  @lru_cache(maxsize=256)
448
412
  def find_files_by_schema(
449
413
  data_dir: Path,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.0
3
+ Version: 0.2.2
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>
@@ -31,6 +31,7 @@ src/unitysvc_core/schema/promotion_v1.json
31
31
  src/unitysvc_core/schema/provider_v1.json
32
32
  src/unitysvc_core/schema/service_group_v1.json
33
33
  tests/test_pricing.py
34
+ tests/test_service_data.py
34
35
  tests/test_service_group.py
35
36
  tests/test_utils.py
36
37
  tests/test_validator.py
@@ -0,0 +1,49 @@
1
+ """Tests for the ServiceData model (the service.json provenance structure)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime
6
+ from uuid import UUID
7
+
8
+ from unitysvc_core.models import ServiceData
9
+
10
+ _SID = "2c1f9e7a-0000-0000-0000-000000000000"
11
+
12
+
13
+ def test_empty_service_data_has_no_identity() -> None:
14
+ sd = ServiceData()
15
+ assert sd.service_id is None
16
+ assert sd.time_created is None
17
+
18
+
19
+ def test_service_id_coerced_to_uuid() -> None:
20
+ sd = ServiceData.model_validate({"service_id": _SID})
21
+ assert sd.service_id == UUID(_SID)
22
+
23
+
24
+ def test_time_created_and_extra_fields() -> None:
25
+ # time_created is a recognized provenance field; unknown extras are
26
+ # tolerated (extra="ignore") rather than rejected.
27
+ sd = ServiceData.model_validate({"service_id": _SID, "time_created": "2026-01-01T00:00:00", "note": "x"})
28
+ assert sd.time_created == datetime(2026, 1, 1, 0, 0)
29
+ assert "note" not in sd.model_dump()
30
+
31
+
32
+ def test_full_identity_record_round_trips() -> None:
33
+ # The shape the ingest task returns and the seller persists to service.json.
34
+ rev = "11111111-0000-0000-0000-000000000000"
35
+ sd = ServiceData.model_validate(
36
+ {
37
+ "service_id": _SID,
38
+ "revision_of": rev,
39
+ "status": "revision_created",
40
+ "name": "acme/widget",
41
+ "display_name": "ACME Widget",
42
+ "time_created": "2026-01-01T00:00:00",
43
+ }
44
+ )
45
+ assert sd.service_id == UUID(_SID)
46
+ assert sd.revision_of == UUID(rev)
47
+ assert sd.status == "revision_created"
48
+ assert sd.name == "acme/widget"
49
+ assert sd.display_name == "ACME Widget"
@@ -11,7 +11,6 @@ from unitysvc_core.utils import (
11
11
  compute_file_hash,
12
12
  deep_merge_dicts,
13
13
  expand_presets,
14
- find_file_by_schema_and_name,
15
14
  find_files_by_schema,
16
15
  generate_content_based_key,
17
16
  get_basename,
@@ -175,19 +174,6 @@ def test_find_files_by_schema_skips_non_dict_roots(tmp_path: Path) -> None:
175
174
  assert {Path(fp).relative_to(tmp_path).as_posix() for fp, _fmt, _data in files} == {"provider.json"}
176
175
 
177
176
 
178
- def test_find_file_by_schema_and_name_skips_non_dict_roots(tmp_path: Path) -> None:
179
- """Sibling discovery helper has the same surface — pin both."""
180
- (tmp_path / "listing.json").write_text(json.dumps({"name": "wanted"}))
181
- (tmp_path / "bad").mkdir()
182
- (tmp_path / "bad" / "listing.json").write_text(json.dumps([{"x": 1}]))
183
-
184
- result = find_file_by_schema_and_name(tmp_path, "listing_v1", "name", "wanted")
185
- assert result is not None
186
- fp, _fmt, data = result
187
- assert Path(fp).name == "listing.json"
188
- assert data["name"] == "wanted"
189
-
190
-
191
177
  # =============================================================================
192
178
  # $preset sentinel expansion
193
179
  # =============================================================================
@@ -375,18 +361,6 @@ def test_find_files_by_schema_warns_on_malformed_json(tmp_path: Path) -> None:
375
361
  assert results == []
376
362
 
377
363
 
378
- def test_find_file_by_schema_and_name_warns_on_load_failure(tmp_path: Path) -> None:
379
- """A malformed file in the search directory must surface as a warning,
380
- not as a silent ``None`` (which would be indistinguishable from
381
- "the name didn't match anything")."""
382
- bad = tmp_path / "provider.json" # spec-named so discovery loads it
383
- bad.write_text("{ this is not json")
384
-
385
- with pytest.warns(DataFileLoadWarning, match="provider.json"):
386
- result = find_file_by_schema_and_name(tmp_path, "provider_v1", "name", "p")
387
- assert result is None
388
-
389
-
390
364
  def test_find_files_by_schema_can_be_promoted_to_error(tmp_path: Path) -> None:
391
365
  """Callers that want fail-fast can promote the warning to an exception."""
392
366
  import warnings as _warnings
File without changes
File without changes
File without changes