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.
@@ -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
- default=None, max_length=50, description="Document version"
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
- default=None, max_length=255, description="Human-readable description"
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
- default=None, description="Monthly usage quota (requests, tokens, etc.)"
291
- )
292
- daily_quota: int | None = Field(
293
- default=None, description="Daily usage quota (requests, tokens, etc.)"
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
- default=None, description="Supported authentication methods"
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
- default=None, description="Maximum request payload size in bytes"
319
- )
320
- max_response_size_bytes: int | None = Field(
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
- default=None, description="Supported input languages (ISO 639-1 codes)"
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
- default=None, description="Uptime SLA percentage (e.g., 99.9)"
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
- default=None, description="Maximum concurrent requests allowed"
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
- default=None, description="Reference URL to upstream pricing"
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
- default=None, description="Name of the seller offering this service listing"
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)
@@ -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: ./data or UNITYSVC_DATA_DIR env var)",
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
- data_dir_str = os.getenv("UNITYSVC_DATA_DIR")
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)