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.
- unitysvc_services/api.py +278 -0
- unitysvc_services/format_data.py +2 -7
- unitysvc_services/list.py +14 -43
- unitysvc_services/models/base.py +139 -0
- unitysvc_services/models/listing_v1.py +23 -3
- unitysvc_services/models/provider_v1.py +23 -2
- unitysvc_services/models/seller_v1.py +12 -6
- unitysvc_services/models/service_v1.py +8 -1
- unitysvc_services/populate.py +2 -6
- unitysvc_services/publisher.py +732 -467
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +521 -318
- unitysvc_services/update.py +10 -14
- unitysvc_services/utils.py +105 -7
- unitysvc_services/validator.py +194 -10
- {unitysvc_services-0.1.0.dist-info → unitysvc_services-0.1.4.dist-info}/METADATA +42 -39
- unitysvc_services-0.1.4.dist-info/RECORD +25 -0
- unitysvc_services-0.1.0.dist-info/RECORD +0 -23
- {unitysvc_services-0.1.0.dist-info → unitysvc_services-0.1.4.dist-info}/WHEEL +0 -0
- {unitysvc_services-0.1.0.dist-info → unitysvc_services-0.1.4.dist-info}/entry_points.txt +0 -0
- {unitysvc_services-0.1.0.dist-info → unitysvc_services-0.1.4.dist-info}/licenses/LICENSE +0 -0
- {unitysvc_services-0.1.0.dist-info → unitysvc_services-0.1.4.dist-info}/top_level.txt +0 -0
unitysvc_services/update.py
CHANGED
@@ -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:
|
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
|
-
|
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:
|
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
|
-
|
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(
|
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(
|
unitysvc_services/utils.py
CHANGED
@@ -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
|
-
|
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:
|
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 =
|
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:
|
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
|
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
|
141
|
-
if not all(data.get(k) == v for k, v in
|
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
|
unitysvc_services/validator.py
CHANGED
@@ -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
|
-
#
|
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:
|
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
|
-
|
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,
|
505
|
-
|
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.
|
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
|
-
-
|
56
|
-
-
|
57
|
-
-
|
58
|
-
-
|
59
|
-
-
|
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
|
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
|
-
-
|
97
|
-
-
|
98
|
-
-
|
99
|
-
-
|
100
|
-
-
|
101
|
-
-
|
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
|
138
|
-
|
139
|
-
| `init`
|
140
|
-
| `list`
|
141
|
-
| `query`
|
142
|
-
| `publish`
|
143
|
-
| `update`
|
144
|
-
| `validate` | Validate data consistency
|
145
|
-
| `format`
|
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
|
-
-
|
153
|
-
-
|
154
|
-
-
|
155
|
-
-
|
156
|
-
-
|
157
|
-
-
|
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
|
-
-
|
162
|
-
-
|
163
|
-
-
|
164
|
-
-
|
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
|
|