unitysvc-services 0.1.1__py3-none-any.whl → 0.1.4__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",
@@ -271,12 +268,8 @@ class RateLimit(BaseModel):
271
268
  window: TimeWindowEnum = Field(description="Time window for the limit")
272
269
 
273
270
  # 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
- )
271
+ description: str | None = Field(default=None, max_length=255, description="Human-readable description")
272
+ burst_limit: int | None = Field(default=None, description="Short-term burst allowance")
280
273
 
281
274
  # Status
282
275
  is_active: bool = Field(default=True, description="Whether rate limit is active")
@@ -286,105 +279,57 @@ class ServiceConstraints(BaseModel):
286
279
  model_config = ConfigDict(extra="forbid")
287
280
 
288
281
  # 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
- )
282
+ monthly_quota: int | None = Field(default=None, description="Monthly usage quota (requests, tokens, etc.)")
283
+ daily_quota: int | None = Field(default=None, description="Daily usage quota (requests, tokens, etc.)")
284
+ quota_unit: RateLimitUnitEnum | None = Field(default=None, description="Unit for quota limits")
285
+ quota_reset_cycle: QuotaResetCycleEnum | None = Field(default=None, description="How often quotas reset")
286
+ overage_policy: OveragePolicyEnum | None = Field(default=None, description="What happens when quota is exceeded")
304
287
 
305
288
  # 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
- )
289
+ auth_methods: list[AuthMethodEnum] | None = Field(default=None, description="Supported authentication methods")
290
+ ip_whitelist_required: bool | None = Field(default=None, description="Whether IP whitelisting is required")
291
+ tls_version_min: str | None = Field(default=None, description="Minimum TLS version required")
315
292
 
316
293
  # 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
- )
294
+ max_request_size_bytes: int | None = Field(default=None, description="Maximum request payload size in bytes")
295
+ max_response_size_bytes: int | None = Field(default=None, description="Maximum response payload size in bytes")
296
+ timeout_seconds: int | None = Field(default=None, description="Request timeout in seconds")
297
+ max_batch_size: int | None = Field(default=None, description="Maximum number of items in batch requests")
329
298
 
330
299
  # Content & Model Restrictions
331
300
  content_filters: list[ContentFilterEnum] | None = Field(
332
301
  default=None, description="Active content filtering policies"
333
302
  )
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
- )
303
+ input_languages: list[str] | None = Field(default=None, description="Supported input languages (ISO 639-1 codes)")
304
+ output_languages: list[str] | None = Field(default=None, description="Supported output languages (ISO 639-1 codes)")
305
+ max_context_length: int | None = Field(default=None, description="Maximum context length in tokens")
343
306
  region_restrictions: list[str] | None = Field(
344
307
  default=None, description="Geographic restrictions (ISO country codes)"
345
308
  )
346
309
 
347
310
  # 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
- )
311
+ uptime_sla_percent: float | None = Field(default=None, description="Uptime SLA percentage (e.g., 99.9)")
312
+ response_time_sla_ms: int | None = Field(default=None, description="Response time SLA in milliseconds")
313
+ maintenance_windows: list[str] | None = Field(default=None, description="Scheduled maintenance windows")
357
314
 
358
315
  # 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
- )
316
+ max_concurrent_requests: int | None = Field(default=None, description="Maximum concurrent requests allowed")
317
+ connection_timeout_seconds: int | None = Field(default=None, description="Connection timeout in seconds")
318
+ max_connections_per_ip: int | None = Field(default=None, description="Maximum connections per IP address")
368
319
 
369
320
 
370
321
  class AccessInterface(BaseModel):
371
322
  model_config = ConfigDict(extra="allow")
372
323
 
373
- access_method: AccessMethodEnum = Field(
374
- default=AccessMethodEnum.http, description="Type of access method"
375
- )
324
+ access_method: AccessMethodEnum = Field(default=AccessMethodEnum.http, description="Type of access method")
376
325
 
377
326
  api_endpoint: str = Field(max_length=500, description="API endpoint URL")
378
327
 
379
- api_key: str | None = Field(
380
- default=None, max_length=2000, description="API key if required"
381
- )
328
+ api_key: str | None = Field(default=None, max_length=2000, description="API key if required")
382
329
 
