unitysvc-services 0.1.1__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- unitysvc_services/api.py +321 -0
- unitysvc_services/cli.py +2 -1
- unitysvc_services/format_data.py +2 -7
- unitysvc_services/list.py +14 -43
- unitysvc_services/models/base.py +169 -102
- unitysvc_services/models/listing_v1.py +25 -9
- unitysvc_services/models/provider_v1.py +19 -8
- unitysvc_services/models/seller_v1.py +10 -8
- unitysvc_services/models/service_v1.py +8 -1
- unitysvc_services/populate.py +20 -6
- unitysvc_services/publisher.py +897 -462
- unitysvc_services/py.typed +0 -0
- unitysvc_services/query.py +577 -384
- unitysvc_services/test.py +769 -0
- unitysvc_services/update.py +4 -13
- unitysvc_services/utils.py +55 -6
- unitysvc_services/validator.py +117 -86
- unitysvc_services-0.1.5.dist-info/METADATA +182 -0
- unitysvc_services-0.1.5.dist-info/RECORD +26 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/entry_points.txt +1 -0
- unitysvc_services-0.1.1.dist-info/METADATA +0 -173
- unitysvc_services-0.1.1.dist-info/RECORD +0 -23
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/WHEEL +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/licenses/LICENSE +0 -0
- {unitysvc_services-0.1.1.dist-info → unitysvc_services-0.1.5.dist-info}/top_level.txt +0 -0
unitysvc_services/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
|
unitysvc_services/utils.py
CHANGED
@@ -7,6 +7,7 @@ from pathlib import Path
|
|
7
7
|
from typing import Any
|
8
8
|
|
9
9
|
import tomli_w
|
10
|
+
from jinja2 import Template
|
10
11
|
|
11
12
|
|
12
13
|
def load_data_file(file_path: Path) -> tuple[dict[str, Any], str]:
|
@@ -56,9 +57,7 @@ def write_data_file(file_path: Path, data: dict[str, Any], format: str) -> None:
|
|
56
57
|
|
57
58
|
|
58
59
|
@lru_cache(maxsize=128)
|
59
|
-
def find_data_files(
|
60
|
-
data_dir: Path, extensions: tuple[str, ...] | None = None
|
61
|
-
) -> list[Path]:
|
60
|
+
def find_data_files(data_dir: Path, extensions: tuple[str, ...] | None = None) -> list[Path]:
|
62
61
|
"""
|
63
62
|
Find all data files in a directory with specified extensions.
|
64
63
|
|
@@ -208,9 +207,7 @@ def resolve_provider_name(file_path: Path) -> str | None:
|
|
208
207
|
return None
|
209
208
|
|
210
209
|
|
211
|
-
def resolve_service_name_for_listing(
|
212
|
-
listing_file: Path, listing_data: dict[str, Any]
|
213
|
-
) -> str | None:
|
210
|
+
def resolve_service_name_for_listing(listing_file: Path, listing_data: dict[str, Any]) -> str | None:
|
214
211
|
"""
|
215
212
|
Resolve the service name for a listing file.
|
216
213
|
|
@@ -340,3 +337,55 @@ def convert_convenience_fields_to_documents(
|
|
340
337
|
del data[terms_field]
|
341
338
|
|
342
339
|
return data
|
340
|
+
|
341
|
+
|
342
|
+
def render_template_file(
|
343
|
+
file_path: Path,
|
344
|
+
listing: dict[str, Any] | None = None,
|
345
|
+
offering: dict[str, Any] | None = None,
|
346
|
+
provider: dict[str, Any] | None = None,
|
347
|
+
seller: dict[str, Any] | None = None,
|
348
|
+
) -> tuple[str, str]:
|
349
|
+
"""Render a Jinja2 template file and return content and new filename.
|
350
|
+
|
351
|
+
If the file is not a template (.j2 extension), returns the file content as-is
|
352
|
+
and the original filename.
|
353
|
+
|
354
|
+
Args:
|
355
|
+
file_path: Path to the file (may or may not be a .j2 template)
|
356
|
+
listing: Listing data for template rendering (optional)
|
357
|
+
offering: Offering data for template rendering (optional)
|
358
|
+
provider: Provider data for template rendering (optional)
|
359
|
+
seller: Seller data for template rendering (optional)
|
360
|
+
|
361
|
+
Returns:
|
362
|
+
Tuple of (rendered_content, new_filename_without_j2)
|
363
|
+
|
364
|
+
Raises:
|
365
|
+
Exception: If template rendering fails
|
366
|
+
"""
|
367
|
+
# Read file content
|
368
|
+
with open(file_path, encoding="utf-8") as f:
|
369
|
+
file_content = f.read()
|
370
|
+
|
371
|
+
# Check if this is a Jinja2 template
|
372
|
+
is_template = file_path.name.endswith(".j2")
|
373
|
+
|
374
|
+
if is_template:
|
375
|
+
# Render the template
|
376
|
+
template = Template(file_content)
|
377
|
+
rendered_content = template.render(
|
378
|
+
listing=listing or {},
|
379
|
+
offering=offering or {},
|
380
|
+
provider=provider or {},
|
381
|
+
seller=seller or {},
|
382
|
+
)
|
383
|
+
|
384
|
+
# Strip .j2 from filename
|
385
|
+
# Example: test.py.j2 -> test.py
|
386
|
+
new_filename = file_path.name[:-3] # Remove last 3 characters (.j2)
|
387
|
+
|
388
|
+
return rendered_content, new_filename
|
389
|
+
else:
|
390
|
+
# Not a template - return as-is
|
391
|
+
return file_content, file_path.name
|
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
|
@@ -60,16 +59,12 @@ class DataValidator:
|
|
60
59
|
if "anyOf" in obj:
|
61
60
|
any_of = obj["anyOf"]
|
62
61
|
# Count non-null items for the check
|
63
|
-
non_null_items = [
|
64
|
-
item for item in any_of if item.get("type") != "null"
|
65
|
-
]
|
62
|
+
non_null_items = [item for item in any_of if item.get("type") != "null"]
|
66
63
|
has_plain_string = any(
|
67
|
-
item.get("type") == "string" and "format" not in item
|
68
|
-
for item in non_null_items
|
64
|
+
item.get("type") == "string" and "format" not in item for item in non_null_items
|
69
65
|
)
|
70
66
|
has_uri_string = any(
|
71
|
-
item.get("type") == "string" and item.get("format") == "uri"
|
72
|
-
for item in non_null_items
|
67
|
+
item.get("type") == "string" and item.get("format") == "uri" for item in non_null_items
|
73
68
|
)
|
74
69
|
|
75
70
|
# Check for Union[str, HttpUrl] or Union[str, HttpUrl, None]
|
@@ -84,9 +79,7 @@ class DataValidator:
|
|
84
79
|
|
85
80
|
# Check other schema structures
|
86
81
|
for key, value in obj.items():
|
87
|
-
if key not in ["properties", "anyOf"] and isinstance(
|
88
|
-
value, dict | list
|
89
|
-
):
|
82
|
+
if key not in ["properties", "anyOf"] and isinstance(value, dict | list):
|
90
83
|
traverse_schema(value, path)
|
91
84
|
|
92
85
|
elif isinstance(obj, list):
|
@@ -96,9 +89,7 @@ class DataValidator:
|
|
96
89
|
traverse_schema(schema)
|
97
90
|
return union_fields
|
98
91
|
|
99
|
-
def validate_file_references(
|
100
|
-
self, data: dict[str, Any], file_path: Path, union_fields: set[str]
|
101
|
-
) -> list[str]:
|
92
|
+
def validate_file_references(self, data: dict[str, Any], file_path: Path, union_fields: set[str]) -> list[str]:
|
102
93
|
"""
|
103
94
|
Validate that file references in Union[str, HttpUrl] fields exist.
|
104
95
|
|
@@ -120,9 +111,7 @@ class DataValidator:
|
|
120
111
|
):
|
121
112
|
# Empty string is not a valid file reference
|
122
113
|
if value == "":
|
123
|
-
errors.append(
|
124
|
-
f"Empty string in field '{new_path}' is not a valid file reference or URL"
|
125
|
-
)
|
114
|
+
errors.append(f"Empty string in field '{new_path}' is not a valid file reference or URL")
|
126
115
|
# It's a file reference, must be relative path
|
127
116
|
elif Path(value).is_absolute():
|
128
117
|
errors.append(
|
@@ -172,9 +161,7 @@ class DataValidator:
|
|
172
161
|
check_field(data, str(file_path))
|
173
162
|
return errors
|
174
163
|
|
175
|
-
def validate_name_consistency(
|
176
|
-
self, data: dict[str, Any], file_path: Path, schema_name: str
|
177
|
-
) -> list[str]:
|
164
|
+
def validate_name_consistency(self, data: dict[str, Any], file_path: Path, schema_name: str) -> list[str]:
|
178
165
|
"""Validate that the name field matches the directory name."""
|
179
166
|
errors: list[str] = []
|
180
167
|
|
@@ -199,9 +186,7 @@ class DataValidator:
|
|
199
186
|
elif file_path.name in ["service.json", "service.toml"]:
|
200
187
|
# For service.json, the service directory should match the service name
|
201
188
|
service_directory_name = file_path.parent.name
|
202
|
-
if self._normalize_name(name_value) != self._normalize_name(
|
203
|
-
service_directory_name
|
204
|
-
):
|
189
|
+
if self._normalize_name(name_value) != self._normalize_name(service_directory_name):
|
205
190
|
normalized_name = self._normalize_name(name_value)
|
206
191
|
errors.append(
|
207
192
|
f"Service name '{name_value}' does not match "
|
@@ -220,9 +205,55 @@ class DataValidator:
|
|
220
205
|
normalized = normalized.strip("-")
|
221
206
|
return normalized
|
222
207
|
|
223
|
-
def
|
224
|
-
|
225
|
-
|
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
|
+
|
256
|
+
def load_data_file(self, file_path: Path) -> tuple[dict[str, Any] | None, list[str]]:
|
226
257
|
"""Load data from JSON or TOML file."""
|
227
258
|
errors: list[str] = []
|
228
259
|
|
@@ -237,9 +268,7 @@ class DataValidator:
|
|
237
268
|
return None, [f"Unsupported file format: {file_path.suffix}"]
|
238
269
|
return data, errors
|
239
270
|
except Exception as e:
|
240
|
-
format_name = {".json": "JSON", ".toml": "TOML"}.get(
|
241
|
-
file_path.suffix, "data"
|
242
|
-
)
|
271
|
+
format_name = {".json": "JSON", ".toml": "TOML"}.get(file_path.suffix, "data")
|
243
272
|
return None, [f"Failed to parse {format_name}: {e}"]
|
244
273
|
|
245
274
|
def validate_data_file(self, file_path: Path) -> tuple[bool, list[str]]:
|
@@ -268,20 +297,20 @@ class DataValidator:
|
|
268
297
|
|
269
298
|
# Validate against schema with format checking enabled
|
270
299
|
try:
|
271
|
-
validator = Draft7Validator(
|
272
|
-
schema, format_checker=Draft7Validator.FORMAT_CHECKER
|
273
|
-
)
|
300
|
+
validator = Draft7Validator(schema, format_checker=Draft7Validator.FORMAT_CHECKER)
|
274
301
|
validator.check_schema(schema) # Validate the schema itself
|
275
302
|
validation_errors = list(validator.iter_errors(data))
|
276
303
|
for error in validation_errors:
|
277
304
|
errors.append(f"Schema validation error: {error.message}")
|
278
305
|
if error.absolute_path:
|
279
|
-
errors.append(
|
280
|
-
f" Path: {'.'.join(str(p) for p in error.absolute_path)}"
|
281
|
-
)
|
306
|
+
errors.append(f" Path: {'.'.join(str(p) for p in error.absolute_path)}")
|
282
307
|
except Exception as e:
|
283
308
|
errors.append(f"Validation error: {e}")
|
284
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
|
+
|
285
314
|
# Find Union[str, HttpUrl] fields and validate file references
|
286
315
|
union_fields = self.find_union_fields(schema)
|
287
316
|
file_ref_errors = self.validate_file_references(data, file_path, union_fields)
|
@@ -293,8 +322,15 @@ class DataValidator:
|
|
293
322
|
|
294
323
|
return len(errors) == 0, errors
|
295
324
|
|
296
|
-
def
|
297
|
-
"""Validate a
|
325
|
+
def validate_jinja2_file(self, file_path: Path) -> tuple[bool, list[str]]:
|
326
|
+
"""Validate a file with Jinja2 template syntax.
|
327
|
+
|
328
|
+
This validates any file ending with .j2 extension, including:
|
329
|
+
- .md.j2 (Jinja2 markdown templates)
|
330
|
+
- .py.j2 (Jinja2 Python code example templates)
|
331
|
+
- .js.j2 (Jinja2 JavaScript code example templates)
|
332
|
+
- .sh.j2 (Jinja2 shell script templates)
|
333
|
+
"""
|
298
334
|
errors: list[str] = []
|
299
335
|
|
300
336
|
try:
|
@@ -315,7 +351,7 @@ class DataValidator:
|
|
315
351
|
|
316
352
|
return len(errors) == 0, errors
|
317
353
|
except Exception as e:
|
318
|
-
return False, [f"Failed to read
|
354
|
+
return False, [f"Failed to read template file: {e}"]
|
319
355
|
|
320
356
|
def validate_seller_uniqueness(self) -> tuple[bool, list[str]]:
|
321
357
|
"""
|
@@ -331,6 +367,10 @@ class DataValidator:
|
|
331
367
|
|
332
368
|
# Find all data files with seller_v1 schema
|
333
369
|
for file_path in self.data_dir.rglob("*"):
|
370
|
+
# Skip hidden directories (those starting with .)
|
371
|
+
if any(part.startswith(".") for part in file_path.parts):
|
372
|
+
continue
|
373
|
+
|
334
374
|
if file_path.is_file() and file_path.suffix in [".json", ".toml"]:
|
335
375
|
try:
|
336
376
|
data, load_errors = self.load_data_file(file_path)
|
@@ -346,9 +386,7 @@ class DataValidator:
|
|
346
386
|
"No seller file found. Each repository must have exactly one data file using the 'seller_v1' schema."
|
347
387
|
)
|
348
388
|
elif len(seller_files) > 1:
|
349
|
-
errors.append(
|
350
|
-
f"Found {len(seller_files)} seller files, but only one is allowed per repository:"
|
351
|
-
)
|
389
|
+
errors.append(f"Found {len(seller_files)} seller files, but only one is allowed per repository:")
|
352
390
|
for seller_file in seller_files:
|
353
391
|
errors.append(f" - {seller_file}")
|
354
392
|
|
@@ -366,20 +404,17 @@ class DataValidator:
|
|
366
404
|
|
367
405
|
warnings: list[str] = []
|
368
406
|
|
369
|
-
# Find all provider files
|
370
|
-
provider_files =
|
407
|
+
# Find all provider files (skip hidden directories)
|
408
|
+
provider_files = [
|
409
|
+
f for f in self.data_dir.glob("*/provider.*") if not any(part.startswith(".") for part in f.parts)
|
410
|
+
]
|
371
411
|
|
372
412
|
for provider_file in provider_files:
|
373
413
|
try:
|
374
|
-
# Load provider data
|
375
|
-
data =
|
376
|
-
if
|
377
|
-
|
378
|
-
data = json.load(f)
|
379
|
-
elif provider_file.suffix == ".toml":
|
380
|
-
with open(provider_file, "rb") as f:
|
381
|
-
data = toml.load(f)
|
382
|
-
else:
|
414
|
+
# Load provider data using existing helper method
|
415
|
+
data, load_errors = self.load_data_file(provider_file)
|
416
|
+
if load_errors or data is None:
|
417
|
+
warnings.append(f"Failed to load provider file {provider_file}: {load_errors}")
|
383
418
|
continue
|
384
419
|
|
385
420
|
# Parse as ProviderV1
|
@@ -400,9 +435,7 @@ class DataValidator:
|
|
400
435
|
)
|
401
436
|
|
402
437
|
except Exception as e:
|
403
|
-
warnings.append(
|
404
|
-
f"Error checking provider status in {provider_file}: {e}"
|
405
|
-
)
|
438
|
+
warnings.append(f"Error checking provider status in {provider_file}: {e}")
|
406
439
|
|
407
440
|
# Return True (valid) but with warnings
|
408
441
|
return True, warnings
|
@@ -418,20 +451,15 @@ class DataValidator:
|
|
418
451
|
|
419
452
|
warnings: list[str] = []
|
420
453
|
|
421
|
-
# Find all seller files
|
422
|
-
seller_files =
|
454
|
+
# Find all seller files (skip hidden files)
|
455
|
+
seller_files = [f for f in self.data_dir.glob("seller.*") if not f.name.startswith(".")]
|
423
456
|
|
424
457
|
for seller_file in seller_files:
|
425
458
|
try:
|
426
|
-
# Load seller data
|
427
|
-
data =
|
428
|
-
if
|
429
|
-
|
430
|
-
data = json.load(f)
|
431
|
-
elif seller_file.suffix == ".toml":
|
432
|
-
with open(seller_file, "rb") as f:
|
433
|
-
data = toml.load(f)
|
434
|
-
else:
|
459
|
+
# Load seller data using existing helper method
|
460
|
+
data, load_errors = self.load_data_file(seller_file)
|
461
|
+
if load_errors or data is None:
|
462
|
+
warnings.append(f"Failed to load seller file {seller_file}: {load_errors}")
|
435
463
|
continue
|
436
464
|
|
437
465
|
# Parse as SellerV1
|
@@ -475,15 +503,24 @@ class DataValidator:
|
|
475
503
|
provider_warnings,
|
476
504
|
) # Warnings, not errors
|
477
505
|
|
478
|
-
# Find all data and MD files recursively
|
506
|
+
# Find all data and MD files recursively, skipping hidden directories
|
479
507
|
for file_path in self.data_dir.rglob("*"):
|
480
|
-
|
508
|
+
# Skip hidden directories (those starting with .)
|
509
|
+
if any(part.startswith(".") for part in file_path.parts):
|
510
|
+
continue
|
511
|
+
|
512
|
+
# Check if file should be validated
|
513
|
+
# Only .j2 files (Jinja2 templates) are validated for Jinja2 syntax
|
514
|
+
is_template = file_path.name.endswith(".j2")
|
515
|
+
is_data_file = file_path.suffix in [".json", ".toml"]
|
516
|
+
|
517
|
+
if file_path.is_file() and (is_data_file or is_template):
|
481
518
|
relative_path = file_path.relative_to(self.data_dir)
|
482
519
|
|
483
|
-
if
|
520
|
+
if is_data_file:
|
484
521
|
is_valid, errors = self.validate_data_file(file_path)
|
485
|
-
elif
|
486
|
-
is_valid, errors = self.
|
522
|
+
elif is_template:
|
523
|
+
is_valid, errors = self.validate_jinja2_file(file_path)
|
487
524
|
else:
|
488
525
|
continue
|
489
526
|
|
@@ -525,9 +562,7 @@ class DataValidator:
|
|
525
562
|
if schema == "service_v1":
|
526
563
|
service_name = data.get("name")
|
527
564
|
if not service_name:
|
528
|
-
raise DataValidationError(
|
529
|
-
f"Service file {file_path} missing 'name' field"
|
530
|
-
)
|
565
|
+
raise DataValidationError(f"Service file {file_path} missing 'name' field")
|
531
566
|
|
532
567
|
# Check for duplicate service names in same directory
|
533
568
|
if service_name in services:
|
@@ -555,9 +590,7 @@ class DataValidator:
|
|
555
590
|
if service_name:
|
556
591
|
# If service_name is explicitly defined, it must match a service in the directory
|
557
592
|
if service_name not in services:
|
558
|
-
available_services = (
|
559
|
-
", ".join(services.keys()) if services else "none"
|
560
|
-
)
|
593
|
+
available_services = ", ".join(services.keys()) if services else "none"
|
561
594
|
raise DataValidationError(
|
562
595
|
f"Listing file {listing_file} references service_name '{service_name}' "
|
563
596
|
f"which does not exist in the same directory.\n"
|
@@ -591,6 +624,10 @@ class DataValidator:
|
|
591
624
|
|
592
625
|
for pattern in ["*.json", "*.toml"]:
|
593
626
|
for file_path in data_dir.rglob(pattern):
|
627
|
+
# Skip hidden directories (those starting with .)
|
628
|
+
if any(part.startswith(".") for part in file_path.parts):
|
629
|
+
continue
|
630
|
+
|
594
631
|
try:
|
595
632
|
data, load_errors = self.load_data_file(file_path)
|
596
633
|
if load_errors or data is None:
|
@@ -621,7 +658,7 @@ console = Console()
|
|
621
658
|
def validate(
|
622
659
|
data_dir: Path | None = typer.Argument(
|
623
660
|
None,
|
624
|
-
help="Directory containing data files to validate (default:
|
661
|
+
help="Directory containing data files to validate (default: current directory)",
|
625
662
|
),
|
626
663
|
):
|
627
664
|
"""
|
@@ -634,11 +671,7 @@ def validate(
|
|
634
671
|
"""
|
635
672
|
# Determine data directory
|
636
673
|
if data_dir is None:
|
637
|
-
|
638
|
-
if data_dir_str:
|
639
|
-
data_dir = Path(data_dir_str)
|
640
|
-
else:
|
641
|
-
data_dir = Path.cwd() / "data"
|
674
|
+
data_dir = Path.cwd()
|
642
675
|
|
643
676
|
if not data_dir.exists():
|
644
677
|
console.print(f"[red]✗[/red] Data directory not found: {data_dir}")
|
@@ -668,9 +701,7 @@ def validate(
|
|
668
701
|
validation_errors.extend(directory_errors)
|
669
702
|
|
670
703
|
if validation_errors:
|
671
|
-
console.print(
|
672
|
-
f"[red]✗ Validation failed with {len(validation_errors)} error(s):[/red]"
|
673
|
-
)
|
704
|
+
console.print(f"[red]✗ Validation failed with {len(validation_errors)} error(s):[/red]")
|
674
705
|
console.print()
|
675
706
|
for i, error in enumerate(validation_errors, 1):
|
676
707
|
console.print(f"[red]{i}.[/red] {error}")
|