unitysvc-services 0.1.1__py3-none-any.whl → 0.1.5__py3-none-any.whl
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_services/api.py +321 -0
- unitysvc_services/cli.py +2 -1
- unitysvc_services/format_data.py +2 -7
- unitysvc_services/list.py +14 -43
- unitysvc_services/models/base.py +169 -102
- unitysvc_services/models/listing_v1.py +25 -9
- unitysvc_services/models/provider_v1.py +19 -8
- unitysvc_services/models/seller_v1.py +10 -8
- unitysvc_services/models/service_v1.py +8 -1
- unitysvc_services/populate.py +20 -6
- unitysvc_services/publisher.py +897 -462
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +577 -384
- unitysvc_services/test.py +769 -0
- unitysvc_services/update.py +4 -13
- unitysvc_services/utils.py +55 -6
- unitysvc_services/validator.py +117 -86
- unitysvc_services-0.1.5.dist-info/METADATA +182 -0
- unitysvc_services-0.1.5.dist-info/RECORD +26 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/entry_points.txt +1 -0
- unitysvc_services-0.1.1.dist-info/METADATA +0 -173
- unitysvc_services-0.1.1.dist-info/RECORD +0 -23
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/WHEEL +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/top_level.txt +0 -0
unitysvc_services/models/base.py
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import re
|
1
2
|
from enum import StrEnum
|
2
3
|
from typing import Any
|
3
4
|
|
@@ -210,6 +211,7 @@ class ProviderStatusEnum(StrEnum):
|
|
210
211
|
"""Provider status enum."""
|
211
212
|
|
212
213
|
active = "active"
|
214
|
+
pending = "pending"
|
213
215
|
disabled = "disabled"
|
214
216
|
incomplete = "incomplete" # Provider information is incomplete
|
215
217
|
|
@@ -218,6 +220,7 @@ class SellerStatusEnum(StrEnum):
|
|
218
220
|
"""Seller status enum."""
|
219
221
|
|
220
222
|
active = "active"
|
223
|
+
pending = "pending"
|
221
224
|
disabled = "disabled"
|
222
225
|
incomplete = "incomplete" # Seller information is incomplete
|
223
226
|
|
@@ -228,16 +231,10 @@ class Document(BaseModel):
|
|
228
231
|
# fields that will be stored in backend database
|
229
232
|
#
|
230
233
|
title: str = Field(min_length=5, max_length=255, description="Document title")
|
231
|
-
description: str | None = Field(
|
232
|
-
default=None, max_length=500, description="Document description"
|
233
|
-
)
|
234
|
+
description: str | None = Field(default=None, max_length=500, description="Document description")
|
234
235
|
mime_type: MimeTypeEnum = Field(description="Document MIME type")
|
235
|
-
version: str | None = Field(
|
236
|
-
|
237
|
-
)
|
238
|
-
category: DocumentCategoryEnum = Field(
|
239
|
-
description="Document category for organization and filtering"
|
240
|
-
)
|
236
|
+
version: str | None = Field(default=None, max_length=50, description="Document version")
|
237
|
+
category: DocumentCategoryEnum = Field(description="Document category for organization and filtering")
|
241
238
|
meta: dict[str, Any] | None = Field(
|
242
239
|
default=None,
|
243
240
|
description="JSON containing operation stats",
|
@@ -258,6 +255,18 @@ class Document(BaseModel):
|
|
258
255
|
default=False,
|
259
256
|
description="Whether document is publicly accessible without authentication",
|
260
257
|
)
|
258
|
+
requirements: list[str] | None = Field(
|
259
|
+
default=None,
|
260
|
+
description="Required packages/modules for running this code example (e.g., ['openai', 'httpx'])",
|
261
|
+
)
|
262
|
+
expect: str | None = Field(
|
263
|
+
default=None,
|
264
|
+
max_length=500,
|
265
|
+
description=(
|
266
|
+
"Expected output substring for code example validation. "
|
267
|
+
"If specified, test passes only if stdout contains this string."
|
268
|
+
),
|
269
|
+
)
|
261
270
|
|
262
271
|
|
263
272
|
class RateLimit(BaseModel):
|
@@ -271,12 +280,8 @@ class RateLimit(BaseModel):
|
|
271
280
|
window: TimeWindowEnum = Field(description="Time window for the limit")
|
272
281
|
|
273
282
|
# Optional additional info
|
274
|
-
description: str | None = Field(
|
275
|
-
|
276
|
-
)
|
277
|
-
burst_limit: int | None = Field(
|
278
|
-
default=None, description="Short-term burst allowance"
|
279
|
-
)
|
283
|
+
description: str | None = Field(default=None, max_length=255, description="Human-readable description")
|
284
|
+
burst_limit: int | None = Field(default=None, description="Short-term burst allowance")
|
280
285
|
|
281
286
|
# Status
|
282
287
|
is_active: bool = Field(default=True, description="Whether rate limit is active")
|
@@ -286,105 +291,57 @@ class ServiceConstraints(BaseModel):
|
|
286
291
|
model_config = ConfigDict(extra="forbid")
|
287
292
|
|
288
293
|
# Usage Quotas & Billing
|
289
|
-
monthly_quota: int | None = Field(
|
290
|
-
|
291
|
-
)
|
292
|
-
|
293
|
-
|
294
|
-
)
|
295
|
-
quota_unit: RateLimitUnitEnum | None = Field(
|
296
|
-
default=None, description="Unit for quota limits"
|
297
|
-
)
|
298
|
-
quota_reset_cycle: QuotaResetCycleEnum | None = Field(
|
299
|
-
default=None, description="How often quotas reset"
|
300
|
-
)
|
301
|
-
overage_policy: OveragePolicyEnum | None = Field(
|
302
|
-
default=None, description="What happens when quota is exceeded"
|
303
|
-
)
|
294
|
+
monthly_quota: int | None = Field(default=None, description="Monthly usage quota (requests, tokens, etc.)")
|
295
|
+
daily_quota: int | None = Field(default=None, description="Daily usage quota (requests, tokens, etc.)")
|
296
|
+
quota_unit: RateLimitUnitEnum | None = Field(default=None, description="Unit for quota limits")
|
297
|
+
quota_reset_cycle: QuotaResetCycleEnum | None = Field(default=None, description="How often quotas reset")
|
298
|
+
overage_policy: OveragePolicyEnum | None = Field(default=None, description="What happens when quota is exceeded")
|
304
299
|
|
305
300
|
# Authentication & Security
|
306
|
-
auth_methods: list[AuthMethodEnum] | None = Field(
|
307
|
-
|
308
|
-
)
|
309
|
-
ip_whitelist_required: bool | None = Field(
|
310
|
-
default=None, description="Whether IP whitelisting is required"
|
311
|
-
)
|
312
|
-
tls_version_min: str | None = Field(
|
313
|
-
default=None, description="Minimum TLS version required"
|
314
|
-
)
|
301
|
+
auth_methods: list[AuthMethodEnum] | None = Field(default=None, description="Supported authentication methods")
|
302
|
+
ip_whitelist_required: bool | None = Field(default=None, description="Whether IP whitelisting is required")
|
303
|
+
tls_version_min: str | None = Field(default=None, description="Minimum TLS version required")
|
315
304
|
|
316
305
|
# Request/Response Constraints
|
317
|
-
max_request_size_bytes: int | None = Field(
|
318
|
-
|
319
|
-
)
|
320
|
-
|
321
|
-
default=None, description="Maximum response payload size in bytes"
|
322
|
-
)
|
323
|
-
timeout_seconds: int | None = Field(
|
324
|
-
default=None, description="Request timeout in seconds"
|
325
|
-
)
|
326
|
-
max_batch_size: int | None = Field(
|
327
|
-
default=None, description="Maximum number of items in batch requests"
|
328
|
-
)
|
306
|
+
max_request_size_bytes: int | None = Field(default=None, description="Maximum request payload size in bytes")
|
307
|
+
max_response_size_bytes: int | None = Field(default=None, description="Maximum response payload size in bytes")
|
308
|
+
timeout_seconds: int | None = Field(default=None, description="Request timeout in seconds")
|
309
|
+
max_batch_size: int | None = Field(default=None, description="Maximum number of items in batch requests")
|
329
310
|
|
330
311
|
# Content & Model Restrictions
|
331
312
|
content_filters: list[ContentFilterEnum] | None = Field(
|
332
313
|
default=None, description="Active content filtering policies"
|
333
314
|
)
|
334
|
-
input_languages: list[str] | None = Field(
|
335
|
-
|
336
|
-
)
|
337
|
-
output_languages: list[str] | None = Field(
|
338
|
-
default=None, description="Supported output languages (ISO 639-1 codes)"
|
339
|
-
)
|
340
|
-
max_context_length: int | None = Field(
|
341
|
-
default=None, description="Maximum context length in tokens"
|
342
|
-
)
|
315
|
+
input_languages: list[str] | None = Field(default=None, description="Supported input languages (ISO 639-1 codes)")
|
316
|
+
output_languages: list[str] | None = Field(default=None, description="Supported output languages (ISO 639-1 codes)")
|
317
|
+
max_context_length: int | None = Field(default=None, description="Maximum context length in tokens")
|
343
318
|
region_restrictions: list[str] | None = Field(
|
344
319
|
default=None, description="Geographic restrictions (ISO country codes)"
|
345
320
|
)
|
346
321
|
|
347
322
|
# Availability & SLA
|
348
|
-
uptime_sla_percent: float | None = Field(
|
349
|
-
|
350
|
-
)
|
351
|
-
response_time_sla_ms: int | None = Field(
|
352
|
-
default=None, description="Response time SLA in milliseconds"
|
353
|
-
)
|
354
|
-
maintenance_windows: list[str] | None = Field(
|
355
|
-
default=None, description="Scheduled maintenance windows"
|
356
|
-
)
|
323
|
+
uptime_sla_percent: float | None = Field(default=None, description="Uptime SLA percentage (e.g., 99.9)")
|
324
|
+
response_time_sla_ms: int | None = Field(default=None, description="Response time SLA in milliseconds")
|
325
|
+
maintenance_windows: list[str] | None = Field(default=None, description="Scheduled maintenance windows")
|
357
326
|
|
358
327
|
# Concurrency & Connection Limits
|
359
|
-
max_concurrent_requests: int | None = Field(
|
360
|
-
|
361
|
-
)
|
362
|
-
connection_timeout_seconds: int | None = Field(
|
363
|
-
default=None, description="Connection timeout in seconds"
|
364
|
-
)
|
365
|
-
max_connections_per_ip: int | None = Field(
|
366
|
-
default=None, description="Maximum connections per IP address"
|
367
|
-
)
|
328
|
+
max_concurrent_requests: int | None = Field(default=None, description="Maximum concurrent requests allowed")
|
329
|
+
connection_timeout_seconds: int | None = Field(default=None, description="Connection timeout in seconds")
|
330
|
+
max_connections_per_ip: int | None = Field(default=None, description="Maximum connections per IP address")
|
368
331
|
|
369
332
|
|
370
333
|
class AccessInterface(BaseModel):
|
371
334
|
model_config = ConfigDict(extra="allow")
|
372
335
|
|
373
|
-
access_method: AccessMethodEnum = Field(
|
374
|
-
default=AccessMethodEnum.http, description="Type of access method"
|
375
|
-
)
|
336
|
+
access_method: AccessMethodEnum = Field(default=AccessMethodEnum.http, description="Type of access method")
|
376
337
|
|
377
338
|
api_endpoint: str = Field(max_length=500, description="API endpoint URL")
|
378
339
|
|
379
|
-
api_key: str | None = Field(
|
380
|
-
default=None, max_length=2000, description="API key if required"
|
381
|
-
)
|
340
|
+
api_key: str | None = Field(default=None, max_length=2000, description="API key if required")
|
382
341
|
|
383
342
|
name: str | None = Field(default=None, max_length=100, description="Interface name")
|
384
343
|
|
385
|
-
description: str | None = Field(
|
386
|
-
default=None, max_length=500, description="Interface description"
|
387
|
-
)
|
344
|
+
description: str | None = Field(default=None, max_length=500, description="Interface description")
|
388
345
|
|
389
346
|
request_transformer: dict[RequestTransformEnum, dict[str, Any]] | None = Field(
|
390
347
|
default=None, description="Request transformation configuration"
|
@@ -398,13 +355,9 @@ class AccessInterface(BaseModel):
|
|
398
355
|
default=None,
|
399
356
|
description="Rate limit",
|
400
357
|
)
|
401
|
-
constraint: ServiceConstraints | None = Field(
|
402
|
-
default=None, description="Service constraints and conditions"
|
403
|
-
)
|
358
|
+
constraint: ServiceConstraints | None = Field(default=None, description="Service constraints and conditions")
|
404
359
|
is_active: bool = Field(default=True, description="Whether interface is active")
|
405
|
-
is_primary: bool = Field(
|
406
|
-
default=False, description="Whether this is the primary interface"
|
407
|
-
)
|
360
|
+
is_primary: bool = Field(default=False, description="Whether this is the primary interface")
|
408
361
|
sort_order: int = Field(default=0, description="Display order")
|
409
362
|
|
410
363
|
|
@@ -412,13 +365,9 @@ class Pricing(BaseModel):
|
|
412
365
|
model_config = ConfigDict(extra="forbid")
|
413
366
|
|
414
367
|
# Pricing tier name (Basic, Pro, Enterprise, etc.)
|
415
|
-
name: str | None = Field(
|
416
|
-
default=None, description="Pricing tier name (e.g., Basic, Pro, Enterprise)"
|
417
|
-
)
|
368
|
+
name: str | None = Field(default=None, description="Pricing tier name (e.g., Basic, Pro, Enterprise)")
|
418
369
|
|
419
|
-
description: str | None = Field(
|
420
|
-
default=None, description="Pricing model description"
|
421
|
-
)
|
370
|
+
description: str | None = Field(default=None, description="Pricing model description")
|
422
371
|
|
423
372
|
# Currency and description
|
424
373
|
currency: str | None = Field(default=None, description="Currency code (e.g., USD)")
|
@@ -431,6 +380,124 @@ class Pricing(BaseModel):
|
|
431
380
|
)
|
432
381
|
|
433
382
|
# Optional reference to upstream pricing
|
434
|
-
reference: str | None = Field(
|
435
|
-
|
436
|
-
|
383
|
+
reference: str | None = Field(default=None, description="Reference URL to upstream pricing")
|
384
|
+
|
385
|
+
|
386
|
+
def validate_name(name: str, entity_type: str, display_name: str | None = None, *, allow_slash: bool = False) -> str:
|
387
|
+
"""
|
388
|
+
Validate that a name field uses valid identifiers.
|
389
|
+
|
390
|
+
Name format rules:
|
391
|
+
- Only letters (upper/lowercase), numbers, dots, dashes, and underscores allowed
|
392
|
+
- If allow_slash=True, slashes are also allowed for hierarchical names
|
393
|
+
- Must start and end with alphanumeric characters (not special characters)
|
394
|
+
- Cannot have consecutive slashes (when allow_slash=True)
|
395
|
+
- Cannot be empty
|
396
|
+
|
397
|
+
Args:
|
398
|
+
name: The name value to validate
|
399
|
+
entity_type: Type of entity (provider, seller, service, listing) for error messages
|
400
|
+
display_name: Optional display name to suggest a valid name from
|
401
|
+
allow_slash: Whether to allow slashes for hierarchical names (default: False)
|
402
|
+
|
403
|
+
Returns:
|
404
|
+
The validated name (unchanged if valid)
|
405
|
+
|
406
|
+
Raises:
|
407
|
+
ValueError: If the name doesn't match the required pattern
|
408
|
+
|
409
|
+
Examples:
|
410
|
+
Without slashes (providers, sellers):
|
411
|
+
- name='amazon-bedrock' or name='Amazon-Bedrock'
|
412
|
+
- name='fireworks.ai' or name='Fireworks.ai'
|
413
|
+
- name='llama-3.1' or name='Llama-3.1'
|
414
|
+
|
415
|
+
With slashes (services, listings):
|
416
|
+
- name='gpt-4' or name='GPT-4'
|
417
|
+
- name='models/gpt-4' or name='models/GPT-4'
|
418
|
+
- name='black-forest-labs/FLUX.1-dev'
|
419
|
+
- name='api/v1/completion'
|
420
|
+
"""
|
421
|
+
# Build pattern based on allow_slash parameter
|
422
|
+
if allow_slash:
|
423
|
+
# Pattern: starts with alphanumeric, can contain alphanumeric/dot/dash/underscore/slash, ends with alphanumeric
|
424
|
+
name_pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9._/-]*[a-zA-Z0-9])?$"
|
425
|
+
allowed_chars = "letters, numbers, dots, dashes, underscores, and slashes"
|
426
|
+
else:
|
427
|
+
# Pattern: starts with alphanumeric, can contain alphanumeric/dot/dash/underscore, ends with alphanumeric
|
428
|
+
name_pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$"
|
429
|
+
allowed_chars = "letters, numbers, dots, dashes, and underscores"
|
430
|
+
|
431
|
+
# Check for consecutive slashes if slashes are allowed
|
432
|
+
if allow_slash and "//" in name:
|
433
|
+
raise ValueError(f"Invalid {entity_type} name '{name}'. Name cannot contain consecutive slashes.")
|
434
|
+
|
435
|
+
if not re.match(name_pattern, name):
|
436
|
+
# Build helpful error message
|
437
|
+
error_msg = (
|
438
|
+
f"Invalid {entity_type} name '{name}'. "
|
439
|
+
f"Name must contain only {allowed_chars}. "
|
440
|
+
f"It must start and end with an alphanumeric character.\n"
|
441
|
+
)
|
442
|
+
|
443
|
+
# Suggest a valid name based on display_name if available
|
444
|
+
if display_name:
|
445
|
+
suggested_name = suggest_valid_name(display_name, allow_slash=allow_slash)
|
446
|
+
if suggested_name and suggested_name != name:
|
447
|
+
error_msg += f" Suggestion: Set name='{suggested_name}' and display_name='{display_name}'\n"
|
448
|
+
|
449
|
+
# Add appropriate examples based on allow_slash
|
450
|
+
if allow_slash:
|
451
|
+
error_msg += (
|
452
|
+
" Examples:\n"
|
453
|
+
" - name='gpt-4' or name='GPT-4'\n"
|
454
|
+
" - name='models/gpt-4' or name='models/GPT-4'\n"
|
455
|
+
" - name='black-forest-labs/FLUX.1-dev'\n"
|
456
|
+
" - name='api/v1/completion'"
|
457
|
+
)
|
458
|
+
else:
|
459
|
+
error_msg += (
|
460
|
+
" Note: Use 'display_name' field for brand names with spaces and special characters.\n"
|
461
|
+
" Examples:\n"
|
462
|
+
" - name='amazon-bedrock' or name='Amazon-Bedrock'\n"
|
463
|
+
" - name='fireworks.ai' or name='Fireworks.ai'\n"
|
464
|
+
" - name='llama-3.1' or name='Llama-3.1'"
|
465
|
+
)
|
466
|
+
|
467
|
+
raise ValueError(error_msg)
|
468
|
+
|
469
|
+
return name
|
470
|
+
|
471
|
+
|
472
|
+
def suggest_valid_name(display_name: str, *, allow_slash: bool = False) -> str:
|
473
|
+
"""
|
474
|
+
Suggest a valid name based on a display name.
|
475
|
+
|
476
|
+
Replaces invalid characters with hyphens and ensures it follows the naming rules.
|
477
|
+
Preserves the original case.
|
478
|
+
|
479
|
+
Args:
|
480
|
+
display_name: The display name to convert
|
481
|
+
allow_slash: Whether to allow slashes for hierarchical names (default: False)
|
482
|
+
|
483
|
+
Returns:
|
484
|
+
A suggested valid name
|
485
|
+
"""
|
486
|
+
if allow_slash:
|
487
|
+
# Replace characters that aren't alphanumeric, dot, dash, underscore, or slash with hyphens
|
488
|
+
suggested = re.sub(r"[^a-zA-Z0-9._/-]+", "-", display_name)
|
489
|
+
# Remove leading/trailing special characters
|
490
|
+
suggested = suggested.strip("._/-")
|
491
|
+
# Collapse multiple consecutive dashes
|
492
|
+
suggested = re.sub(r"-+", "-", suggested)
|
493
|
+
# Remove consecutive slashes
|
494
|
+
suggested = re.sub(r"/+", "/", suggested)
|
495
|
+
else:
|
496
|
+
# Replace characters that aren't alphanumeric, dot, dash, or underscore with hyphens
|
497
|
+
suggested = re.sub(r"[^a-zA-Z0-9._-]+", "-", display_name)
|
498
|
+
# Remove leading/trailing dots, dashes, or underscores
|
499
|
+
suggested = suggested.strip("._-")
|
500
|
+
# Collapse multiple consecutive dashes
|
501
|
+
suggested = re.sub(r"-+", "-", suggested)
|
502
|
+
|
503
|
+
return suggested
|
@@ -1,13 +1,14 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
5
5
|
|
6
6
|
from unitysvc_services.models.base import (
|
7
7
|
AccessInterface,
|
8
8
|
Document,
|
9
9
|
ListingStatusEnum,
|
10
10
|
Pricing,
|
11
|
+
validate_name,
|
11
12
|
)
|
12
13
|
|
13
14
|
|
@@ -17,9 +18,7 @@ class ListingV1(BaseModel):
|
|
17
18
|
#
|
18
19
|
# fields for business data collection and maintenance
|
19
20
|
#
|
20
|
-
schema_version: str = Field(
|
21
|
-
default="listing_v1", description="Schema identifier", alias="schema"
|
22
|
-
)
|
21
|
+
schema_version: str = Field(default="listing_v1", description="Schema identifier", alias="schema")
|
23
22
|
time_created: datetime
|
24
23
|
|
25
24
|
#
|
@@ -32,8 +31,19 @@ class ListingV1(BaseModel):
|
|
32
31
|
),
|
33
32
|
)
|
34
33
|
|
35
|
-
seller_name: str | None = Field(
|
36
|
-
|
34
|
+
seller_name: str | None = Field(default=None, description="Name of the seller offering this service listing")
|
35
|
+
|
36
|
+
name: str | None = Field(
|
37
|
+
default=None,
|
38
|
+
max_length=255,
|
39
|
+
description="Name identifier for the service listing, default to filename",
|
40
|
+
)
|
41
|
+
|
42
|
+
# Display name for UI (human-readable listing name)
|
43
|
+
display_name: str | None = Field(
|
44
|
+
default=None,
|
45
|
+
max_length=200,
|
46
|
+
description="Human-readable listing name (e.g., 'Premium GPT-4 Access', 'Enterprise AI Services')",
|
37
47
|
)
|
38
48
|
|
39
49
|
# unique name for each provider, usually following upstream naming convention
|
@@ -50,9 +60,7 @@ class ListingV1(BaseModel):
|
|
50
60
|
# - code_examples
|
51
61
|
# multiple access interfaces can be provided, for example, if the service
|
52
62
|
# is available through multiple interfaces or service groups
|
53
|
-
user_access_interfaces: list[AccessInterface] = Field(
|
54
|
-
description="Dictionary of user access interfaces"
|
55
|
-
)
|
63
|
+
user_access_interfaces: list[AccessInterface] = Field(description="Dictionary of user access interfaces")
|
56
64
|
|
57
65
|
#
|
58
66
|
# how upstream charges for their services, which can include
|
@@ -74,3 +82,11 @@ class ListingV1(BaseModel):
|
|
74
82
|
user_parameters_ui_schema: dict[str, Any] | None = Field(
|
75
83
|
default=None, description="Dictionary of user parameters UI schema"
|
76
84
|
)
|
85
|
+
|
86
|
+
@field_validator("name")
|
87
|
+
@classmethod
|
88
|
+
def validate_name_format(cls, v: str | None) -> str | None:
|
89
|
+
"""Validate that listing name uses valid identifiers (allows slashes for hierarchical names)."""
|
90
|
+
if v is None:
|
91
|
+
return v
|
92
|
+
return validate_name(v, "listing", allow_slash=True)
|
@@ -1,9 +1,9 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl
|
4
|
+
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl, field_validator
|
5
5
|
|
6
|
-
from unitysvc_services.models.base import AccessInterface, Document, ProviderStatusEnum
|
6
|
+
from unitysvc_services.models.base import AccessInterface, Document, ProviderStatusEnum, validate_name
|
7
7
|
|
8
8
|
|
9
9
|
class ProviderV1(BaseModel):
|
@@ -12,17 +12,13 @@ class ProviderV1(BaseModel):
|
|
12
12
|
#
|
13
13
|
# fields for business data collection and maintenance
|
14
14
|
#
|
15
|
-
schema_version: str = Field(
|
16
|
-
default="provider_v1", description="Schema identifier", alias="schema"
|
17
|
-
)
|
15
|
+
schema_version: str = Field(default="provider_v1", description="Schema identifier", alias="schema")
|
18
16
|
time_created: datetime
|
19
17
|
# how to automatically populate service data, if available
|
20
18
|
services_populator: dict[str, Any] | None = None
|
21
19
|
# parameters for accessing service provider, which typically
|
22
20
|
# include "api_endpoint" and "api_key"
|
23
|
-
provider_access_info: AccessInterface = Field(
|
24
|
-
description="Dictionary of upstream access interface"
|
25
|
-
)
|
21
|
+
provider_access_info: AccessInterface = Field(description="Dictionary of upstream access interface")
|
26
22
|
#
|
27
23
|
# fields that will be stored in backend database
|
28
24
|
#
|
@@ -30,6 +26,13 @@ class ProviderV1(BaseModel):
|
|
30
26
|
# name of the provider should be the same as directory name
|
31
27
|
name: str
|
32
28
|
|
29
|
+
# Display name for UI (human-readable brand name)
|
30
|
+
display_name: str | None = Field(
|
31
|
+
default=None,
|
32
|
+
max_length=200,
|
33
|
+
description="Human-readable provider name (e.g., 'Amazon Bedrock', 'Fireworks.ai')",
|
34
|
+
)
|
35
|
+
|
33
36
|
# this field is added for convenience. It will be converted to
|
34
37
|
# documents during importing.
|
35
38
|
logo: str | HttpUrl | None = None
|
@@ -61,3 +64,11 @@ class ProviderV1(BaseModel):
|
|
61
64
|
default=ProviderStatusEnum.active,
|
62
65
|
description="Provider status: active, disabled, or incomplete",
|
63
66
|
)
|
67
|
+
|
68
|
+
@field_validator("name")
|
69
|
+
@classmethod
|
70
|
+
def validate_name_format(cls, v: str) -> str:
|
71
|
+
"""Validate that provider name uses URL-safe identifiers."""
|
72
|
+
# Note: display_name is not available in the validator context for suggesting
|
73
|
+
# Display name will be shown in error if user provides it
|
74
|
+
return validate_name(v, "provider", allow_slash=False)
|
@@ -1,8 +1,8 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
|
3
|
-
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl
|
3
|
+
from pydantic import BaseModel, ConfigDict, EmailStr, Field, HttpUrl, field_validator
|
4
4
|
|
5
|
-
from unitysvc_services.models.base import Document, SellerStatusEnum, SellerTypeEnum
|
5
|
+
from unitysvc_services.models.base import Document, SellerStatusEnum, SellerTypeEnum, validate_name
|
6
6
|
|
7
7
|
|
8
8
|
class SellerV1(BaseModel):
|
@@ -17,9 +17,7 @@ class SellerV1(BaseModel):
|
|
17
17
|
#
|
18
18
|
# fields for business data collection and maintenance
|
19
19
|
#
|
20
|
-
schema_version: str = Field(
|
21
|
-
default="seller_v1", description="Schema identifier", alias="schema"
|
22
|
-
)
|
20
|
+
schema_version: str = Field(default="seller_v1", description="Schema identifier", alias="schema")
|
23
21
|
time_created: datetime
|
24
22
|
|
25
23
|
#
|
@@ -49,9 +47,7 @@ class SellerV1(BaseModel):
|
|
49
47
|
# Contact information
|
50
48
|
contact_email: EmailStr = Field(description="Primary contact email for the seller")
|
51
49
|
|
52
|
-
secondary_contact_email: EmailStr | None = Field(
|
53
|
-
default=None, description="Secondary contact email"
|
54
|
-
)
|
50
|
+
secondary_contact_email: EmailStr | None = Field(default=None, description="Secondary contact email")
|
55
51
|
|
56
52
|
# Account manager
|
57
53
|
account_manager: str | None = Field(
|
@@ -112,3 +108,9 @@ class SellerV1(BaseModel):
|
|
112
108
|
default=False,
|
113
109
|
description="Whether the seller has been verified (KYC/business verification)",
|
114
110
|
)
|
111
|
+
|
112
|
+
@field_validator("name")
|
113
|
+
@classmethod
|
114
|
+
def validate_name_format(cls, v: str) -> str:
|
115
|
+
"""Validate that seller name uses URL-safe identifiers."""
|
116
|
+
return validate_name(v, "seller", allow_slash=False)
|
@@ -1,7 +1,7 @@
|
|
1
1
|
from datetime import datetime
|
2
2
|
from typing import Any
|
3
3
|
|
4
|
-
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
|
4
|
+
from pydantic import BaseModel, ConfigDict, Field, HttpUrl, field_validator
|
5
5
|
|
6
6
|
from unitysvc_services.models.base import (
|
7
7
|
AccessInterface,
|
@@ -10,6 +10,7 @@ from unitysvc_services.models.base import (
|
|
10
10
|
ServiceTypeEnum,
|
11
11
|
TagEnum,
|
12
12
|
UpstreamStatusEnum,
|
13
|
+
validate_name,
|
13
14
|
)
|
14
15
|
|
15
16
|
|
@@ -78,3 +79,9 @@ class ServiceV1(BaseModel):
|
|
78
79
|
# a list of pricing models
|
79
80
|
#
|
80
81
|
upstream_price: Pricing | None = Field(description="List of pricing information")
|
82
|
+
|
83
|
+
@field_validator("name")
|
84
|
+
@classmethod
|
85
|
+
def validate_name_format(cls, v: str) -> str:
|
86
|
+
"""Validate that service name uses valid identifiers (allows slashes for hierarchical names)."""
|
87
|
+
return validate_name(v, "service", allow_slash=True)
|
unitysvc_services/populate.py
CHANGED
@@ -9,6 +9,7 @@ from pathlib import Path
|
|
9
9
|
import typer
|
10
10
|
from rich.console import Console
|
11
11
|
|
12
|
+
from .format_data import format_data
|
12
13
|
from .utils import find_files_by_schema
|
13
14
|
|
14
15
|
app = typer.Typer(help="Populate services")
|
@@ -19,7 +20,7 @@ console = Console()
|
|
19
20
|
def populate(
|
20
21
|
data_dir: Path | None = typer.Argument(
|
21
22
|
None,
|
22
|
-
help="Directory containing provider data files (default:
|
23
|
+
help="Directory containing provider data files (default: current directory)",
|
23
24
|
),
|
24
25
|
provider_name: str | None = typer.Option(
|
25
26
|
None,
|
@@ -38,14 +39,13 @@ def populate(
|
|
38
39
|
|
39
40
|
This command scans provider files for 'services_populator' configuration and executes
|
40
41
|
the specified commands with environment variables from 'provider_access_info'.
|
42
|
+
|
43
|
+
After successful execution, automatically runs formatting on all generated files to
|
44
|
+
ensure they conform to the format specification (equivalent to running 'usvc format').
|
41
45
|
"""
|
42
46
|
# Set data directory
|
43
47
|
if data_dir is None:
|
44
|
-
|
45
|
-
if data_dir_str:
|
46
|
-
data_dir = Path(data_dir_str)
|
47
|
-
else:
|
48
|
-
data_dir = Path.cwd() / "data"
|
48
|
+
data_dir = Path.cwd()
|
49
49
|
|
50
50
|
if not data_dir.is_absolute():
|
51
51
|
data_dir = Path.cwd() / data_dir
|
@@ -182,5 +182,19 @@ def populate(
|
|
182
182
|
console.print(f" [yellow]⏭️ Skipped: {total_skipped}[/yellow]")
|
183
183
|
console.print(f" [red]✗ Failed: {total_failed}[/red]")
|
184
184
|
|
185
|
+
# Format generated files if any populate scripts executed successfully
|
186
|
+
if total_executed > 0 and not dry_run:
|
187
|
+
console.print("\n" + "=" * 50)
|
188
|
+
console.print("[bold cyan]Formatting generated files...[/bold cyan]")
|
189
|
+
console.print("[dim]Running automatic formatting to ensure data conforms to format specification[/dim]\n")
|
190
|
+
|
191
|
+
try:
|
192
|
+
# Run format command on the data directory
|
193
|
+
format_data(data_dir)
|
194
|
+
console.print("\n[green]✓ Formatting completed successfully[/green]")
|
195
|
+
except Exception as e:
|
196
|
+
console.print(f"\n[yellow]⚠ Warning: Formatting failed: {e}[/yellow]")
|
197
|
+
console.print("[dim]You may want to run 'usvc format' manually to fix formatting issues[/dim]")
|
198
|
+
|
185
199
|
if total_failed > 0:
|
186
200
|
raise typer.Exit(code=1)
|