383
330
  name: str | None = Field(default=None, max_length=100, description="Interface name")
384
331
 
385
- description: str | None = Field(
386
- default=None, max_length=500, description="Interface description"
387
- )
332
+ description: str | None = Field(default=None, max_length=500, description="Interface description")
388
333
 
389
334
  request_transformer: dict[RequestTransformEnum, dict[str, Any]] | None = Field(
390
335
  default=None, description="Request transformation configuration"
@@ -398,13 +343,9 @@ class AccessInterface(BaseModel):
398
343
  default=None,
399
344
  description="Rate limit",
400
345
  )
401
- constraint: ServiceConstraints | None = Field(
402
- default=None, description="Service constraints and conditions"
403
- )
346
+ constraint: ServiceConstraints | None = Field(default=None, description="Service constraints and conditions")
404
347
  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
- )
348
+ is_primary: bool = Field(default=False, description="Whether this is the primary interface")
408
349
  sort_order: int = Field(default=0, description="Display order")
409
350
 
410
351
 
@@ -412,13 +353,9 @@ class Pricing(BaseModel):
412
353
  model_config = ConfigDict(extra="forbid")
413
354
 
414
355
  # 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
- )
356
+ name: str | None = Field(default=None, description="Pricing tier name (e.g., Basic, Pro, Enterprise)")
418
357
 
419
- description: str | None = Field(
420
- default=None, description="Pricing model description"
421
- )
358
+ description: str | None = Field(default=None, description="Pricing model description")
422
359
 
423
360
  # Currency and description
424
361
  currency: str | None = Field(default=None, description="Currency code (e.g., USD)")
@@ -431,6 +368,124 @@ class Pricing(BaseModel):
431
368
  )
432
369
 
433
370
  # Optional reference to upstream pricing
