unitysvc-core 0.2.25__tar.gz → 0.2.26__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.25/src/unitysvc_core.egg-info → unitysvc_core-0.2.26}/PKG-INFO +1 -1
  2. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/pyproject.toml +1 -1
  3. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/__init__.py +4 -4
  4. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/base.py +34 -16
  5. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/provider_data.py +8 -5
  6. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/service.py +5 -5
  7. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/provider_v1.json +21 -27
  8. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
  9. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_provider_account_rate_limits.py +59 -1
  10. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/LICENSE +0 -0
  11. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/README.md +0 -0
  12. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/setup.cfg +0 -0
  13. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/__init__.py +0 -0
  14. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/documents.py +0 -0
  15. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/listing_data.py +0 -0
  16. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/listing_v1.py +0 -0
  17. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/offering_data.py +0 -0
  18. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/offering_v1.py +0 -0
  19. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/pricing.py +0 -0
  20. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/promotion_data.py +0 -0
  21. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/provider_v1.py +0 -0
  22. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/service_group_data.py +0 -0
  23. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/models/validators.py +0 -0
  24. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/py.typed +0 -0
  25. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/base.json +0 -0
  26. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/listing_v1.json +0 -0
  27. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/offering_v1.json +0 -0
  28. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/promotion_v1.json +0 -0
  29. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/schema/service_group_v1.json +0 -0
  30. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/utils.py +0 -0
  31. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core/validator.py +0 -0
  32. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/SOURCES.txt +0 -0
  33. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
  34. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/requires.txt +0 -0
  35. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/src/unitysvc_core.egg-info/top_level.txt +0 -0
  36. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_access_interface_routing.py +0 -0
  37. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_hidden_path_skip.py +0 -0
  38. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_pricing.py +0 -0
  39. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_service_group.py +0 -0
  40. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_service_status.py +0 -0
  41. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_utils.py +0 -0
  42. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/tests/test_validator.py +0 -0
  43. {unitysvc_core-0.2.25 → unitysvc_core-0.2.26}/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.25
3
+ Version: 0.2.26
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.25"
7
+ version = "0.2.26"
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" }]
@@ -17,13 +17,13 @@ from .base import (
17
17
  PricingTypeEnum,
18
18
  ProviderStatusEnum,
19
19
  QuotaResetCycleEnum,
20
- RateLimitUnitEnum,
20
+ RateLimitUnit,
21
21
  RequestTransformEnum,
22
22
  SellerTypeEnum,
23
23
  ServiceGroupStatusEnum,
24
24
  ServiceTypeEnum,
25
25
  ServiceVisibilityEnum,
26
- TimeWindowEnum,
26
+ TimeWindow,
27
27
  UpstreamStatusEnum, # Backwards compatibility alias for OfferingStatusEnum
28
28
  )
29
29
  from .documents import DocumentData
