unitysvc-core 0.2.30__tar.gz → 0.2.32__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.30/src/unitysvc_core.egg-info → unitysvc_core-0.2.32}/PKG-INFO +1 -1
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/pyproject.toml +1 -1
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/pricing.py +19 -5
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/utils.py +52 -32
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/validator.py +40 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_pricing.py +50 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_utils.py +65 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_validator.py +29 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/LICENSE +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/README.md +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/setup.cfg +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/__init__.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/base.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/listing_data.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/service.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/validators.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/listing_v1.json +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/offering_v1.json +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_access_interface_routing.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_effective_price.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_hidden_path_skip.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_offering_data.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_provider_account_rate_limits.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_service_status.py +0 -0
- {unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/tests/test_validators.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.32"
|
|
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" }]
|
|
@@ -12,13 +12,14 @@ Contains:
|
|
|
12
12
|
from __future__ import annotations
|
|
13
13
|
|
|
14
14
|
import ast
|
|
15
|
+
import functools
|
|
15
16
|
import operator
|
|
16
17
|
import re
|
|
17
18
|
from collections.abc import Callable, Iterable
|
|
18
19
|
from decimal import Decimal, InvalidOperation
|
|
19
20
|
from typing import Annotated, Any, Literal
|
|
20
21
|
|
|
21
|
-
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
22
|
+
from pydantic import BaseModel, ConfigDict, Field, TypeAdapter, model_validator
|
|
22
23
|
from pydantic.functional_validators import BeforeValidator
|
|
23
24
|
|
|
24
25
|
|
|
@@ -1876,6 +1877,22 @@ Pricing = Annotated[
|
|
|
1876
1877
|
]
|
|
1877
1878
|
|
|
1878
1879
|
|
|
1880
|
+
@functools.cache
|
|
1881
|
+
def _pricing_adapter() -> TypeAdapter[Pricing]:
|
|
1882
|
+
"""The ``Pricing`` union's validator, built once and reused.
|
|
1883
|
+
|
|
1884
|
+
Building a ``TypeAdapter`` compiles the validation schema for every member
|
|
1885
|
+
of the union, which costs ~0.4-0.5 ms; validating with it afterwards costs
|
|
1886
|
+
microseconds. ``validate_pricing`` used to build one per call, and
|
|
1887
|
+
composite prices (``multiply``, ``add``, ...) validate their nested parts
|
|
1888
|
+
through it too, so pricing one usage event built it several times: ~2 ms
|
|
1889
|
+
of CPU per event in the usage consumer (unitysvc/unitysvc#2591). Built
|
|
1890
|
+
lazily rather than at import so ``Pricing``'s forward references are
|
|
1891
|
+
resolved first. ``TypeAdapter`` is safe to share across threads.
|
|
1892
|
+
"""
|
|
1893
|
+
return TypeAdapter(Pricing)
|
|
1894
|
+
|
|
1895
|
+
|
|
1879
1896
|
def validate_pricing(
|
|
1880
1897
|
data: dict[str, Any],
|
|
1881
1898
|
) -> (
|
|
@@ -1915,7 +1932,4 @@ def validate_pricing(
|
|
|
1915
1932
|
>>> validated = validate_pricing(data)
|
|
1916
1933
|
>>> print(validated.input) # "0.5"
|
|
1917
1934
|
"""
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
adapter: TypeAdapter[Pricing] = TypeAdapter(Pricing)
|
|
1921
|
-
return adapter.validate_python(data)
|
|
1935
|
+
return _pricing_adapter().validate_python(data)
|
|
@@ -11,6 +11,7 @@ and unitysvc backend, including:
|
|
|
11
11
|
|
|
12
12
|
import hashlib
|
|
13
13
|
import json
|
|
14
|
+
import mimetypes
|
|
14
15
|
import os
|
|
15
16
|
import tomllib
|
|
16
17
|
import warnings
|
|
@@ -104,52 +105,71 @@ def get_basename(filename: str) -> str:
|
|
|
104
105
|
|
|
105
106
|
|
|
106
107
|
def mime_type_to_extension(mime_type: str) -> str:
|
|
107
|
-
"""
|
|
108
|
+
"""File extension for a media type or a document-kind token.
|
|
109
|
+
|
|
110
|
+
Both forms arrive here. ``Document.mime_type`` is a :class:`MimeTypeEnum`,
|
|
111
|
+
whose values are document KINDS rather than media types -- ``png``,
|
|
112
|
+
``markdown``, ``python``, and ``url``, which is not a media type at all --
|
|
113
|
+
while other callers pass a real ``image/png``. A kind that fell through to
|
|
114
|
+
``txt`` meant a PNG stored under a ``.txt`` key and served as
|
|
115
|
+
``text/plain``, which browsers refuse to render under ``nosniff``.
|
|
116
|
+
|
|
117
|
+
Resolution order: the explicit table, then the stdlib (which knows every
|
|
118
|
+
image kind both as a media type and as an extension), then the media type's
|
|
119
|
+
subtype, then ``txt``. The fallback stays: an honest, uninformative
|
|
120
|
+
extension beats a guessed-wrong media type.
|
|
108
121
|
|
|
109
122
|
Args:
|
|
110
|
-
mime_type:
|
|
123
|
+
mime_type: A media type (``image/png``) or a kind token (``png``).
|
|
111
124
|
|
|
112
125
|
Returns:
|
|
113
|
-
File extension without dot
|
|
126
|
+
File extension without the dot.
|
|
114
127
|
"""
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
if not mime_type:
|
|
129
|
+
return "txt"
|
|
130
|
+
|
|
131
|
+
value = mime_type.strip().lower()
|
|
132
|
+
if not value:
|
|
133
|
+
return "txt"
|
|
134
|
+
|
|
135
|
+
# Spelled out where the stdlib is absent or disagrees with what we store:
|
|
136
|
+
# the languages have no media type it knows, ``url`` is not a media type,
|
|
137
|
+
# and we store ``.md`` and ``.jpg`` rather than ``.markdown`` and ``.jpeg``.
|
|
138
|
+
explicit = {
|
|
117
139
|
"text": "txt",
|
|
118
140
|
"plain": "txt",
|
|
119
141
|
"text/plain": "txt",
|
|
120
|
-
"text/html": "html",
|
|
121
142
|
"text/markdown": "md",
|
|
122
|
-
"text/csv": "csv",
|
|
123
|
-
"application/json": "json",
|
|
124
|
-
"application/pdf": "pdf",
|
|
125
|
-
"application/xml": "xml",
|
|
126
|
-
"application/x-yaml": "yaml",
|
|
127
|
-
"application/octet-stream": "bin",
|
|
128
|
-
"image/png": "png",
|
|
129
|
-
"image/jpeg": "jpg",
|
|
130
|
-
"image/gif": "gif",
|
|
131
143
|
"markdown": "md",
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
144
|
+
"image/jpeg": "jpg",
|
|
145
|
+
"jpeg": "jpg",
|
|
146
|
+
"python": "py",
|
|
147
|
+
"javascript": "js",
|
|
148
|
+
"bash": "sh",
|
|
136
149
|
"yaml": "yaml",
|
|
137
|
-
"
|
|
150
|
+
"application/x-yaml": "yaml",
|
|
151
|
+
"application/octet-stream": "bin",
|
|
138
152
|
"url": "url",
|
|
139
153
|
}
|
|
154
|
+
if value in explicit:
|
|
155
|
+
return explicit[value]
|
|
156
|
+
|
|
157
|
+
if "/" in value:
|
|
158
|
+
guessed = mimetypes.guess_extension(value)
|
|
159
|
+
if guessed:
|
|
160
|
+
return guessed.lstrip(".")
|
|
161
|
+
subtype = value.split("/", 1)[1]
|
|
162
|
+
if subtype in explicit:
|
|
163
|
+
return explicit[subtype]
|
|
164
|
+
if mimetypes.guess_type(f"x.{subtype}")[0]:
|
|
165
|
+
return subtype
|
|
166
|
+
return "txt"
|
|
167
|
+
|
|
168
|
+
# A bare token that is already a real extension: png, svg, webp, pdf, json,
|
|
169
|
+
# html, csv, xml. Resolved rather than listed, so the table stays short.
|
|
170
|
+
if mimetypes.guess_type(f"x.{value}")[0]:
|
|
171
|
+
return value
|
|
140
172
|
|
|
141
|
-
# Try exact match first
|
|
142
|
-
mime_lower = mime_type.lower()
|
|
143
|
-
if mime_lower in mime_map:
|
|
144
|
-
return mime_map[mime_lower]
|
|
145
|
-
|
|
146
|
-
# Try to extract from mime type parts
|
|
147
|
-
if "/" in mime_lower:
|
|
148
|
-
_, subtype = mime_lower.split("/", 1)
|
|
149
|
-
if subtype in mime_map:
|
|
150
|
-
return mime_map[subtype]
|
|
151
|
-
|
|
152
|
-
# Default to txt
|
|
153
173
|
return "txt"
|
|
154
174
|
|
|
155
175
|
|
|
@@ -12,6 +12,12 @@ from jsonschema.validators import Draft7Validator
|
|
|
12
12
|
|
|
13
13
|
from .utils import SCHEMA_TO_STEM, is_hidden_path, load_data_file, schema_for_path
|
|
14
14
|
|
|
15
|
+
# ``details.model``: a provider-neutral model name (``glm-5.2``, not
|
|
16
|
+
# ``zai-org/GLM-5.2`` or Fireworks' ``glm-5p2``). Lowercase, no ``org/``
|
|
17
|
+
# prefix. The backend re-checks the same pattern before it shows a name on a
|
|
18
|
+
# platform tier (``app/crud/platform_served.py``).
|
|
19
|
+
CANONICAL_MODEL_RE = re.compile(r"^[a-z0-9][a-z0-9._:-]*$")
|
|
20
|
+
|
|
15
21
|
# svcpass api_key dispositions (#1198, #1786). On an upstream_access_config
|
|
16
22
|
# interface these literal values are not credentials: "" / "__strip__" strip
|
|
17
23
|
# the svcpass-bearing header, "__forward__" forwards svcpass verbatim, and
|
|
@@ -665,6 +671,37 @@ class DataValidator:
|
|
|
665
671
|
|
|
666
672
|
return errors
|
|
667
673
|
|
|
674
|
+
def validate_offering_model_name(self, data: dict[str, Any], schema_name: str) -> list[str]:
|
|
675
|
+
"""Validate the optional ``details.model`` of an offering.
|
|
676
|
+
|
|
677
|
+
``details.model`` names the model an offering serves in a
|
|
678
|
+
provider-neutral way, independent of the provider's own spelling
|
|
679
|
+
in ``details.upstream_model``. A platform tier lists its members'
|
|
680
|
+
names as the models it is currently served by (unitysvc#2567).
|
|
681
|
+
|
|
682
|
+
Only the shape is checked (lowercase, no ``org/`` prefix); there
|
|
683
|
+
is no registry of allowed names. Absent is fine.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
data: The offering data being validated.
|
|
687
|
+
schema_name: The schema name (only ``offering_v1`` is checked).
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
List of validation error messages.
|
|
691
|
+
"""
|
|
692
|
+
if schema_name != "offering_v1":
|
|
693
|
+
return []
|
|
694
|
+
details = data.get("details")
|
|
695
|
+
if not isinstance(details, dict) or "model" not in details:
|
|
696
|
+
return []
|
|
697
|
+
value = details["model"]
|
|
698
|
+
if isinstance(value, str) and CANONICAL_MODEL_RE.match(value):
|
|
699
|
+
return []
|
|
700
|
+
return [
|
|
701
|
+
f"details.model must be a provider-neutral model name: lowercase letters, "
|
|
702
|
+
f"digits and '.', '_', ':', '-', with no 'org/' prefix (e.g. 'glm-5.2'), got {value!r}."
|
|
703
|
+
]
|
|
704
|
+
|
|
668
705
|
def validate_connectivity_test_exists(self, data: dict[str, Any], schema_name: str) -> list[str]:
|
|
669
706
|
"""Validate that at least one connectivity_test document exists.
|
|
670
707
|
|
|
@@ -921,6 +958,9 @@ class DataValidator:
|
|
|
921
958
|
llm_metadata_errors = self.validate_llm_offering_metadata(data, schema_name)
|
|
922
959
|
errors.extend(llm_metadata_errors)
|
|
923
960
|
|
|
961
|
+
# Validate the optional provider-neutral details.model (offering_v1 only)
|
|
962
|
+
errors.extend(self.validate_offering_model_name(data, schema_name))
|
|
963
|
+
|
|
924
964
|
# Validate MCP offerings declare an mcp upstream channel (offering_v1
|
|
925
965
|
# only); the customer-facing gateway interface is checked below with
|
|
926
966
|
# the other listing_v1 interface rules.
|
|
@@ -2156,3 +2156,53 @@ class TestChannelPriceData:
|
|
|
2156
2156
|
assert pricing.calculate_cost(usage, request_count=10, channel="managed") == Decimal("5")
|
|
2157
2157
|
assert pricing.calculate_cost(usage, request_count=10_000, channel="managed") == Decimal("50")
|
|
2158
2158
|
assert pricing.calculate_cost(usage, request_count=10_000, channel="byok") == Decimal("0")
|
|
2159
|
+
|
|
2160
|
+
|
|
2161
|
+
class TestValidatePricingAdapterIsBuiltOnce:
|
|
2162
|
+
"""``validate_pricing`` reuses one ``TypeAdapter`` (unitysvc/unitysvc#2591).
|
|
2163
|
+
|
|
2164
|
+
Building the adapter compiles the whole ``Pricing`` union's schema
|
|
2165
|
+
(~0.4-0.5 ms); the usage consumer validates several prices per event, so
|
|
2166
|
+
rebuilding it per call made billing CPU-bound.
|
|
2167
|
+
"""
|
|
2168
|
+
|
|
2169
|
+
def test_repeated_calls_build_the_adapter_once(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
2170
|
+
import unitysvc_core.models.pricing as pricing
|
|
2171
|
+
|
|
2172
|
+
built: list[object] = []
|
|
2173
|
+
real = pricing.TypeAdapter
|
|
2174
|
+
|
|
2175
|
+
def counting(*args: object, **kwargs: object) -> object:
|
|
2176
|
+
built.append(args)
|
|
2177
|
+
return real(*args, **kwargs)
|
|
2178
|
+
|
|
2179
|
+
pricing._pricing_adapter.cache_clear()
|
|
2180
|
+
monkeypatch.setattr(pricing, "TypeAdapter", counting)
|
|
2181
|
+
try:
|
|
2182
|
+
token = {"type": "one_million_tokens", "input": "0.5", "output": "1.5"}
|
|
2183
|
+
composite = {
|
|
2184
|
+
"type": "multiply",
|
|
2185
|
+
"factor": "1.05",
|
|
2186
|
+
"base": {"type": "one_million_tokens", "input": "0.5", "output": "1.5"},
|
|
2187
|
+
}
|
|
2188
|
+
for _ in range(50):
|
|
2189
|
+
assert validate_pricing(token).input == "0.5"
|
|
2190
|
+
validate_pricing(composite)
|
|
2191
|
+
finally:
|
|
2192
|
+
pricing._pricing_adapter.cache_clear()
|
|
2193
|
+
assert len(built) == 1
|
|
2194
|
+
|
|
2195
|
+
def test_results_match_a_fresh_adapter(self) -> None:
|
|
2196
|
+
from pydantic import TypeAdapter
|
|
2197
|
+
|
|
2198
|
+
from unitysvc_core.models.pricing import Pricing
|
|
2199
|
+
|
|
2200
|
+
for data in (
|
|
2201
|
+
{"type": "one_million_tokens", "input": "0.5", "output": "1.5"},
|
|
2202
|
+
{"type": "multiply", "factor": "2", "base": {"type": "constant", "price": "1"}},
|
|
2203
|
+
):
|
|
2204
|
+
assert validate_pricing(data) == TypeAdapter(Pricing).validate_python(data)
|
|
2205
|
+
|
|
2206
|
+
def test_invalid_data_still_raises(self) -> None:
|
|
2207
|
+
with pytest.raises(ValueError):
|
|
2208
|
+
validate_pricing({"type": "no_such_price_type"})
|
|
@@ -5,6 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
+
from unitysvc_core.models.base import MimeTypeEnum
|
|
8
9
|
from unitysvc_core.utils import (
|
|
9
10
|
DEFAULT_PRESET_FNS,
|
|
10
11
|
DataFileLoadWarning,
|
|
@@ -64,6 +65,70 @@ def test_mime_type_to_extension() -> None:
|
|
|
64
65
|
assert mime_type_to_extension("application/pdf") == "pdf"
|
|
65
66
|
|
|
66
67
|
|
|
68
|
+
@pytest.mark.parametrize(
|
|
69
|
+
("value", "expected"),
|
|
70
|
+
[
|
|
71
|
+
("image/png", "png"),
|
|
72
|
+
("image/jpeg", "jpg"),
|
|
73
|
+
("image/svg+xml", "svg"),
|
|
74
|
+
("image/webp", "webp"),
|
|
75
|
+
("text/markdown", "md"),
|
|
76
|
+
("text/plain", "txt"),
|
|
77
|
+
("application/json", "json"),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
def test_a_media_type_resolves_to_its_extension(value: str, expected: str) -> None:
|
|
81
|
+
assert mime_type_to_extension(value) == expected
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
("value", "expected"),
|
|
86
|
+
[
|
|
87
|
+
("markdown", "md"),
|
|
88
|
+
("python", "py"),
|
|
89
|
+
("javascript", "js"),
|
|
90
|
+
("bash", "sh"),
|
|
91
|
+
("html", "html"),
|
|
92
|
+
("json", "json"),
|
|
93
|
+
("text", "txt"),
|
|
94
|
+
("pdf", "pdf"),
|
|
95
|
+
("jpeg", "jpg"),
|
|
96
|
+
("png", "png"),
|
|
97
|
+
("svg", "svg"),
|
|
98
|
+
("webp", "webp"),
|
|
99
|
+
("url", "url"),
|
|
100
|
+
],
|
|
101
|
+
)
|
|
102
|
+
def test_a_document_kind_token_resolves_to_its_extension(
|
|
103
|
+
value: str, expected: str
|
|
104
|
+
) -> None:
|
|
105
|
+
"""``Document.mime_type`` is a ``MimeTypeEnum``, whose values are kinds
|
|
106
|
+
rather than media types. A kind that fell through to ``txt`` meant a PNG
|
|
107
|
+
stored under a ``.txt`` key and served as ``text/plain``."""
|
|
108
|
+
assert mime_type_to_extension(value) == expected
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def test_every_mime_type_enum_value_resolves() -> None:
|
|
112
|
+
"""The regression this guards: an unknown kind became ``txt`` silently."""
|
|
113
|
+
unresolved = [
|
|
114
|
+
kind.value
|
|
115
|
+
for kind in MimeTypeEnum
|
|
116
|
+
if kind is not MimeTypeEnum.text and mime_type_to_extension(kind.value) == "txt"
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
assert unresolved == []
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@pytest.mark.parametrize("value", ["", " ", "not-a-thing", "application/x-unknown"])
|
|
123
|
+
def test_an_unresolvable_value_falls_back_to_txt(value: str) -> None:
|
|
124
|
+
assert mime_type_to_extension(value) == "txt"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_case_and_whitespace_are_not_the_callers_problem() -> None:
|
|
128
|
+
assert mime_type_to_extension("PNG") == "png"
|
|
129
|
+
assert mime_type_to_extension(" image/png ") == "png"
|
|
130
|
+
|
|
131
|
+
|
|
67
132
|
# =============================================================================
|
|
68
133
|
# deep_merge_dicts
|
|
69
134
|
# =============================================================================
|
|
@@ -1164,6 +1164,35 @@ class TestAccessInterfaceNameValidation:
|
|
|
1164
1164
|
assert "'ok_two'" not in joined
|
|
1165
1165
|
|
|
1166
1166
|
|
|
1167
|
+
class TestOfferingModelNameValidation:
|
|
1168
|
+
"""Tests for ``validate_offering_model_name``: ``details.model`` is an
|
|
1169
|
+
optional, provider-neutral model name, checked for shape only."""
|
|
1170
|
+
|
|
1171
|
+
@pytest.mark.parametrize("name", ["glm-5.2", "kimi-k2.6", "deepseek-v4-pro", "qwen3:8b", "gpt_oss_120b"])
|
|
1172
|
+
def test_canonical_names_pass(self, schema_dir, example_data_dir, name):
|
|
1173
|
+
validator = DataValidator(example_data_dir, schema_dir)
|
|
1174
|
+
data = {"service_type": "llm", "details": {"model": name}}
|
|
1175
|
+
assert validator.validate_offering_model_name(data, "offering_v1") == []
|
|
1176
|
+
|
|
1177
|
+
def test_absent_passes(self, schema_dir, example_data_dir):
|
|
1178
|
+
validator = DataValidator(example_data_dir, schema_dir)
|
|
1179
|
+
assert validator.validate_offering_model_name({"details": {}}, "offering_v1") == []
|
|
1180
|
+
assert validator.validate_offering_model_name({}, "offering_v1") == []
|
|
1181
|
+
|
|
1182
|
+
@pytest.mark.parametrize("name", ["zai-org/glm-5.2", "GLM-5.2", "", "-glm", "glm 5.2", None, 5])
|
|
1183
|
+
def test_provider_spellings_rejected(self, schema_dir, example_data_dir, name):
|
|
1184
|
+
validator = DataValidator(example_data_dir, schema_dir)
|
|
1185
|
+
data = {"service_type": "llm", "details": {"model": name}}
|
|
1186
|
+
errors = validator.validate_offering_model_name(data, "offering_v1")
|
|
1187
|
+
assert len(errors) == 1
|
|
1188
|
+
assert "details.model" in errors[0]
|
|
1189
|
+
|
|
1190
|
+
def test_other_schemas_ignored(self, schema_dir, example_data_dir):
|
|
1191
|
+
validator = DataValidator(example_data_dir, schema_dir)
|
|
1192
|
+
data = {"details": {"model": "Not/Canonical"}}
|
|
1193
|
+
assert validator.validate_offering_model_name(data, "listing_v1") == []
|
|
1194
|
+
|
|
1195
|
+
|
|
1167
1196
|
class TestLLMOfferingMetadataValidation:
|
|
1168
1197
|
"""Tests for ``validate_llm_offering_metadata``.
|
|
1169
1198
|
|
|
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.30 → unitysvc_core-0.2.32}/src/unitysvc_core/models/service_group_data.py
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
|
{unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/src/unitysvc_core/schema/service_group_v1.json
RENAMED
|
File without changes
|
|
File without changes
|
{unitysvc_core-0.2.30 → unitysvc_core-0.2.32}/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
|
|
File without changes
|