unitysvc-core 0.2.2__tar.gz → 0.2.4__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.2/src/unitysvc_core.egg-info → unitysvc_core-0.2.4}/PKG-INFO +1 -1
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/pyproject.toml +1 -1
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/__init__.py +4 -2
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/base.py +2 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/listing_data.py +2 -2
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/pricing.py +102 -7
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/service.py +16 -15
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/schema/listing_v1.json +76 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/schema/offering_v1.json +76 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4/src/unitysvc_core.egg-info}/PKG-INFO +1 -1
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core.egg-info/SOURCES.txt +1 -1
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/tests/test_pricing.py +121 -0
- unitysvc_core-0.2.2/tests/test_service_data.py → unitysvc_core-0.2.4/tests/test_service_status.py +6 -6
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/LICENSE +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/README.md +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/setup.cfg +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/__init__.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/documents.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/listing_v1.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/offering_data.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/offering_v1.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/promotion_data.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/provider_data.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/provider_v1.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/service_group_data.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/models/validators.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/py.typed +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/schema/base.json +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/schema/promotion_v1.json +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/schema/provider_v1.json +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/schema/service_group_v1.json +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/utils.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core/validator.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core.egg-info/dependency_links.txt +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core.egg-info/requires.txt +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/src/unitysvc_core.egg-info/top_level.txt +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/tests/test_service_group.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/tests/test_utils.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/tests/test_validator.py +0 -0
- {unitysvc_core-0.2.2 → unitysvc_core-0.2.4}/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.4"
|
|
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" }]
|
|
@@ -43,6 +43,7 @@ from .pricing import (
|
|
|
43
43
|
ImagePriceData,
|
|
44
44
|
MaxPriceData,
|
|
45
45
|
MinPriceData,
|
|
46
|
+
ModePriceData,
|
|
46
47
|
MultiplyPriceData,
|
|
47
48
|
PercentageStr,
|
|
48
49
|
PriceStr,
|
|
@@ -70,7 +71,7 @@ from .service import (
|
|
|
70
71
|
AccessInterfaceData,
|
|
71
72
|
RateLimit,
|
|
72
73
|
ServiceConstraints,
|
|
73
|
-
|
|
74
|
+
ServiceStatus,
|
|
74
75
|
UpstreamAccessConfigData,
|
|
75
76
|
)
|
|
76
77
|
from .service_group_data import (
|
|
@@ -110,7 +111,7 @@ __all__ = [
|
|
|
110
111
|
"UpstreamAccessConfigData",
|
|
111
112
|
"RateLimit",
|
|
112
113
|
"ServiceConstraints",
|
|
113
|
-
"
|
|
114
|
+
"ServiceStatus",
|
|
114
115
|
# Enums
|
|
115
116
|
"AccessMethodEnum",
|
|
116
117
|
"AuthMethodEnum",
|
|
@@ -158,6 +159,7 @@ __all__ = [
|
|
|
158
159
|
"MultiplyPriceData",
|
|
159
160
|
"MaxPriceData",
|
|
160
161
|
"MinPriceData",
|
|
162
|
+
"ModePriceData",
|
|
161
163
|
"FirstPriceData",
|
|
162
164
|
"TieredPriceData",
|
|
163
165
|
"GraduatedPriceData",
|
|
@@ -216,6 +216,8 @@ class PricingTypeEnum(StrEnum):
|
|
|
216
216
|
graduated = "graduated" # Graduated tiers (each tier's units at that rate)
|
|
217
217
|
# Expression-based pricing (payout_price only)
|
|
218
218
|
expr = "expr" # Arbitrary expression using usage metrics
|
|
219
|
+
# Mode-based pricing: select a sub-price by the request's resolved mode
|
|
220
|
+
mode = "mode" # Per-mode pricing keyed by mode name (e.g. managed, byok)
|
|
219
221
|
|
|
220
222
|
|
|
221
223
|
class QuotaResetCycleEnum(StrEnum):
|
|
@@ -44,8 +44,8 @@ class ServiceListingData(BaseModel):
|
|
|
44
44
|
|
|
45
45
|
# NOTE: a listing carries no service_id. Service identity is backend-
|
|
46
46
|
# assigned provenance, not authored listing content — it lives in
|
|
47
|
-
# ``service.json``
|
|
48
|
-
#
|
|
47
|
+
# ``service.json`` (the ``ServiceStatus`` sidecar) and travels separately
|
|
48
|
+
# on upload/revision, never embedded in the listing.
|
|
49
49
|
|
|
50
50
|
# Listing identification
|
|
51
51
|
name: str | None = Field(
|
|
@@ -280,6 +280,7 @@ class TokenPriceData(BasePriceData):
|
|
|
280
280
|
usage: UsageData,
|
|
281
281
|
customer_charge: Decimal | None = None,
|
|
282
282
|
request_count: int | None = None,
|
|
283
|
+
mode: str | None = None,
|
|
283
284
|
) -> Decimal:
|
|
284
285
|
"""Calculate cost for token-based pricing.
|
|
285
286
|
|
|
@@ -346,6 +347,7 @@ class TimePriceData(BasePriceData):
|
|
|
346
347
|
usage: UsageData,
|
|
347
348
|
customer_charge: Decimal | None = None,
|
|
348
349
|
request_count: int | None = None,
|
|
350
|
+
mode: str | None = None,
|
|
349
351
|
) -> Decimal:
|
|
350
352
|
"""Calculate cost for time-based pricing.
|
|
351
353
|
|
|
@@ -386,6 +388,7 @@ class ImagePriceData(BasePriceData):
|
|
|
386
388
|
usage: UsageData,
|
|
387
389
|
customer_charge: Decimal | None = None,
|
|
388
390
|
request_count: int | None = None,
|
|
391
|
+
mode: str | None = None,
|
|
389
392
|
) -> Decimal:
|
|
390
393
|
"""Calculate cost for image-based pricing.
|
|
391
394
|
|
|
@@ -422,6 +425,7 @@ class StepPriceData(BasePriceData):
|
|
|
422
425
|
usage: UsageData,
|
|
423
426
|
customer_charge: Decimal | None = None,
|
|
424
427
|
request_count: int | None = None,
|
|
428
|
+
mode: str | None = None,
|
|
425
429
|
) -> Decimal:
|
|
426
430
|
"""Calculate cost for step-based pricing.
|
|
427
431
|
|
|
@@ -461,6 +465,7 @@ class DataPriceData(BasePriceData):
|
|
|
461
465
|
usage: UsageData,
|
|
462
466
|
customer_charge: Decimal | None = None,
|
|
463
467
|
request_count: int | None = None,
|
|
468
|
+
mode: str | None = None,
|
|
464
469
|
) -> Decimal:
|
|
465
470
|
"""Calculate cost for data-volume pricing.
|
|
466
471
|
|
|
@@ -504,6 +509,7 @@ class CountPriceData(BasePriceData):
|
|
|
504
509
|
usage: UsageData,
|
|
505
510
|
customer_charge: Decimal | None = None,
|
|
506
511
|
request_count: int | None = None,
|
|
512
|
+
mode: str | None = None,
|
|
507
513
|
) -> Decimal:
|
|
508
514
|
"""Calculate cost for count-scaled pricing.
|
|
509
515
|
|
|
@@ -566,6 +572,7 @@ class RevenueSharePriceData(BasePriceData):
|
|
|
566
572
|
usage: UsageData,
|
|
567
573
|
customer_charge: Decimal | None = None,
|
|
568
574
|
request_count: int | None = None,
|
|
575
|
+
mode: str | None = None,
|
|
569
576
|
) -> Decimal:
|
|
570
577
|
"""Calculate cost for revenue share pricing.
|
|
571
578
|
|
|
@@ -605,6 +612,7 @@ class ConstantPriceData(BasePriceData):
|
|
|
605
612
|
usage: UsageData,
|
|
606
613
|
customer_charge: Decimal | None = None,
|
|
607
614
|
request_count: int | None = None,
|
|
615
|
+
mode: str | None = None,
|
|
608
616
|
) -> Decimal:
|
|
609
617
|
"""Return the constant price regardless of usage.
|
|
610
618
|
|
|
@@ -677,6 +685,7 @@ class AddPriceData(BasePriceData):
|
|
|
677
685
|
usage: UsageData,
|
|
678
686
|
customer_charge: Decimal | None = None,
|
|
679
687
|
request_count: int | None = None,
|
|
688
|
+
mode: str | None = None,
|
|
680
689
|
) -> Decimal:
|
|
681
690
|
"""Calculate total cost by summing all price components.
|
|
682
691
|
|
|
@@ -691,7 +700,7 @@ class AddPriceData(BasePriceData):
|
|
|
691
700
|
total = Decimal("0")
|
|
692
701
|
for price_data in self.prices:
|
|
693
702
|
component = validate_pricing(price_data)
|
|
694
|
-
total += component.calculate_cost(usage, customer_charge, request_count)
|
|
703
|
+
total += component.calculate_cost(usage, customer_charge, request_count, mode)
|
|
695
704
|
return total
|
|
696
705
|
|
|
697
706
|
|
|
@@ -736,6 +745,7 @@ class MultiplyPriceData(BasePriceData):
|
|
|
736
745
|
usage: UsageData,
|
|
737
746
|
customer_charge: Decimal | None = None,
|
|
738
747
|
request_count: int | None = None,
|
|
748
|
+
mode: str | None = None,
|
|
739
749
|
) -> Decimal:
|
|
740
750
|
"""Calculate cost by multiplying base price by factor.
|
|
741
751
|
|
|
@@ -748,7 +758,7 @@ class MultiplyPriceData(BasePriceData):
|
|
|
748
758
|
Base cost multiplied by factor
|
|
749
759
|
"""
|
|
750
760
|
base_pricing = validate_pricing(self.base)
|
|
751
|
-
base_cost = base_pricing.calculate_cost(usage, customer_charge, request_count)
|
|
761
|
+
base_cost = base_pricing.calculate_cost(usage, customer_charge, request_count, mode)
|
|
752
762
|
return base_cost * Decimal(self.factor)
|
|
753
763
|
|
|
754
764
|
|
|
@@ -789,6 +799,7 @@ class MaxPriceData(BasePriceData):
|
|
|
789
799
|
usage: UsageData,
|
|
790
800
|
customer_charge: Decimal | None = None,
|
|
791
801
|
request_count: int | None = None,
|
|
802
|
+
mode: str | None = None,
|
|
792
803
|
) -> Decimal:
|
|
793
804
|
"""Return the highest cost among applicable children.
|
|
794
805
|
|
|
@@ -799,7 +810,7 @@ class MaxPriceData(BasePriceData):
|
|
|
799
810
|
for price_data in self.prices:
|
|
800
811
|
try:
|
|
801
812
|
component = validate_pricing(price_data)
|
|
802
|
-
costs.append(component.calculate_cost(usage, customer_charge, request_count))
|
|
813
|
+
costs.append(component.calculate_cost(usage, customer_charge, request_count, mode))
|
|
803
814
|
except ValueError:
|
|
804
815
|
continue
|
|
805
816
|
if not costs:
|
|
@@ -846,6 +857,7 @@ class MinPriceData(BasePriceData):
|
|
|
846
857
|
usage: UsageData,
|
|
847
858
|
customer_charge: Decimal | None = None,
|
|
848
859
|
request_count: int | None = None,
|
|
860
|
+
mode: str | None = None,
|
|
849
861
|
) -> Decimal:
|
|
850
862
|
"""Return the lowest cost among applicable children.
|
|
851
863
|
|
|
@@ -856,7 +868,7 @@ class MinPriceData(BasePriceData):
|
|
|
856
868
|
for price_data in self.prices:
|
|
857
869
|
try:
|
|
858
870
|
component = validate_pricing(price_data)
|
|
859
|
-
costs.append(component.calculate_cost(usage, customer_charge, request_count))
|
|
871
|
+
costs.append(component.calculate_cost(usage, customer_charge, request_count, mode))
|
|
860
872
|
except ValueError:
|
|
861
873
|
continue
|
|
862
874
|
if not costs:
|
|
@@ -902,6 +914,7 @@ class FirstPriceData(BasePriceData):
|
|
|
902
914
|
usage: UsageData,
|
|
903
915
|
customer_charge: Decimal | None = None,
|
|
904
916
|
request_count: int | None = None,
|
|
917
|
+
mode: str | None = None,
|
|
905
918
|
) -> Decimal:
|
|
906
919
|
"""Return the cost from the first child that can handle the usage.
|
|
907
920
|
|
|
@@ -912,7 +925,7 @@ class FirstPriceData(BasePriceData):
|
|
|
912
925
|
for price_data in self.prices:
|
|
913
926
|
try:
|
|
914
927
|
component = validate_pricing(price_data)
|
|
915
|
-
return component.calculate_cost(usage, customer_charge, request_count)
|
|
928
|
+
return component.calculate_cost(usage, customer_charge, request_count, mode)
|
|
916
929
|
except ValueError:
|
|
917
930
|
continue
|
|
918
931
|
raise ValueError("No child pricing could handle the provided usage data")
|
|
@@ -1127,6 +1140,7 @@ class ExprPriceData(BasePriceData):
|
|
|
1127
1140
|
usage: UsageData,
|
|
1128
1141
|
customer_charge: Decimal | None = None,
|
|
1129
1142
|
request_count: int | None = None,
|
|
1143
|
+
mode: str | None = None,
|
|
1130
1144
|
) -> Decimal:
|
|
1131
1145
|
"""Calculate cost by evaluating the expression with usage data.
|
|
1132
1146
|
|
|
@@ -1200,6 +1214,7 @@ class TieredPriceData(BasePriceData):
|
|
|
1200
1214
|
usage: UsageData,
|
|
1201
1215
|
customer_charge: Decimal | None = None,
|
|
1202
1216
|
request_count: int | None = None,
|
|
1217
|
+
mode: str | None = None,
|
|
1203
1218
|
) -> Decimal:
|
|
1204
1219
|
"""Calculate cost based on which tier the usage falls into.
|
|
1205
1220
|
|
|
@@ -1217,7 +1232,7 @@ class TieredPriceData(BasePriceData):
|
|
|
1217
1232
|
for tier in self.tiers:
|
|
1218
1233
|
if tier.up_to is None or metric_value <= tier.up_to:
|
|
1219
1234
|
tier_pricing = validate_pricing(tier.price)
|
|
1220
|
-
return tier_pricing.calculate_cost(usage, customer_charge, request_count)
|
|
1235
|
+
return tier_pricing.calculate_cost(usage, customer_charge, request_count, mode)
|
|
1221
1236
|
|
|
1222
1237
|
# Should not reach here if tiers are properly configured
|
|
1223
1238
|
raise ValueError("No matching tier found")
|
|
@@ -1282,6 +1297,7 @@ class GraduatedPriceData(BasePriceData):
|
|
|
1282
1297
|
usage: UsageData,
|
|
1283
1298
|
customer_charge: Decimal | None = None,
|
|
1284
1299
|
request_count: int | None = None,
|
|
1300
|
+
mode: str | None = None,
|
|
1285
1301
|
) -> Decimal:
|
|
1286
1302
|
"""Calculate cost with graduated pricing across tiers.
|
|
1287
1303
|
|
|
@@ -1317,6 +1333,83 @@ class GraduatedPriceData(BasePriceData):
|
|
|
1317
1333
|
return total_cost
|
|
1318
1334
|
|
|
1319
1335
|
|
|
1336
|
+
class ModePriceData(BasePriceData):
|
|
1337
|
+
"""
|
|
1338
|
+
Mode-based pricing that selects a sub-price by the request's resolved mode.
|
|
1339
|
+
|
|
1340
|
+
The active mode is decided elsewhere — e.g. the gateway picks which keyed
|
|
1341
|
+
``upstream_access_config`` a request routes through (``"managed"``,
|
|
1342
|
+
``"byok"``, ``"byoe"``, …) — and threads it into
|
|
1343
|
+
``calculate_cost(..., mode=...)``. The matching entry in ``modes`` is
|
|
1344
|
+
evaluated; when the mode is unknown or unset, the ``default`` mode is used.
|
|
1345
|
+
|
|
1346
|
+
There is no special-casing on the consumer side: every pricing type accepts
|
|
1347
|
+
an optional ``mode`` and a mode-independent price simply ignores it, so a
|
|
1348
|
+
plain price behaves identically for all modes. Mode-dependent pricing is
|
|
1349
|
+
just this one extra optional pricing type.
|
|
1350
|
+
|
|
1351
|
+
Each ``modes`` value is itself any pricing object (flat, composite, tiered,
|
|
1352
|
+
or even nested mode-based), validated lazily — mirroring how tiered pricing
|
|
1353
|
+
holds per-tier prices.
|
|
1354
|
+
|
|
1355
|
+
Example:
|
|
1356
|
+
{
|
|
1357
|
+
"type": "mode",
|
|
1358
|
+
"default": "managed",
|
|
1359
|
+
"modes": {
|
|
1360
|
+
"managed": {"type": "one_million_tokens", "input": "0.50", "output": "1.50"},
|
|
1361
|
+
"byok": {"type": "constant", "price": "0"}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
"""
|
|
1365
|
+
|
|
1366
|
+
type: Literal["mode"] = "mode"
|
|
1367
|
+
|
|
1368
|
+
default: str = Field(
|
|
1369
|
+
description="Mode key evaluated when the request's mode is unknown or unset. "
|
|
1370
|
+
"Must be one of the keys in `modes`.",
|
|
1371
|
+
)
|
|
1372
|
+
|
|
1373
|
+
modes: dict[str, dict[str, Any]] = Field(
|
|
1374
|
+
description="Per-mode pricing keyed by mode name (e.g. 'managed', 'byok'). "
|
|
1375
|
+
"Each value is a nested pricing object of any type.",
|
|
1376
|
+
min_length=1,
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
@model_validator(mode="after")
|
|
1380
|
+
def _validate_default_and_price(self) -> ModePriceData:
|
|
1381
|
+
"""Require ``default`` to exist and auto-compute the nominal price from it."""
|
|
1382
|
+
if self.default not in self.modes:
|
|
1383
|
+
raise ValueError(f"default mode {self.default!r} is not defined in modes ({sorted(self.modes)})")
|
|
1384
|
+
if self.price is None:
|
|
1385
|
+
self.price = _extract_nominal_price(self.modes[self.default])
|
|
1386
|
+
return self
|
|
1387
|
+
|
|
1388
|
+
def calculate_cost(
|
|
1389
|
+
self,
|
|
1390
|
+
usage: UsageData,
|
|
1391
|
+
customer_charge: Decimal | None = None,
|
|
1392
|
+
request_count: int | None = None,
|
|
1393
|
+
mode: str | None = None,
|
|
1394
|
+
) -> Decimal:
|
|
1395
|
+
"""Evaluate the price for the resolved ``mode`` (falling back to ``default``).
|
|
1396
|
+
|
|
1397
|
+
Args:
|
|
1398
|
+
usage: Usage data passed to the selected mode's price.
|
|
1399
|
+
customer_charge: Customer charge passed through.
|
|
1400
|
+
request_count: Request count passed through.
|
|
1401
|
+
mode: The resolved mode; unknown/``None`` falls back to ``default``.
|
|
1402
|
+
|
|
1403
|
+
Returns:
|
|
1404
|
+
Cost from the selected mode's pricing.
|
|
1405
|
+
"""
|
|
1406
|
+
selected = self.modes.get(mode) if mode is not None else None
|
|
1407
|
+
if selected is None:
|
|
1408
|
+
selected = self.modes[self.default]
|
|
1409
|
+
pricing = validate_pricing(selected)
|
|
1410
|
+
return pricing.calculate_cost(usage, customer_charge, request_count, mode)
|
|
1411
|
+
|
|
1412
|
+
|
|
1320
1413
|
# Discriminated union of all pricing types
|
|
1321
1414
|
# This is the type used for payout_price and list_price fields
|
|
1322
1415
|
# Note: ExprPriceData should only be used for payout_price
|
|
@@ -1336,7 +1429,8 @@ Pricing = Annotated[
|
|
|
1336
1429
|
| FirstPriceData
|
|
1337
1430
|
| TieredPriceData
|
|
1338
1431
|
| GraduatedPriceData
|
|
1339
|
-
| ExprPriceData
|
|
1432
|
+
| ExprPriceData
|
|
1433
|
+
| ModePriceData,
|
|
1340
1434
|
Field(discriminator="type"),
|
|
1341
1435
|
]
|
|
1342
1436
|
|
|
@@ -1360,6 +1454,7 @@ def validate_pricing(
|
|
|
1360
1454
|
| TieredPriceData
|
|
1361
1455
|
| GraduatedPriceData
|
|
1362
1456
|
| ExprPriceData
|
|
1457
|
+
| ModePriceData
|
|
1363
1458
|
):
|
|
1364
1459
|
"""
|
|
1365
1460
|
Validate pricing dict and return the appropriate typed model.
|
|
@@ -144,21 +144,22 @@ class UpstreamAccessConfigData(AccessInterfaceData):
|
|
|
144
144
|
)
|
|
145
145
|
|
|
146
146
|
|
|
147
|
-
class
|
|
148
|
-
"""Backend-assigned service identity
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
the
|
|
154
|
-
the
|
|
155
|
-
|
|
156
|
-
existing service.
|
|
157
|
-
|
|
158
|
-
Of these fields,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
unknown keys from
|
|
147
|
+
class ServiceStatus(BaseModel):
|
|
148
|
+
"""Backend-assigned service **status / identity sidecar** (the ``service.json``
|
|
149
|
+
file) — *not* the authored service data.
|
|
150
|
+
|
|
151
|
+
A service's ``provider_data`` / ``offering_data`` / ``listing_data`` are the
|
|
152
|
+
authored service data; this is the *other*, backend-owned half — the status
|
|
153
|
+
record the backend materializes once a service exists. It is the
|
|
154
|
+
**round-trip** sidecar: the ingest task returns it, the seller stores it in
|
|
155
|
+
``service.json`` beside the spec files, and replays it on the next
|
|
156
|
+
upload/revision so the backend can match the upload to the existing service.
|
|
157
|
+
|
|
158
|
+
Of these fields, ``service_id`` and ``template_instance_id`` are consumed on
|
|
159
|
+
the way *in* (they declare which service / which template the publish
|
|
160
|
+
targets); the rest are populated on the way *out* as informational status /
|
|
161
|
+
provenance for the seller. ``extra="ignore"`` keeps unknown keys from
|
|
162
|
+
breaking either direction.
|
|
162
163
|
"""
|
|
163
164
|
|
|
164
165
|
model_config = ConfigDict(extra="ignore")
|
|
@@ -964,6 +964,78 @@
|
|
|
964
964
|
"title": "MinPriceData",
|
|
965
965
|
"type": "object"
|
|
966
966
|
},
|
|
967
|
+
"ModePriceData": {
|
|
968
|
+
"additionalProperties": false,
|
|
969
|
+
"description": "Mode-based pricing that selects a sub-price by the request's resolved mode.\n\nThe active mode is decided elsewhere — e.g. the gateway picks which keyed\n``upstream_access_config`` a request routes through (``\"managed\"``,\n``\"byok\"``, ``\"byoe\"``, …) — and threads it into\n``calculate_cost(..., mode=...)``. The matching entry in ``modes`` is\nevaluated; when the mode is unknown or unset, the ``default`` mode is used.\n\nThere is no special-casing on the consumer side: every pricing type accepts\nan optional ``mode`` and a mode-independent price simply ignores it, so a\nplain price behaves identically for all modes. Mode-dependent pricing is\njust this one extra optional pricing type.\n\nEach ``modes`` value is itself any pricing object (flat, composite, tiered,\nor even nested mode-based), validated lazily — mirroring how tiered pricing\nholds per-tier prices.\n\nExample:\n {\n \"type\": \"mode\",\n \"default\": \"managed\",\n \"modes\": {\n \"managed\": {\"type\": \"one_million_tokens\", \"input\": \"0.50\", \"output\": \"1.50\"},\n \"byok\": {\"type\": \"constant\", \"price\": \"0\"}\n }\n }",
|
|
970
|
+
"properties": {
|
|
971
|
+
"default": {
|
|
972
|
+
"description": "Mode key evaluated when the request's mode is unknown or unset. Must be one of the keys in `modes`.",
|
|
973
|
+
"title": "Default",
|
|
974
|
+
"type": "string"
|
|
975
|
+
},
|
|
976
|
+
"description": {
|
|
977
|
+
"anyOf": [
|
|
978
|
+
{
|
|
979
|
+
"type": "string"
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
"type": "null"
|
|
983
|
+
}
|
|
984
|
+
],
|
|
985
|
+
"default": null,
|
|
986
|
+
"description": "Human-readable description of the pricing model",
|
|
987
|
+
"title": "Description"
|
|
988
|
+
},
|
|
989
|
+
"modes": {
|
|
990
|
+
"additionalProperties": {
|
|
991
|
+
"additionalProperties": true,
|
|
992
|
+
"type": "object"
|
|
993
|
+
},
|
|
994
|
+
"description": "Per-mode pricing keyed by mode name (e.g. 'managed', 'byok'). Each value is a nested pricing object of any type.",
|
|
995
|
+
"minProperties": 1,
|
|
996
|
+
"title": "Modes",
|
|
997
|
+
"type": "object"
|
|
998
|
+
},
|
|
999
|
+
"price": {
|
|
1000
|
+
"anyOf": [
|
|
1001
|
+
{
|
|
1002
|
+
"type": "string"
|
|
1003
|
+
},
|
|
1004
|
+
{
|
|
1005
|
+
"type": "null"
|
|
1006
|
+
}
|
|
1007
|
+
],
|
|
1008
|
+
"default": null,
|
|
1009
|
+
"description": "Summary price for marketplace comparison and sorting. For simple pricing types this is the billing rate. For composite types (add, multiply, tiered, graduated) this is a seller-provided nominal value for marketplace display.",
|
|
1010
|
+
"title": "Price"
|
|
1011
|
+
},
|
|
1012
|
+
"reference": {
|
|
1013
|
+
"anyOf": [
|
|
1014
|
+
{
|
|
1015
|
+
"type": "string"
|
|
1016
|
+
},
|
|
1017
|
+
{
|
|
1018
|
+
"type": "null"
|
|
1019
|
+
}
|
|
1020
|
+
],
|
|
1021
|
+
"default": null,
|
|
1022
|
+
"description": "URL to upstream provider's pricing page",
|
|
1023
|
+
"title": "Reference"
|
|
1024
|
+
},
|
|
1025
|
+
"type": {
|
|
1026
|
+
"const": "mode",
|
|
1027
|
+
"default": "mode",
|
|
1028
|
+
"title": "Type",
|
|
1029
|
+
"type": "string"
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
"required": [
|
|
1033
|
+
"default",
|
|
1034
|
+
"modes"
|
|
1035
|
+
],
|
|
1036
|
+
"title": "ModePriceData",
|
|
1037
|
+
"type": "object"
|
|
1038
|
+
},
|
|
967
1039
|
"MultiplyPriceData": {
|
|
968
1040
|
"additionalProperties": false,
|
|
969
1041
|
"description": "Composite pricing that multiplies a base price by a factor.\n\nUseful for applying percentage-based adjustments to a base price.\n\nExample:\n {\n \"type\": \"multiply\",\n \"factor\": \"0.70\",\n \"base\": {\"type\": \"one_million_tokens\", \"input\": \"0.50\", \"output\": \"1.50\"}\n }",
|
|
@@ -1867,6 +1939,7 @@
|
|
|
1867
1939
|
"image": "#/$defs/ImagePriceData",
|
|
1868
1940
|
"max": "#/$defs/MaxPriceData",
|
|
1869
1941
|
"min": "#/$defs/MinPriceData",
|
|
1942
|
+
"mode": "#/$defs/ModePriceData",
|
|
1870
1943
|
"multiply": "#/$defs/MultiplyPriceData",
|
|
1871
1944
|
"one_byte": "#/$defs/DataPriceData",
|
|
1872
1945
|
"one_day": "#/$defs/TimePriceData",
|
|
@@ -1936,6 +2009,9 @@
|
|
|
1936
2009
|
},
|
|
1937
2010
|
{
|
|
1938
2011
|
"$ref": "#/$defs/ExprPriceData"
|
|
2012
|
+
},
|
|
2013
|
+
{
|
|
2014
|
+
"$ref": "#/$defs/ModePriceData"
|
|
1939
2015
|
}
|
|
1940
2016
|
]
|
|
1941
2017
|
},
|
|
@@ -834,6 +834,78 @@
|
|
|
834
834
|
"title": "MinPriceData",
|
|
835
835
|
"type": "object"
|
|
836
836
|
},
|
|
837
|
+
"ModePriceData": {
|
|
838
|
+
"additionalProperties": false,
|
|
839
|
+
"description": "Mode-based pricing that selects a sub-price by the request's resolved mode.\n\nThe active mode is decided elsewhere — e.g. the gateway picks which keyed\n``upstream_access_config`` a request routes through (``\"managed\"``,\n``\"byok\"``, ``\"byoe\"``, …) — and threads it into\n``calculate_cost(..., mode=...)``. The matching entry in ``modes`` is\nevaluated; when the mode is unknown or unset, the ``default`` mode is used.\n\nThere is no special-casing on the consumer side: every pricing type accepts\nan optional ``mode`` and a mode-independent price simply ignores it, so a\nplain price behaves identically for all modes. Mode-dependent pricing is\njust this one extra optional pricing type.\n\nEach ``modes`` value is itself any pricing object (flat, composite, tiered,\nor even nested mode-based), validated lazily — mirroring how tiered pricing\nholds per-tier prices.\n\nExample:\n {\n \"type\": \"mode\",\n \"default\": \"managed\",\n \"modes\": {\n \"managed\": {\"type\": \"one_million_tokens\", \"input\": \"0.50\", \"output\": \"1.50\"},\n \"byok\": {\"type\": \"constant\", \"price\": \"0\"}\n }\n }",
|
|
840
|
+
"properties": {
|
|
841
|
+
"default": {
|
|
842
|
+
"description": "Mode key evaluated when the request's mode is unknown or unset. Must be one of the keys in `modes`.",
|
|
843
|
+
"title": "Default",
|
|
844
|
+
"type": "string"
|
|
845
|
+
},
|
|
846
|
+
"description": {
|
|
847
|
+
"anyOf": [
|
|
848
|
+
{
|
|
849
|
+
"type": "string"
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
"type": "null"
|
|
853
|
+
}
|
|
854
|
+
],
|
|
855
|
+
"default": null,
|
|
856
|
+
"description": "Human-readable description of the pricing model",
|
|
857
|
+
"title": "Description"
|
|
858
|
+
},
|
|
859
|
+
"modes": {
|
|
860
|
+
"additionalProperties": {
|
|
861
|
+
"additionalProperties": true,
|
|
862
|
+
"type": "object"
|
|
863
|
+
},
|
|
864
|
+
"description": "Per-mode pricing keyed by mode name (e.g. 'managed', 'byok'). Each value is a nested pricing object of any type.",
|
|
865
|
+
"minProperties": 1,
|
|
866
|
+
"title": "Modes",
|
|
867
|
+
"type": "object"
|
|
868
|
+
},
|
|
869
|
+
"price": {
|
|
870
|
+
"anyOf": [
|
|
871
|
+
{
|
|
872
|
+
"type": "string"
|
|
873
|
+
},
|
|
874
|
+
{
|
|
875
|
+
"type": "null"
|
|
876
|
+
}
|
|
877
|
+
],
|
|
878
|
+
"default": null,
|
|
879
|
+
"description": "Summary price for marketplace comparison and sorting. For simple pricing types this is the billing rate. For composite types (add, multiply, tiered, graduated) this is a seller-provided nominal value for marketplace display.",
|
|
880
|
+
"title": "Price"
|
|
881
|
+
},
|
|
882
|
+
"reference": {
|
|
883
|
+
"anyOf": [
|
|
884
|
+
{
|
|
885
|
+
"type": "string"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
"type": "null"
|
|
889
|
+
}
|
|
890
|
+
],
|
|
891
|
+
"default": null,
|
|
892
|
+
"description": "URL to upstream provider's pricing page",
|
|
893
|
+
"title": "Reference"
|
|
894
|
+
},
|
|
895
|
+
"type": {
|
|
896
|
+
"const": "mode",
|
|
897
|
+
"default": "mode",
|
|
898
|
+
"title": "Type",
|
|
899
|
+
"type": "string"
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
"required": [
|
|
903
|
+
"default",
|
|
904
|
+
"modes"
|
|
905
|
+
],
|
|
906
|
+
"title": "ModePriceData",
|
|
907
|
+
"type": "object"
|
|
908
|
+
},
|
|
837
909
|
"MultiplyPriceData": {
|
|
838
910
|
"additionalProperties": false,
|
|
839
911
|
"description": "Composite pricing that multiplies a base price by a factor.\n\nUseful for applying percentage-based adjustments to a base price.\n\nExample:\n {\n \"type\": \"multiply\",\n \"factor\": \"0.70\",\n \"base\": {\"type\": \"one_million_tokens\", \"input\": \"0.50\", \"output\": \"1.50\"}\n }",
|
|
@@ -1949,6 +2021,7 @@
|
|
|
1949
2021
|
"image": "#/$defs/ImagePriceData",
|
|
1950
2022
|
"max": "#/$defs/MaxPriceData",
|
|
1951
2023
|
"min": "#/$defs/MinPriceData",
|
|
2024
|
+
"mode": "#/$defs/ModePriceData",
|
|
1952
2025
|
"multiply": "#/$defs/MultiplyPriceData",
|
|
1953
2026
|
"one_byte": "#/$defs/DataPriceData",
|
|
1954
2027
|
"one_day": "#/$defs/TimePriceData",
|
|
@@ -2018,6 +2091,9 @@
|
|
|
2018
2091
|
},
|
|
2019
2092
|
{
|
|
2020
2093
|
"$ref": "#/$defs/ExprPriceData"
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
"$ref": "#/$defs/ModePriceData"
|
|
2021
2097
|
}
|
|
2022
2098
|
]
|
|
2023
2099
|
},
|
|
@@ -31,8 +31,8 @@ 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
|
|
35
34
|
tests/test_service_group.py
|
|
35
|
+
tests/test_service_status.py
|
|
36
36
|
tests/test_utils.py
|
|
37
37
|
tests/test_validator.py
|
|
38
38
|
tests/test_validators.py
|
|
@@ -1806,3 +1806,124 @@ class TestNominalPriceAutoCompute:
|
|
|
1806
1806
|
)
|
|
1807
1807
|
# Explicit price on the child is preserved
|
|
1808
1808
|
assert pricing.price == "9.00"
|
|
1809
|
+
|
|
1810
|
+
|
|
1811
|
+
class TestModePriceData:
|
|
1812
|
+
"""Tests for mode-based pricing (select a sub-price by resolved mode)."""
|
|
1813
|
+
|
|
1814
|
+
def _managed_byok(self) -> dict:
|
|
1815
|
+
"""Canonical managed+byok: managed is paid, byok is free.
|
|
1816
|
+
|
|
1817
|
+
Uses ``constant`` sub-prices so the tests exercise mode *selection*
|
|
1818
|
+
independently of any usage-metric math.
|
|
1819
|
+
"""
|
|
1820
|
+
return {
|
|
1821
|
+
"type": "mode",
|
|
1822
|
+
"default": "managed",
|
|
1823
|
+
"modes": {
|
|
1824
|
+
"managed": {"type": "constant", "price": "6.00"},
|
|
1825
|
+
"byok": {"type": "constant", "price": "0"},
|
|
1826
|
+
},
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
def test_selects_matching_mode(self) -> None:
|
|
1830
|
+
"""The resolved mode picks that mode's price."""
|
|
1831
|
+
pricing = validate_pricing(self._managed_byok())
|
|
1832
|
+
usage = UsageData()
|
|
1833
|
+
|
|
1834
|
+
assert pricing.calculate_cost(usage, mode="managed") == Decimal("6.00")
|
|
1835
|
+
assert pricing.calculate_cost(usage, mode="byok") == Decimal("0")
|
|
1836
|
+
|
|
1837
|
+
def test_unknown_mode_falls_back_to_default(self) -> None:
|
|
1838
|
+
"""An unrecognised mode evaluates the default mode."""
|
|
1839
|
+
pricing = validate_pricing(self._managed_byok())
|
|
1840
|
+
|
|
1841
|
+
assert pricing.calculate_cost(UsageData(), mode="nonexistent") == Decimal("6.00")
|
|
1842
|
+
|
|
1843
|
+
def test_none_mode_falls_back_to_default(self) -> None:
|
|
1844
|
+
"""No mode (the common case for non-mode-aware callers) uses the default."""
|
|
1845
|
+
pricing = validate_pricing(self._managed_byok())
|
|
1846
|
+
|
|
1847
|
+
assert pricing.calculate_cost(UsageData()) == Decimal("6.00")
|
|
1848
|
+
|
|
1849
|
+
def test_non_mode_price_ignores_mode(self) -> None:
|
|
1850
|
+
"""A plain (mode-independent) price yields the same cost for every mode."""
|
|
1851
|
+
pricing = validate_pricing({"type": "constant", "price": "6.00"})
|
|
1852
|
+
usage = UsageData()
|
|
1853
|
+
|
|
1854
|
+
base = pricing.calculate_cost(usage)
|
|
1855
|
+
assert pricing.calculate_cost(usage, mode="byok") == base
|
|
1856
|
+
assert pricing.calculate_cost(usage, mode="managed") == base
|
|
1857
|
+
assert base == Decimal("6.00")
|
|
1858
|
+
|
|
1859
|
+
def test_default_must_exist(self) -> None:
|
|
1860
|
+
"""`default` must name one of the modes."""
|
|
1861
|
+
with pytest.raises(ValueError, match="default mode"):
|
|
1862
|
+
validate_pricing(
|
|
1863
|
+
{
|
|
1864
|
+
"type": "mode",
|
|
1865
|
+
"default": "missing",
|
|
1866
|
+
"modes": {"managed": {"type": "constant", "price": "1"}},
|
|
1867
|
+
}
|
|
1868
|
+
)
|
|
1869
|
+
|
|
1870
|
+
def test_nominal_price_from_default_mode(self) -> None:
|
|
1871
|
+
"""The summary price auto-computes from the default mode when unset."""
|
|
1872
|
+
pricing = validate_pricing(self._managed_byok())
|
|
1873
|
+
assert pricing.price == "6.00" # managed (default) summary price
|
|
1874
|
+
|
|
1875
|
+
def test_explicit_nominal_price_preserved(self) -> None:
|
|
1876
|
+
"""A seller-provided summary price is not overwritten."""
|
|
1877
|
+
data = self._managed_byok()
|
|
1878
|
+
data["price"] = "1.50"
|
|
1879
|
+
pricing = validate_pricing(data)
|
|
1880
|
+
assert pricing.price == "1.50"
|
|
1881
|
+
|
|
1882
|
+
def test_mode_threads_through_composite(self) -> None:
|
|
1883
|
+
"""`mode` propagates into nested children (e.g. a mode price inside `add`)."""
|
|
1884
|
+
pricing = validate_pricing(
|
|
1885
|
+
{
|
|
1886
|
+
"type": "add",
|
|
1887
|
+
"prices": [
|
|
1888
|
+
{
|
|
1889
|
+
"type": "mode",
|
|
1890
|
+
"default": "managed",
|
|
1891
|
+
"modes": {
|
|
1892
|
+
"managed": {"type": "constant", "price": "10"},
|
|
1893
|
+
"byok": {"type": "constant", "price": "0"},
|
|
1894
|
+
},
|
|
1895
|
+
},
|
|
1896
|
+
{"type": "constant", "price": "1.00"}, # flat platform fee
|
|
1897
|
+
],
|
|
1898
|
+
}
|
|
1899
|
+
)
|
|
1900
|
+
usage = UsageData()
|
|
1901
|
+
|
|
1902
|
+
# byok: 0 (mode price) + 1.00 (fee); managed: 10 + 1.00
|
|
1903
|
+
assert pricing.calculate_cost(usage, mode="byok") == Decimal("1.00")
|
|
1904
|
+
assert pricing.calculate_cost(usage, mode="managed") == Decimal("11.00")
|
|
1905
|
+
|
|
1906
|
+
def test_mode_price_can_nest_tiered(self) -> None:
|
|
1907
|
+
"""A mode's pricing may itself be any pricing type (here: tiered)."""
|
|
1908
|
+
pricing = validate_pricing(
|
|
1909
|
+
{
|
|
1910
|
+
"type": "mode",
|
|
1911
|
+
"default": "managed",
|
|
1912
|
+
"modes": {
|
|
1913
|
+
"managed": {
|
|
1914
|
+
"type": "tiered",
|
|
1915
|
+
"based_on": "request_count",
|
|
1916
|
+
"tiers": [
|
|
1917
|
+
{"up_to": 100, "price": {"type": "constant", "price": "5"}},
|
|
1918
|
+
{"up_to": None, "price": {"type": "constant", "price": "50"}},
|
|
1919
|
+
],
|
|
1920
|
+
},
|
|
1921
|
+
"byok": {"type": "constant", "price": "0"},
|
|
1922
|
+
},
|
|
1923
|
+
}
|
|
1924
|
+
)
|
|
1925
|
+
usage = UsageData()
|
|
1926
|
+
|
|
1927
|
+
assert pricing.calculate_cost(usage, request_count=10, mode="managed") == Decimal("5")
|
|
1928
|
+
assert pricing.calculate_cost(usage, request_count=10_000, mode="managed") == Decimal("50")
|
|
1929
|
+
assert pricing.calculate_cost(usage, request_count=10_000, mode="byok") == Decimal("0")
|
unitysvc_core-0.2.2/tests/test_service_data.py → unitysvc_core-0.2.4/tests/test_service_status.py
RENAMED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
"""Tests for the
|
|
1
|
+
"""Tests for the ServiceStatus model (the service.json provenance structure)."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from datetime import datetime
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
|
-
from unitysvc_core.models import
|
|
8
|
+
from unitysvc_core.models import ServiceStatus
|
|
9
9
|
|
|
10
10
|
_SID = "2c1f9e7a-0000-0000-0000-000000000000"
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def test_empty_service_data_has_no_identity() -> None:
|
|
14
|
-
sd =
|
|
14
|
+
sd = ServiceStatus()
|
|
15
15
|
assert sd.service_id is None
|
|
16
16
|
assert sd.time_created is None
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def test_service_id_coerced_to_uuid() -> None:
|
|
20
|
-
sd =
|
|
20
|
+
sd = ServiceStatus.model_validate({"service_id": _SID})
|
|
21
21
|
assert sd.service_id == UUID(_SID)
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def test_time_created_and_extra_fields() -> None:
|
|
25
25
|
# time_created is a recognized provenance field; unknown extras are
|
|
26
26
|
# tolerated (extra="ignore") rather than rejected.
|
|
27
|
-
sd =
|
|
27
|
+
sd = ServiceStatus.model_validate({"service_id": _SID, "time_created": "2026-01-01T00:00:00", "note": "x"})
|
|
28
28
|
assert sd.time_created == datetime(2026, 1, 1, 0, 0)
|
|
29
29
|
assert "note" not in sd.model_dump()
|
|
30
30
|
|
|
@@ -32,7 +32,7 @@ def test_time_created_and_extra_fields() -> None:
|
|
|
32
32
|
def test_full_identity_record_round_trips() -> None:
|
|
33
33
|
# The shape the ingest task returns and the seller persists to service.json.
|
|
34
34
|
rev = "11111111-0000-0000-0000-000000000000"
|
|
35
|
-
sd =
|
|
35
|
+
sd = ServiceStatus.model_validate(
|
|
36
36
|
{
|
|
37
37
|
"service_id": _SID,
|
|
38
38
|
"revision_of": rev,
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|