unitysvc-services 0.1.0__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,6 +1,5 @@
1
1
  """Update command group - update local data files."""
2
2
 
3
- import os
4
3
  from pathlib import Path
5
4
  from typing import Any
6
5
 
@@ -46,7 +45,7 @@ def update_offering(
46
45
  None,
47
46
  "--data-dir",
48
47
  "-d",
49
- help="Directory containing data files (default: ./data or UNITYSVC_DATA_DIR env var)",
48
+ help="Directory containing data files (default: current directory)",
50
49
  ),
51
50
  ):
52
51
  """
@@ -83,11 +82,7 @@ def update_offering(
83
82
 
84
83
  # Set data directory
85
84
  if data_dir is None:
86
- data_dir_str = os.getenv("UNITYSVC_DATA_DIR")
87
- if data_dir_str:
88
- data_dir = Path(data_dir_str)
89
- else:
90
- data_dir = Path.cwd() / "data"
85
+ data_dir = Path.cwd()
91
86
 
92
87
  if not data_dir.is_absolute():
93
88
  data_dir = Path.cwd() / data_dir
@@ -181,7 +176,7 @@ def update_listing(
181
176
  None,
182
177
  "--data-dir",
183
178
  "-d",
184
- help="Directory containing data files (default: ./data or UNITYSVC_DATA_DIR env var)",
179
+ help="Directory containing data files (default: current directory)",
185
180
  ),
186
181
  ):
187
182
  """