@@ -130,14 +130,14 @@ __all__ = [
130
130
  "PricingTypeEnum",
131
131
  "ProviderStatusEnum",
132
132
  "QuotaResetCycleEnum",
133
- "RateLimitUnitEnum",
133
+ "RateLimitUnit",
134
134
  "RequestTransformEnum",
135
135
  "ROUTABLE_GROUP_TYPES",
136
136
  "SellerTypeEnum",
137
137
  "ServiceGroupStatusEnum",
138
138
  "ServiceTypeEnum",
139
139
  "ServiceVisibilityEnum",
140
- "TimeWindowEnum",
140
+ "TimeWindow",
141
141
  "UpstreamStatusEnum", # Backwards compatibility alias for OfferingStatusEnum
142
142
  # Pricing — primitives
143
143
  "PriceStr",
@@ -1,6 +1,7 @@
1
1
  """Enum types and basic definitions shared across the data models.
2
2
 
3
- This module contains only enums and simple constants. Data classes with
3
+ This module contains only enums, closed-value type aliases, and simple
4
+ constants. Data classes with
4
5
  behavior (pricing, documents, service constraints, etc.) and validation
5
6
  functions live in their own modules:
6
7
 
@@ -13,6 +14,7 @@ functions live in their own modules:
13
14
  from __future__ import annotations
14
15
 
15
16
  from enum import StrEnum
17
+ from typing import Literal
16
18
 
17
19
 
18
20
  class AccessMethodEnum(StrEnum):
@@ -234,13 +236,37 @@ class QuotaResetCycleEnum(StrEnum):
234
236
  yearly = "yearly"
235
237
 
236
238
 
237
- class RateLimitUnitEnum(StrEnum):
238
- requests = "requests"
239
- tokens = "tokens"
240
- input_tokens = "input_tokens"
241
- output_tokens = "output_tokens"
242
- bytes = "bytes"
243
- concurrent = "concurrent"
239
+ # Rate-limit unit and time window: Literals, not StrEnums.
240
+ #
241
+ # Both are used by ``ProviderAccountRateLimit``, which is nested inside
242
+ # ``ProviderData`` — a model the unitysvc backend serves in BOTH directions (a
243
+ # request-body field on ``POST /seller/services``, a response field on
244
+ # ``GET /seller/services/{id}``).
245
+ #
246
+ # An enum one level down makes pydantic treat the *outer* model's validation and
247
+ # serialization schemas as distinct, so FastAPI emits an ``X-Input``/``X-Output``
248
+ # pair — both halves carrying the same ``title``. Python SDK codegen names a
249
+ # top-level component from its title, so the halves collide on one class name:
250
+ # the generator drops the second and silently removes every schema that
251
+ # ``$ref``s it. That is how ``ServiceDetailResponse`` vanished from
252
+ # unitysvc-sellers 0.3.7+, leaving ``client.services.get()`` returning raw dicts
253
+ # (unitysvc/unitysvc-sellers#205).
254
+ #
255
+ # An enum *directly* on a served model is fine, and the rendered JSON schema is
256
+ # identical either way — the split is an artifact of how the two modes are
257
+ # deduplicated, not a real difference. A Literal sidesteps it and keeps the
258
+ # values just as closed: same ``enum`` in the JSON schema, same rejection of an
259
+ # unlisted value, and a string-literal union in every generated client.
260
+ RateLimitUnit = Literal[
261
+ "requests",
262
+ "tokens",
263
+ "input_tokens",
264
+ "output_tokens",
265
+ "bytes",
266
+ "concurrent",
267
+ ]
268
+
269
+ TimeWindow = Literal["second", "minute", "hour", "day", "month"]
244
270
 
245
271
 
246
272
  class RequestTransformEnum(StrEnum):
@@ -317,14 +343,6 @@ class ServiceVisibilityEnum(StrEnum):
317
343
  private = "private"
318
344
 
319
345
 
320
- class TimeWindowEnum(StrEnum):
321
- second = "second"
322
- minute = "minute"
323
- hour = "hour"
324
- day = "day"
325
- month = "month"
326
-
327
-
328
346
  class OfferingStatusEnum(StrEnum):
329
347
  """
330
348
  Status values that sellers can set for service offerings.
@@ -13,7 +13,7 @@ from typing import Any
13
13
 
14
14
  from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl, model_validator
15
15
 
16
- from .base import ProviderStatusEnum, RateLimitUnitEnum, TimeWindowEnum
16
+ from .base import ProviderStatusEnum, RateLimitUnit, TimeWindow
17
17
 
18
18
 
19
19
  class ProviderAccountRateLimit(BaseModel):
@@ -48,9 +48,12 @@ class ProviderAccountRateLimit(BaseModel):
48
48
 
49
49
  limit: int = Field(gt=0, description="Maximum allowed — in flight for `concurrent`, per window otherwise")
50
50
 
51
- unit: RateLimitUnitEnum = Field(description="What is being limited (requests, tokens, concurrent, …)")
51
+ # RateLimitUnit / TimeWindow are Literals rather than enums, because an enum
52
+ # nested this deep splits every model that carries one. See base.py for the
53
+ # mechanism — it is not a style preference.
54
+ unit: RateLimitUnit = Field(description="What is being limited (requests, tokens, concurrent, …)")
52
55
 
53
- window: TimeWindowEnum | None = Field(
56
+ window: TimeWindow | None = Field(
54
57
  default=None,
55
58
  description="Time window. Omitted for `concurrent`, which is a gauge rather than a counter.",
56
59
  )
@@ -72,11 +75,11 @@ class ProviderAccountRateLimit(BaseModel):
72
75
  it. Requiring the field to match the unit stops a limit being authored
73
76
  that the enforcement layer cannot honour as written.
74
77
  """
75
- if self.unit is RateLimitUnitEnum.concurrent:
78
+ if self.unit == "concurrent":
76
79
  if self.window is not None:
77
80
  raise ValueError("`concurrent` is an in-flight gauge and takes no window; omit it")
78
81
  elif self.window is None:
79
- raise ValueError(f"`{self.unit.value}` is counted over a window; set `window`")
82
+ raise ValueError(f"`{self.unit}` is counted over a window; set `window`")
80
83
  return self
81
84
 
82
85
 
@@ -14,8 +14,8 @@ from .base import (
14
14
  ContentFilterEnum,
15
15
  OveragePolicyEnum,
16
16
  QuotaResetCycleEnum,
17
- RateLimitUnitEnum,
18
- TimeWindowEnum,
17
+ RateLimitUnit,
18
+ TimeWindow,
19
19
  )
20
20
 
21
21
 
@@ -26,8 +26,8 @@ class RateLimit(BaseModel):
26
26
 
27
27
  # Core rate limit definition
28
28
  limit: int = Field(description="Maximum allowed in the time window")
29
- unit: RateLimitUnitEnum = Field(description="What is being limited")
30
- window: TimeWindowEnum = Field(description="Time window for the limit")
29
+ unit: RateLimitUnit = Field(description="What is being limited")
30
+ window: TimeWindow = Field(description="Time window for the limit")
31
31
 
32
32
  # Optional additional info
33
33
  description: str | None = Field(default=None, max_length=255, description="Human-readable description")
@@ -52,7 +52,7 @@ class ServiceConstraints(BaseModel):
52
52
  # Usage Quotas & Billing
53
53
  monthly_quota: int | None = Field(default=None, description="Monthly usage quota (requests, tokens, etc.)")
54
54
  daily_quota: int | None = Field(default=None, description="Daily usage quota (requests, tokens, etc.)")
55
- quota_unit: RateLimitUnitEnum | None = Field(default=None, description="Unit for quota limits")
55
+ quota_unit: RateLimitUnit | None = Field(default=None, description="Unit for quota limits")
56
56
  quota_reset_cycle: QuotaResetCycleEnum | None = Field(default=None, description="How often quotas reset")
57
57
  overage_policy: OveragePolicyEnum | None = Field(default=None, description="What happens when quota is exceeded")
58
58
 
@@ -187,20 +187,37 @@
187
187
  "type": "string"
188
188
  },
189
189
  "unit": {
190
- "$ref": "#/$defs/RateLimitUnitEnum",
191
- "description": "What is being limited (requests, tokens, concurrent, …)"
190
+ "description": "What is being limited (requests, tokens, concurrent, …)",
191
+ "enum": [
192
+ "requests",
193
+ "tokens",
194
+ "input_tokens",
195
+ "output_tokens",
196
+ "bytes",
197
+ "concurrent"
198
+ ],
199
+ "title": "Unit",
200
+ "type": "string"
192
201
  },
193
202
  "window": {
194
203
  "anyOf": [
195
204
  {
196
- "$ref": "#/$defs/TimeWindowEnum"
205
+ "enum": [
206
+ "second",
207
+ "minute",
208
+ "hour",
209
+ "day",
210
+ "month"
211
+ ],
212
+ "type": "string"
197
213
  },
198
214
  {
199
215
  "type": "null"
200
216
  }
201
217
  ],
202
218
  "default": null,
203
- "description": "Time window. Omitted for `concurrent`, which is a gauge rather than a counter."
219
+ "description": "Time window. Omitted for `concurrent`, which is a gauge rather than a counter.",
220
+ "title": "Window"
204
221
  }
205
222
  },
206
223
  "required": [
@@ -221,18 +238,6 @@
221
238
  "title": "ProviderStatusEnum",
222
239
  "type": "string"
223
240
  },
224
- "RateLimitUnitEnum": {
225
- "enum": [
226
- "requests",
227
- "tokens",
228
- "input_tokens",
229
- "output_tokens",
230
- "bytes",
231
- "concurrent"
232
- ],
233
- "title": "RateLimitUnitEnum",
234
- "type": "string"
235
- },
236
241
  "ServicesPopulator": {
237
242
  "additionalProperties": false,
238
243
  "description": "Configuration for automatically populating service data.",
@@ -289,17 +294,6 @@
289
294
  },
290
295
  "title": "ServicesPopulator",
291
296
  "type": "object"
292
- },
293
- "TimeWindowEnum": {
294
- "enum": [
295
- "second",
296
- "minute",
297
- "hour",
298
- "day",
299
- "month"
300
- ],
301
- "title": "TimeWindowEnum",
302
- "type": "string"
303
297
  }
304
298
  },
305
299
  "additionalProperties": false,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-core
3
- Version: 0.2.25
3
+ Version: 0.2.26
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>
@@ -7,10 +7,13 @@ can state an individual customer's share, which depends on who is active at
7
7
  request time.
8
8
  """
9
9
 
10
+ from typing import get_args
11
+
10
12
  import pytest
11
13
  from pydantic import ValidationError
12
14
 
13
15
  from unitysvc_core.models import ProviderAccountRateLimit, ProviderV1
16
+ from unitysvc_core.models.base import RateLimitUnit, TimeWindow
14
17
 
15
18
  BASE = {
16
19
  "name": "fireworks",
@@ -84,8 +87,63 @@ def test_several_dimensions_coexist() -> None:
84
87
  {"name": "fireworks_input_tokens", "limit": 60000, "unit": "input_tokens", "window": "minute"},
85
88
  ],
86
89
  )
87
- assert [rl.unit.value for rl in provider.rate_limits] == [
90
+ assert [rl.unit for rl in provider.rate_limits] == [
88
91
  "concurrent",
89
92
  "requests",
90
93
  "input_tokens",
91
94
  ]
95
+
96
+
97
+ def test_the_value_sets_are_the_documented_ones() -> None:
98
+ """``RateLimitUnit``/``TimeWindow`` are the only definition of these sets.
99
+
100
+ They replaced ``RateLimitUnitEnum``/``TimeWindowEnum`` (see base.py for why
101
+ a Literal and not an enum). Pinning the members here makes a silent
102
+ widening or narrowing fail rather than quietly changing what a provider may
103
+ author and what every generated client will accept.
104
+ """
105
+ assert set(get_args(RateLimitUnit)) == {
106
+ "requests",
107
+ "tokens",
108
+ "input_tokens",
109
+ "output_tokens",
110
+ "bytes",
111
+ "concurrent",
112
+ }
113
+ assert set(get_args(TimeWindow)) == {"second", "minute", "hour", "day", "month"}
114
+
115
+
116
+ def test_the_nested_model_pulls_in_no_named_subschema() -> None:
117
+ """The condition that broke Python SDK codegen — kept from coming back.
118
+
119
+ An enum (or any named sub-schema) one level down makes pydantic treat the
120
+ *outer* model's validation and serialization schemas as distinct. FastAPI
121
+ then splits every model that carries this one into an ``X-Input``/
122
+ ``X-Output`` pair whose halves share a title, and Python SDK codegen drops
123
+ one of them plus everything referencing it — silently. ``ProviderData`` is
124
+ such a carrier: it is a request body on ``POST /seller/services`` and a
125
+ response field on ``GET /seller/services/{id}``.
126
+ """
127
+ assert ProviderAccountRateLimit.model_json_schema().get("$defs", {}) == {}
128
+
129
+
130
+ def test_the_value_set_is_still_closed() -> None:
131
+ with pytest.raises(ValidationError):
132
+ ProviderAccountRateLimit(name="fireworks_typo", limit=10, unit="concurent")
133
+ with pytest.raises(ValidationError):
134
+ ProviderAccountRateLimit(name="fireworks_rpm", limit=10, unit="requests", window="fortnight")
135
+
136
+
137
+ def test_values_read_back_as_plain_strings() -> None:
138
+ """The one behaviour change from dropping the enums.
139
+
140
+ ``.unit``/``.window`` are ``str``, not enum members, so ``.unit.value`` no
141
+ longer works — ``.unit`` does. Consumers already saw strings over the wire
142
+ and in ``model_dump(mode="json")``; only in-process attribute access
143
+ differs.
144
+ """
145
+ limit = ProviderAccountRateLimit(name="fireworks_rpm", limit=600, unit="requests", window="minute")
146
+
147
+ assert limit.unit == "requests"
148
+ assert type(limit.unit) is str
149
+ assert limit.model_dump(mode="json")["window"] == "minute"
File without changes
File without changes
File without changes