434
- reference: str | None = Field(
435
- default=None, description="Reference URL to upstream pricing"
436
- )
371
+ reference: str | None = Field(default=None, description="Reference URL to upstream pricing")
372
+
373
+
374
+ def validate_name(name: str, entity_type: str, display_name: str | None = None, *, allow_slash: bool = False) -> str:
375
+ """
376
+ Validate that a name field uses valid identifiers.
377
+
378
+ Name format rules:
379
+ - Only letters (upper/lowercase), numbers, dots, dashes, and underscores allowed
380
+ - If allow_slash=True, slashes are also allowed for hierarchical names
381
+ - Must start and end with alphanumeric characters (not special characters)
382
+ - Cannot have consecutive slashes (when allow_slash=True)
383
+ - Cannot be empty
384
+
385
+ Args:
386
+ name: The name value to validate
387
+ entity_type: Type of entity (provider, seller, service, listing) for error messages
388
+ display_name: Optional display name to suggest a valid name from
389
+ allow_slash: Whether to allow slashes for hierarchical names (default: False)
390
+
391
+ Returns:
392
+ The validated name (unchanged if valid)
393
+
394
+ Raises:
395
+ ValueError: If the name doesn't match the required pattern
396
+
397
+ Examples:
398
+ Without slashes (providers, sellers):
399
+ - name='amazon-bedrock' or name='Amazon-Bedrock'
400
+ - name='fireworks.ai' or name='Fireworks.ai'
401
+ - name='llama-3.1' or name='Llama-3.1'
402
+
403
+ With slashes (services, listings):
404
+ - name='gpt-4' or name='GPT-4'
405
+ - name='models/gpt-4' or name='models/GPT-4'
406
+ - name='black-forest-labs/FLUX.1-dev'
407
+ - name='api/v1/completion'
408
+ """
409
+ # Build pattern based on allow_slash parameter
410
+ if allow_slash:
411
+ # Pattern: starts with alphanumeric, can contain alphanumeric/dot/dash/underscore/slash, ends with alphanumeric
412
+ name_pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9._/-]*[a-zA-Z0-9])?$"
413
+ allowed_chars = "letters, numbers, dots, dashes, underscores, and slashes"
414
+ else:
415
+ # Pattern: starts with alphanumeric, can contain alphanumeric/dot/dash/underscore, ends with alphanumeric
416
+ name_pattern = r"^[a-zA-Z0-9]([a-zA-Z0-9._-]*[a-zA-Z0-9])?$"
417
+ allowed_chars = "letters, numbers, dots, dashes, and underscores"
418
+
419
+ # Check for consecutive slashes if slashes are allowed
420
+ if allow_slash and "//" in name:
421
+ raise ValueError(f"Invalid {entity_type} name '{name}'. Name cannot contain consecutive slashes.")
422
+
423
+ if not re.match(name_pattern, name):
424
+ # Build helpful error message
425
+ error_msg = (
426
+ f"Invalid {entity_type} name '{name}'. "
427
+ f"Name must contain only {allowed_chars}. "
428
+ f"It must start and end with an alphanumeric character.\n"
429
+ )
430
+
431
+ # Suggest a valid name based on display_name if available
432
+ if display_name:
433
+ suggested_name = suggest_valid_name(display_name, allow_slash=allow_slash)
434
+ if suggested_name and suggested_name != name:
435
+ error_msg += f" Suggestion: Set name='{suggested_name}' and display_name='{display_name}'\n"
436
+
437
+ # Add appropriate examples based on allow_slash
438
+ if allow_slash:
439
+ error_msg += (
440
+ " Examples:\n"
441
+ " - name='gpt-4' or name='GPT-4'\n"
442
+ " - name='models/gpt-4' or name='models/GPT-4'\n"
443
+ " - name='black-forest-labs/FLUX.1-dev'\n"
444
+ " - name='api/v1/completion'"
445
+ )
446
+ else:
447
+ error_msg += (
448
+ " Note: Use 'display_name' field for brand names with spaces and special characters.\n"
449
+ " Examples:\n"
450
+ " - name='amazon-bedrock' or name='Amazon-Bedrock'\n"
451
+ " - name='fireworks.ai' or name='Fireworks.ai'\n"
452
+ " - name='llama-3.1' or name='Llama-3.1'"
453
+ )
454
+
455
+ raise ValueError(error_msg)
456
+
457
+ return name
458
+
459
+
460
+ def suggest_valid_name(display_name: str, *, allow_slash: bool = False) -> str:
461
+ """
462
+ Suggest a valid name based on a display name.
463
+
464
+ Replaces invalid characters with hyphens and ensures it follows the naming rules.
465
+ Preserves the original case.
466
+
467
+ Args:
468
+ display_name: The display name to convert
469
+ allow_slash: Whether to allow slashes for hierarchical names (default: False)
470
+
471
+ Returns:
472
+ A suggested valid name
473
+ """
474
+ if allow_slash:
475
+ # Replace characters that aren't alphanumeric, dot, dash, underscore, or slash with hyphens
476
+ suggested = re.sub(r"[^a-zA-Z0-9._/-]+", "-", display_name)
477
+ # Remove leading/trailing special characters
478
+ suggested = suggested.strip("._/-")
479
+ # Collapse multiple consecutive dashes
480
+ suggested = re.sub(r"-+", "-", suggested)
481
+ # Remove consecutive slashes
482
+ suggested = re.sub(r"/+", "/", suggested)
483
+ else:
484
+ # Replace characters that aren't alphanumeric, dot, dash, or underscore with hyphens
485
+ suggested = re.sub(r"[^a-zA-Z0-9._-]+", "-", display_name)
486
+ # Remove leading/trailing dots, dashes, or underscores
487
+ suggested = suggested.strip("._-")
488
+ # Collapse multiple consecutive dashes
489
+ suggested = re.sub(r"-+", "-", suggested)
490
+
491
+ 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)
@@ -19,7 +19,7 @@ console = Console()
19
19
  def populate(
20
20
  data_dir: Path | None = typer.Argument(
21
21
  None,
22
- help="Directory containing provider data files (default: ./data or UNITYSVC_DATA_DIR env var)",
22
+ help="Directory containing provider data files (default: current directory)",
23
23
  ),
24
24
  provider_name: str | None = typer.Option(
25
25
  None,
@@ -41,11 +41,7 @@ def populate(
41
41
  """
42
42
  # Set data directory
43
43
  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"
44
+ data_dir = Path.cwd()
49
45
 
50
46
  if not data_dir.is_absolute():
51
47
  data_dir = Path.cwd() / data_dir