@@ -227,11 +222,7 @@ def update_listing(
227
222
 
228
223
  # Set data directory
229
224
  if data_dir is None:
230
- data_dir_str = os.getenv("UNITYSVC_DATA_DIR")
231
- if data_dir_str:
232
- data_dir = Path(data_dir_str)
233
- else:
234
- data_dir = Path.cwd() / "data"
225
+ data_dir = Path.cwd()
235
226
 
236
227
  if not data_dir.is_absolute():
237
228
  data_dir = Path.cwd() / data_dir
@@ -251,8 +242,13 @@ def update_listing(
251
242
  if seller_name:
252
243
  field_filter["seller_name"] = seller_name
253
244
 
245
+ # Convert field_filter dict to tuple for caching
246
+ field_filter_tuple = tuple(sorted(field_filter.items())) if field_filter else None
247
+
254
248
  # Find listing files matching criteria
255
- listing_files = find_files_by_schema(data_dir, "listing_v1", path_filter=service_name, field_filter=field_filter)
249
+ listing_files = find_files_by_schema(
250
+ data_dir, "listing_v1", path_filter=service_name, field_filter=field_filter_tuple
251
+ )
256
252
 
257
253
  if not listing_files:
258
254
  console.print(
@@ -2,6 +2,7 @@
2
2
 
3
3
  import json
4
4
  import tomllib
5
+ from functools import lru_cache
5
6
  from pathlib import Path
6
7
  from typing import Any
7
8
 
@@ -54,23 +55,25 @@ def write_data_file(file_path: Path, data: dict[str, Any], format: str) -> None:
54
55
  raise ValueError(f"Unsupported format: {format}")
55
56
 
56
57
 
57
- def find_data_files(data_dir: Path, extensions: list[str] | None = None) -> list[Path]:
58
+ @lru_cache(maxsize=128)
59
+ def find_data_files(data_dir: Path, extensions: tuple[str, ...] | None = None) -> list[Path]:
58
60
  """
59
61
  Find all data files in a directory with specified extensions.
60
62
 
61
63
  Args:
62
64
  data_dir: Directory to search
63
- extensions: List of extensions to search for (default: ["json", "toml"])
65
+ extensions: Tuple of extensions to search for (default: ("json", "toml"))
64
66
 
65
67
  Returns:
66
68
  List of Path objects for matching files
67
69
  """
68
70
  if extensions is None:
69
- extensions = ["json", "toml"]
71
+ extensions = ("json", "toml")
70
72
 
71
73
  data_files: list[Path] = []
72
74
  for ext in extensions:
73
75
  data_files.extend(data_dir.rglob(f"*.{ext}"))
76
+
74
77
  return data_files
75
78
 
76
79
 
@@ -103,11 +106,12 @@ def find_file_by_schema_and_name(
103
106
  return None
104
107
 
105
108
 
109
+ @lru_cache(maxsize=256)
106
110
  def find_files_by_schema(
107
111
  data_dir: Path,
108
112
  schema: str,
109
113
  path_filter: str | None = None,
110
- field_filter: dict[str, Any] | None = None,
114
+ field_filter: tuple[tuple[str, Any], ...] | None = None,
111
115
  ) -> list[tuple[Path, str, dict[str, Any]]]:
112
116
  """
113
117
  Find all data files matching a schema with optional filters.
@@ -116,7 +120,7 @@ def find_files_by_schema(
116
120
  data_dir: Directory to search
117
121
  schema: Schema identifier (e.g., "service_v1", "listing_v1")
118
122
  path_filter: Optional string that must be in the file path
119
- field_filter: Optional dict of field:value pairs to filter by
123
+ field_filter: Optional tuple of (key, value) pairs to filter by
120
124
 
121
125
  Returns:
122
126
  List of tuples (file_path, format, data) for matching files
@@ -124,6 +128,9 @@ def find_files_by_schema(
124
128
  data_files = find_data_files(data_dir)
125
129
  matching_files: list[tuple[Path, str, dict[str, Any]]] = []
126
130
 
131
+ # Convert field_filter tuple back to dict for filtering
132
+ field_filter_dict = dict(field_filter) if field_filter else None
133
+
127
134
  for data_file in data_files:
128
135
  try:
129
136
  # Apply path filter
@@ -137,8 +144,8 @@ def find_files_by_schema(
137
144
  continue
138
145
 
139
146
  # Apply field filters
140
- if field_filter:
141
- if not all(data.get(k) == v for k, v in field_filter.items()):
147
+ if field_filter_dict:
148
+ if not all(data.get(k) == v for k, v in field_filter_dict.items()):
142
149
  continue
143
150
 
144
151
  matching_files.append((data_file, file_format, data))
@@ -238,3 +245,94 @@ def resolve_service_name_for_listing(listing_file: Path, listing_data: dict[str,
238
245
 
239
246
  # Otherwise, return None (either no service files or multiple service files)
240
247
  return None
248
+
249
+
250
+ def convert_convenience_fields_to_documents(
251
+ data: dict[str, Any],
252
+ base_path: Path,
253
+ *,
254
+ logo_field: str = "logo",
255
+ terms_field: str | None = "terms_of_service",
256
+ ) -> dict[str, Any]:
257
+ """
258
+ Convert convenience fields (logo, terms_of_service) to Document objects.
259
+
260
+ This utility function converts file paths or URLs in convenience fields
261
+ to proper Document structures that can be stored in the backend.
262
+
263
+ Args:
264
+ data: Data dictionary containing potential convenience fields
265
+ base_path: Base path for resolving relative file paths
266
+ logo_field: Name of the logo field (default: "logo")
267
+ terms_field: Name of the terms of service field (default: "terms_of_service", None to skip)
268
+
269
+ Returns:
270
+ Updated data dictionary with convenience fields converted to documents list
271
+
272
+ Example:
273
+ >>> data = {"logo": "assets/logo.png", "documents": []}
274
+ >>> result = convert_convenience_fields_to_documents(data, Path("/data/provider"))
275
+ >>> # Result will have logo removed and added to documents list
276
+ """
277
+ # Initialize documents list if not present
278
+ if "documents" not in data or data["documents"] is None:
279
+ data["documents"] = []
280
+
281
+ # Helper to determine MIME type from file path/URL
282
+ def get_mime_type(path_or_url: str) -> str:
283
+ path_lower = path_or_url.lower()
284
+ if path_lower.endswith((".png", ".jpg", ".jpeg")):
285
+ return "png" if ".png" in path_lower else "jpeg"
286
+ elif path_lower.endswith(".svg"):
287
+ return "svg"
288
+ elif path_lower.endswith(".pdf"):
289
+ return "pdf"
290
+ elif path_lower.endswith(".md"):
291
+ return "markdown"
292
+ else:
293
+ # Default to URL if it looks like a URL, otherwise markdown
294
+ return "url" if path_or_url.startswith("http") else "markdown"
295
+
296
+ # Convert logo field
297
+ if logo_field in data and data[logo_field]:
298
+ logo_value = data[logo_field]
299
+ logo_doc: dict[str, Any] = {
300
+ "title": "Company Logo",
301
+ "category": "logo",
302
+ "mime_type": get_mime_type(str(logo_value)),
303
+ "is_public": True,
304
+ }
305
+
306
+ # Check if it's a URL or file path
307
+ if str(logo_value).startswith("http"):
308
+ logo_doc["external_url"] = str(logo_value)
309
+ else:
310
+ # It's a file path - will be resolved by resolve_file_references
311
+ logo_doc["file_path"] = str(logo_value)
312
+
313
+ data["documents"].append(logo_doc)
314
+ # Remove the convenience field
315
+ del data[logo_field]
316
+
317
+ # Convert terms_of_service field if specified
318
+ if terms_field and terms_field in data and data[terms_field]:
319
+ terms_value = data[terms_field]
320
+ terms_doc: dict[str, Any] = {
321
+ "title": "Terms of Service",
322
+ "category": "terms_of_service",
323
+ "mime_type": get_mime_type(str(terms_value)),
324
+ "is_public": True,
325
+ }
326
+
327
+ # Check if it's a URL or file path
328
+ if str(terms_value).startswith("http"):
329
+ terms_doc["external_url"] = str(terms_value)
330
+ else:
331
+ # It's a file path - will be resolved by resolve_file_references
332
+ terms_doc["file_path"] = str(terms_value)
333
+
334
+ data["documents"].append(terms_doc)
335
+ # Remove the convenience field
336
+ del data[terms_field]
337
+
338
+ return data
@@ -1,7 +1,6 @@
1
1
  """Data validation module for unitysvc_services."""
2
2
 
3
3
  import json
4
- import os
5
4
  import re
6
5
  import tomllib as toml
7
6
  from pathlib import Path
@@ -13,6 +12,8 @@ from jinja2 import Environment, TemplateSyntaxError
13
12
  from jsonschema.validators import Draft7Validator
14
13
  from rich.console import Console
15
14
 
15
+ import unitysvc_services
16
+
16
17
 
17
18
  class DataValidationError(Exception):
18
19
  """Exception raised when data validation fails."""
@@ -139,6 +140,14 @@ class DataValidator:
139
140
  f"File path '{value}' in field '{new_path}' "
140
141
  f"must be a relative path, not an absolute path"
141
142
  )
143
+ # Check that the file exists
144
+ else:
145
+ referenced_file = file_path.parent / value
146
+ if not referenced_file.exists():
147
+ errors.append(
148
+ f"File reference '{value}' in field '{new_path}' "
149
+ f"does not exist at {referenced_file}"
150
+ )
142
151
 
143
152
  # Recurse into nested objects
144
153
  if isinstance(value, dict | list):
@@ -196,6 +205,54 @@ class DataValidator:
196
205
  normalized = normalized.strip("-")
197
206
  return normalized
198
207
 
208
+ def validate_with_pydantic_model(self, data: dict[str, Any], schema_name: str) -> list[str]:
209
+ """
210
+ Validate data using Pydantic models for additional validation rules.
211
+
212
+ This complements JSON schema validation with Pydantic field validators
213
+ like name format validation.
214
+
215
+ Args:
216
+ data: The data to validate
217
+ schema_name: The schema name (e.g., 'provider_v1', 'seller_v1')
218
+
219
+ Returns:
220
+ List of validation error messages
221
+ """
222
+ from pydantic import BaseModel
223
+
224
+ from unitysvc_services.models import ListingV1, ProviderV1, SellerV1, ServiceV1
225
+
226
+ errors: list[str] = []
227
+
228
+ # Map schema names to Pydantic model classes
229
+ model_map: dict[str, type[BaseModel]] = {
230
+ "provider_v1": ProviderV1,
231
+ "seller_v1": SellerV1,
232
+ "service_v1": ServiceV1,
233
+ "listing_v1": ListingV1,
234
+ }
235
+
236
+ if schema_name not in model_map:
237
+ return errors # No Pydantic model for this schema
238
+
239
+ model_class = model_map[schema_name]
240
+
241
+ try:
242
+ # Validate using the Pydantic model
243
+ model_class.model_validate(data)
244
+
245
+ except Exception as e:
246
+ # Extract meaningful error message from Pydantic ValidationError
247
+ error_msg = str(e)
248
+ # Pydantic errors can be verbose, try to extract just the relevant part
249
+ if "validation error" in error_msg.lower():
250
+ errors.append(f"Pydantic validation error: {error_msg}")
251
+ else:
252
+ errors.append(error_msg)
253
+
254
+ return errors
255
+
199
256
  def load_data_file(self, file_path: Path) -> tuple[dict[str, Any] | None, list[str]]:
200
257
  """Load data from JSON or TOML file."""
201
258
  errors: list[str] = []
@@ -250,6 +307,10 @@ class DataValidator:
250
307
  except Exception as e:
251
308
  errors.append(f"Validation error: {e}")
252
309
 
310
+ # Also validate using Pydantic models for additional validation rules
311
+ pydantic_errors = self.validate_with_pydantic_model(data, schema_name)
312
+ errors.extend(pydantic_errors)
313
+
253
314
  # Find Union[str, HttpUrl] fields and validate file references
254
315
  union_fields = self.find_union_fields(schema)
255
316
  file_ref_errors = self.validate_file_references(data, file_path, union_fields)
@@ -299,6 +360,10 @@ class DataValidator:
299
360
 
300
361
  # Find all data files with seller_v1 schema
301
362
  for file_path in self.data_dir.rglob("*"):
363
+ # Skip hidden directories (those starting with .)
364
+ if any(part.startswith(".") for part in file_path.parts):
365
+ continue
366
+
302
367
  if file_path.is_file() and file_path.suffix in [".json", ".toml"]:
303
368
  try:
304
369
  data, load_errors = self.load_data_file(file_path)
@@ -320,6 +385,92 @@ class DataValidator:
320
385
 
321
386
  return len(errors) == 0, errors
322
387
 
388
+ def validate_provider_status(self) -> tuple[bool, list[str]]:
389
+ """
390
+ Validate provider status and warn about services under disabled/incomplete providers.
391
+
392
+ Returns tuple of (is_valid, warnings) where warnings indicate services
393
+ that will be affected by provider status.
394
+ """
395
+ from unitysvc_services.models.base import ProviderStatusEnum
396
+ from unitysvc_services.models.provider_v1 import ProviderV1
397
+
398
+ warnings: list[str] = []
399
+
400
+ # Find all provider files (skip hidden directories)
401
+ provider_files = [
402
+ f for f in self.data_dir.glob("*/provider.*") if not any(part.startswith(".") for part in f.parts)
403
+ ]
404
+
405
+ for provider_file in provider_files:
406
+ try:
407
+ # Load provider data using existing helper method
408
+ data, load_errors = self.load_data_file(provider_file)
409
+ if load_errors or data is None:
410
+ warnings.append(f"Failed to load provider file {provider_file}: {load_errors}")
411
+ continue
412
+
413
+ # Parse as ProviderV1
414
+ provider = ProviderV1.model_validate(data)
415
+ provider_dir = provider_file.parent
416
+ provider_name = provider.name
417
+
418
+ # Check if provider is not active
419
+ if provider.status != ProviderStatusEnum.active:
420
+ # Find all services under this provider
421
+ services_dir = provider_dir / "services"
422
+ if services_dir.exists():
423
+ service_count = len(list(services_dir.iterdir()))
424
+ if service_count > 0:
425
+ warnings.append(
426
+ f"Provider '{provider_name}' has status '{provider.status}' but has {service_count} "
427
+ f"service(s). All services under this provider will be affected."
428
+ )
429
+
430
+ except Exception as e:
431
+ warnings.append(f"Error checking provider status in {provider_file}: {e}")
432
+
433
+ # Return True (valid) but with warnings
434
+ return True, warnings
435
+
436
+ def validate_seller_status(self) -> tuple[bool, list[str]]:
437
+ """
438
+ Validate seller status and warn if seller is disabled/incomplete.
439
+
440
+ Returns tuple of (is_valid, warnings) where warnings indicate seller issues.
441
+ """
442
+ from unitysvc_services.models.base import SellerStatusEnum
443
+ from unitysvc_services.models.seller_v1 import SellerV1
444
+
445
+ warnings: list[str] = []
446
+
447
+ # Find all seller files (skip hidden files)
448
+ seller_files = [f for f in self.data_dir.glob("seller.*") if not f.name.startswith(".")]
449
+
450
+ for seller_file in seller_files:
451
+ try:
452
+ # Load seller data using existing helper method
453
+ data, load_errors = self.load_data_file(seller_file)
454
+ if load_errors or data is None:
455
+ warnings.append(f"Failed to load seller file {seller_file}: {load_errors}")
456
+ continue
457
+
458
+ # Parse as SellerV1
459
+ seller = SellerV1.model_validate(data)
460
+ seller_name = seller.name
461
+
462
+ # Check if seller is not active
463
+ if seller.status != SellerStatusEnum.active:
464
+ warnings.append(
465
+ f"Seller '{seller_name}' has status '{seller.status}'. Seller will not be published to backend."
466
+ )
467
+
468
+ except Exception as e:
469
+ warnings.append(f"Error checking seller status in {seller_file}: {e}")
470
+
471
+ # Return True (valid) but with warnings
472
+ return True, warnings
473
+
323
474
  def validate_all(self) -> dict[str, tuple[bool, list[str]]]:
324
475
  """Validate all files in the data directory."""
325
476
  results: dict[str, tuple[bool, list[str]]] = {}
@@ -332,8 +483,25 @@ class DataValidator:
332
483
  if not seller_valid:
333
484
  results["_seller_uniqueness"] = (False, seller_errors)
334
485
 
335
- # Find all data and MD files recursively
486
+ # Validate seller status
487
+ seller_status_valid, seller_warnings = self.validate_seller_status()
488
+ if seller_warnings:
489
+ results["_seller_status"] = (True, seller_warnings) # Warnings, not errors
490
+
491
+ # Validate provider status and check for affected services
492
+ provider_status_valid, provider_warnings = self.validate_provider_status()
493
+ if provider_warnings:
494
+ results["_provider_status"] = (
495
+ True,
496
+ provider_warnings,
497
+ ) # Warnings, not errors
498
+
499
+ # Find all data and MD files recursively, skipping hidden directories
336
500
  for file_path in self.data_dir.rglob("*"):
501
+ # Skip hidden directories (those starting with .)
502
+ if any(part.startswith(".") for part in file_path.parts):
503
+ continue
504
+
337
505
  if file_path.is_file() and file_path.suffix in [".json", ".toml", ".md"]:
338
506
  relative_path = file_path.relative_to(self.data_dir)
339
507
 
@@ -444,6 +612,10 @@ class DataValidator:
444
612
 
445
613
  for pattern in ["*.json", "*.toml"]:
446
614
  for file_path in data_dir.rglob(pattern):
615
+ # Skip hidden directories (those starting with .)
616
+ if any(part.startswith(".") for part in file_path.parts):
617
+ continue
618
+
447
619
  try:
448
620
  data, load_errors = self.load_data_file(file_path)
449
621
  if load_errors or data is None:
@@ -474,7 +646,7 @@ console = Console()
474
646
  def validate(
475
647
  data_dir: Path | None = typer.Argument(
476
648
  None,
477
- help="Directory containing data files to validate (default: ./data or UNITYSVC_DATA_DIR env var)",
649
+ help="Directory containing data files to validate (default: current directory)",
478
650
  ),
479
651
  ):
480
652
  """
@@ -487,11 +659,7 @@ def validate(
487
659
  """
488
660
  # Determine data directory
489
661
  if data_dir is None:
490
- data_dir_str = os.environ.get("UNITYSVC_DATA_DIR")
491
- if data_dir_str:
492
- data_dir = Path(data_dir_str)
493
- else:
494
- data_dir = Path.cwd() / "data"
662
+ data_dir = Path.cwd()
495
663
 
496
664
  if not data_dir.exists():
497
665
  console.print(f"[red]✗[/red] Data directory not found: {data_dir}")
@@ -500,9 +668,25 @@ def validate(
500
668
  console.print(f"[cyan]Validating data files in:[/cyan] {data_dir}")
501
669
  console.print()
502
670
 
671
+ # Get schema directory from installed package
672
+ schema_dir = Path(unitysvc_services.__file__).parent / "schema"
673
+
503
674
  # Create validator and run validation
504
- validator = DataValidator(data_dir, data_dir.parent / "schema")
505
- validation_errors = validator.validate_all_service_directories(data_dir)
675
+ validator = DataValidator(data_dir, schema_dir)
676
+
677
+ # Run comprehensive validation (schema, file references, etc.)
678
+ all_results = validator.validate_all()
679
+ validation_errors = []
680
+
681
+ # Collect all errors from validate_all()
682
+ for file_path, (is_valid, errors) in all_results.items():
683
+ if not is_valid and errors:
684
+ for error in errors:
685
+ validation_errors.append(f"{file_path}: {error}")
686
+
687
+ # Also run service directory validation (service/listing relationships)
688
+ directory_errors = validator.validate_all_service_directories(data_dir)
689
+ validation_errors.extend(directory_errors)
506
690
 
507
691
  if validation_errors:
508
692
  console.print(f"[red]✗ Validation failed with {len(validation_errors)} error(s):[/red]")
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: unitysvc-services
3
- Version: 0.1.0
3
+ Version: 0.1.4
4
4
  Summary: SDK for digital service providers on UnitySVC
5
5
  Author-email: Bo Peng <bo.peng@unitysvc.com>
6
6
  Maintainer-email: Bo Peng <bo.peng@unitysvc.com>
7
- License: MIT
7
+ License-Expression: MIT
8
8
  Project-URL: bugs, https://github.com/unitysvc/unitysvc-services/issues
9
9
  Project-URL: changelog, https://github.com/unitysvc/unitysvc-services/blob/master/changelog.md
10
10
  Project-URL: homepage, https://github.com/unitysvc/unitysvc-services
@@ -13,6 +13,7 @@ Description-Content-Type: text/markdown
13
13
  License-File: LICENSE
14
14
  Requires-Dist: typer
15
15
  Requires-Dist: pydantic
16
+ Requires-Dist: email-validator
16
17
  Requires-Dist: jsonschema
17
18
  Requires-Dist: jinja2
18
19
  Requires-Dist: rich
@@ -52,11 +53,11 @@ Client library and CLI tools for digital service providers to interact with the
52
53
 
53
54
  UnitySVC Provider SDK enables digital service providers to manage their service offerings through a **local-first, version-controlled workflow**:
54
55
 
55
- - **Define** service data using schema-validated files (JSON/TOML)
56
- - **Manage** everything locally in git-controlled directories
57
- - **Validate** data against schemas before publishing
58
- - **Publish** to UnitySVC platform when ready
59
- - **Automate** with populate scripts for dynamic catalogs
56
+ - **Define** service data using schema-validated files (JSON/TOML)
57
+ - **Manage** everything locally in git-controlled directories
58
+ - **Validate** data against schemas before publishing
59
+ - **Publish** to UnitySVC platform when ready
60
+ - **Automate** with populate scripts for dynamic catalogs
60
61
 
61
62
  ## Installation
62
63
 
@@ -78,27 +79,29 @@ unitysvc_services init seller my-marketplace
78
79
  unitysvc_services validate
79
80
  unitysvc_services format
80
81
 
81
- # Publish to platform
82
- export UNITYSVC_BACKEND_URL="https://api.unitysvc.com/api/v1"
82
+ # Publish to platform (publishes all: sellers, providers, offerings, listings)
83
+ export UNITYSVC_BASE_URL="https://api.unitysvc.com/api/v1"
83
84
  export UNITYSVC_API_KEY="your-api-key"
85
+ unitysvc_services publish
84
86
 
87
+ # Or publish specific types only
85
88
  unitysvc_services publish providers
86
- unitysvc_services publish sellers
87
- unitysvc_services publish offerings
88
- unitysvc_services publish listings
89
89
 
90
- # Verify
90
+ # Verify with default fields
91
91
  unitysvc_services query offerings
92
+
93
+ # Query with custom fields
94
+ unitysvc_services query providers --fields id,name,contact_email
92
95
  ```
93
96
 
94
97
  ## Key Features
95
98
 
96
- - 📋 **Pydantic Models** - Type-safe data models for all entities
97
- - ✅ **Data Validation** - Comprehensive schema validation
98
- - 🔄 **Local-First** - Work offline, commit to git, publish when ready
99
- - 🚀 **CLI Tools** - Complete command-line interface
100
- - 🤖 **Automation** - Script-based service generation
101
- - 📝 **Multiple Formats** - Support for JSON and TOML
99
+ - 📋 **Pydantic Models** - Type-safe data models for all entities
100
+ - ✅ **Data Validation** - Comprehensive schema validation
101
+ - 🔄 **Local-First** - Work offline, commit to git, publish when ready
102
+ - 🚀 **CLI Tools** - Complete command-line interface
103
+ - 🤖 **Automation** - Script-based service generation
104
+ - 📝 **Multiple Formats** - Support for JSON and TOML
102
105
 
103
106
  ## Workflows
104
107
 
@@ -134,34 +137,34 @@ See [Data Structure Documentation](https://unitysvc-services.readthedocs.io/en/l
134
137
 
135
138
  ## CLI Commands
136
139
 
137
- | Command | Description |
138
- |---------|-------------|
139
- | `init` | Initialize new data files from schemas |
140
- | `list` | List local data files |
141
- | `query` | Query backend API for published data |
142
- | `publish` | Publish data to backend |
143
- | `update` | Update local file fields |
144
- | `validate` | Validate data consistency |
145
- | `format` | Format data files |
146
- | `populate` | Execute provider populate scripts |
140
+ | Command | Description |
141
+ | ---------- | -------------------------------------- |
142
+ | `init` | Initialize new data files from schemas |
143
+ | `list` | List local data files |
144
+ | `query` | Query backend API for published data |
145
+ | `publish` | Publish data to backend |
146
+ | `update` | Update local file fields |
147
+ | `validate` | Validate data consistency |
148
+ | `format` | Format data files |
149
+ | `populate` | Execute provider populate scripts |
147
150
 
148
151
  Run `unitysvc_services --help` or see [CLI Reference](https://unitysvc-services.readthedocs.io/en/latest/cli-reference/) for complete documentation.
149
152
 
150
153
  ## Documentation
151
154
 
152
- - **[Getting Started](https://unitysvc-services.readthedocs.io/en/latest/getting-started/)** - Installation and first steps
153
- - **[Data Structure](https://unitysvc-services.readthedocs.io/en/latest/data-structure/)** - File organization rules
154
- - **[Workflows](https://unitysvc-services.readthedocs.io/en/latest/workflows/)** - Manual and automated patterns
155
- - **[CLI Reference](https://unitysvc-services.readthedocs.io/en/latest/cli-reference/)** - All commands and options
156
- - **[File Schemas](https://unitysvc-services.readthedocs.io/en/latest/file-schemas/)** - Schema specifications
157
- - **[Python API](https://unitysvc-services.readthedocs.io/en/latest/api-reference/)** - Programmatic usage
155
+ - **[Getting Started](https://unitysvc-services.readthedocs.io/en/latest/getting-started/)** - Installation and first steps
156
+ - **[Data Structure](https://unitysvc-services.readthedocs.io/en/latest/data-structure/)** - File organization rules
157
+ - **[Workflows](https://unitysvc-services.readthedocs.io/en/latest/workflows/)** - Manual and automated patterns
158
+ - **[CLI Reference](https://unitysvc-services.readthedocs.io/en/latest/cli-reference/)** - All commands and options
159
+ - **[File Schemas](https://unitysvc-services.readthedocs.io/en/latest/file-schemas/)** - Schema specifications
160
+ - **[Python API](https://unitysvc-services.readthedocs.io/en/latest/api-reference/)** - Programmatic usage
158
161
 
159
162
  ## Links
160
163
 
161
- - **PyPI**: https://pypi.org/project/unitysvc-services/
162
- - **Documentation**: https://unitysvc-services.readthedocs.io
163
- - **Source Code**: https://github.com/unitysvc/unitysvc-services
164
- - **Issue Tracker**: https://github.com/unitysvc/unitysvc-services/issues
164
+ - **PyPI**: https://pypi.org/project/unitysvc-services/
165
+ - **Documentation**: https://unitysvc-services.readthedocs.io
166
+ - **Source Code**: https://github.com/unitysvc/unitysvc-services
167
+ - **Issue Tracker**: https://github.com/unitysvc/unitysvc-services/issues
165
168
 
166
169
  ## License
167
170