ostruct-cli 0.6.1__py3-none-any.whl → 0.6.2__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.
ostruct/cli/cli.py CHANGED
@@ -78,7 +78,11 @@ from .path_utils import validate_path_mapping
78
78
  from .security import SecurityManager
79
79
  from .serialization import LogSerializer
80
80
  from .template_env import create_jinja_env
81
- from .template_utils import SystemPromptError, render_template
81
+ from .template_utils import (
82
+ SystemPromptError,
83
+ render_template,
84
+ validate_json_schema,
85
+ )
82
86
  from .token_utils import estimate_tokens_with_encoding
83
87
 
84
88
  # Constants
@@ -831,7 +835,7 @@ def validate_schema_file(
831
835
  logger.error(msg)
832
836
  raise SchemaFileError(msg, schema_path=path)
833
837
  except Exception as e:
834
- if isinstance(e, InvalidJSONError):
838
+ if isinstance(e, (InvalidJSONError, SchemaValidationError)):
835
839
  raise
836
840
  msg = f"Failed to read schema file {path}: {e}"
837
841
  logger.error(msg)
@@ -846,7 +850,13 @@ def validate_schema_file(
846
850
  if not isinstance(schema, dict):
847
851
  msg = f"Schema in {path} must be a JSON object"
848
852
  logger.error(msg)
849
- raise SchemaValidationError(msg, context={"path": path})
853
+ raise SchemaValidationError(
854
+ msg,
855
+ context={
856
+ "validation_type": "schema",
857
+ "schema_path": path,
858
+ },
859
+ )
850
860
 
851
861
  # Validate schema structure
852
862
  if "schema" in schema:
@@ -856,7 +866,13 @@ def validate_schema_file(
856
866
  if not isinstance(inner_schema, dict):
857
867
  msg = f"Inner schema in {path} must be a JSON object"
858
868
  logger.error(msg)
859
- raise SchemaValidationError(msg, context={"path": path})
869
+ raise SchemaValidationError(
870
+ msg,
871
+ context={
872
+ "validation_type": "schema",
873
+ "schema_path": path,
874
+ },
875
+ )
860
876
  if verbose:
861
877
  logger.debug("Inner schema validated successfully")
862
878
  logger.debug(
@@ -871,7 +887,20 @@ def validate_schema_file(
871
887
  if "type" not in schema.get("schema", schema):
872
888
  msg = f"Schema in {path} must specify a type"
873
889
  logger.error(msg)
874
- raise SchemaValidationError(msg, context={"path": path})
890
+ raise SchemaValidationError(
891
+ msg,
892
+ context={
893
+ "validation_type": "schema",
894
+ "schema_path": path,
895
+ },
896
+ )
897
+
898
+ # Validate schema against JSON Schema spec
899
+ try:
900
+ validate_json_schema(schema)
901
+ except SchemaValidationError as e:
902
+ logger.error("Schema validation error: %s", str(e))
903
+ raise # Re-raise to preserve error chain
875
904
 
876
905
  # Return the full schema including wrapper
877
906
  return schema
@@ -1225,19 +1254,24 @@ def handle_error(e: Exception) -> None:
1225
1254
  Provides enhanced debug logging for CLI errors.
1226
1255
  """
1227
1256
  # 1. Determine error type and message
1228
- if isinstance(e, click.UsageError):
1257
+ if isinstance(e, SchemaValidationError):
1258
+ msg = str(e) # Already formatted in SchemaValidationError
1259
+ exit_code = e.exit_code
1260
+ elif isinstance(e, ModelCreationError):
1261
+ # Unwrap ModelCreationError that might wrap SchemaValidationError
1262
+ if isinstance(e.__cause__, SchemaValidationError):
1263
+ return handle_error(e.__cause__)
1264
+ msg = f"Model creation error: {str(e)}"
1265
+ exit_code = ExitCode.SCHEMA_ERROR
1266
+ elif isinstance(e, click.UsageError):
1229
1267
  msg = f"Usage error: {str(e)}"
1230
1268
  exit_code = ExitCode.USAGE_ERROR
1231
1269
  elif isinstance(e, SchemaFileError):
1232
- # Preserve specific schema error handling
1233
1270
  msg = str(e) # Use existing __str__ formatting
1234
1271
  exit_code = ExitCode.SCHEMA_ERROR
1235
1272
  elif isinstance(e, (InvalidJSONError, json.JSONDecodeError)):
1236
1273
  msg = f"Invalid JSON error: {str(e)}"
1237
1274
  exit_code = ExitCode.DATA_ERROR
1238
- elif isinstance(e, SchemaValidationError):
1239
- msg = f"Schema validation error: {str(e)}"
1240
- exit_code = ExitCode.VALIDATION_ERROR
1241
1275
  elif isinstance(e, CLIError):
1242
1276
  msg = str(e) # Use existing __str__ formatting
1243
1277
  exit_code = ExitCode(e.exit_code) # Convert int to ExitCode
@@ -1249,7 +1283,7 @@ def handle_error(e: Exception) -> None:
1249
1283
  if isinstance(e, CLIError) and logger.isEnabledFor(logging.DEBUG):
1250
1284
  # Format context fields with lowercase keys and simple values
1251
1285
  context_str = ""
1252
- if hasattr(e, "context"):
1286
+ if hasattr(e, "context") and e.context:
1253
1287
  for key, value in sorted(e.context.items()):
1254
1288
  if key not in {
1255
1289
  "timestamp",
@@ -1257,13 +1291,18 @@ def handle_error(e: Exception) -> None:
1257
1291
  "version",
1258
1292
  "python_version",
1259
1293
  }:
1260
- context_str += f"{key.lower()}: {value}\n"
1294
+ if isinstance(value, dict):
1295
+ context_str += (
1296
+ f"{key.lower()}:\n{json.dumps(value, indent=2)}\n"
1297
+ )
1298
+ else:
1299
+ context_str += f"{key.lower()}: {value}\n"
1261
1300
 
1262
- logger.debug(
1263
- "Error details:\n"
1264
- f"Type: {type(e).__name__}\n"
1265
- f"{context_str.rstrip()}"
1266
- )
1301
+ logger.debug(
1302
+ "Error details:\n"
1303
+ f"Type: {type(e).__name__}\n"
1304
+ f"{context_str.rstrip()}"
1305
+ )
1267
1306
  elif not isinstance(e, click.UsageError):
1268
1307
  logger.error(msg, exc_info=True)
1269
1308
  else:
@@ -1467,30 +1506,11 @@ def run(
1467
1506
  ) -> None:
1468
1507
  """Run a structured task with template and schema.
1469
1508
 
1470
- TASK_TEMPLATE is the path to your Jinja2 template file that defines the task.
1471
- SCHEMA_FILE is the path to your JSON schema file that defines the expected output structure.
1472
-
1473
- The command supports various options for file handling, variable definition,
1474
- model configuration, and output control. Use --help to see all available options.
1475
-
1476
- Examples:
1477
- # Basic usage
1478
- ostruct run task.j2 schema.json
1479
-
1480
- # Process multiple files
1481
- ostruct run task.j2 schema.json -f code main.py -f test tests/test_main.py
1482
-
1483
- # Scan directories recursively
1484
- ostruct run task.j2 schema.json -d src ./src -R
1485
-
1486
- # Define variables
1487
- ostruct run task.j2 schema.json -V debug=true -J config='{"env":"prod"}'
1488
-
1489
- # Configure model
1490
- ostruct run task.j2 schema.json -m gpt-4 --temperature 0.7 --max-output-tokens 1000
1491
-
1492
- # Control output
1493
- ostruct run task.j2 schema.json --output-file result.json --verbose
1509
+ Args:
1510
+ ctx: Click context
1511
+ task_template: Path to task template file
1512
+ schema_file: Path to schema file
1513
+ **kwargs: Additional CLI options
1494
1514
  """
1495
1515
  try:
1496
1516
  # Convert Click parameters to typed dict
@@ -1511,25 +1531,33 @@ def run(
1511
1531
  try:
1512
1532
  exit_code = loop.run_until_complete(run_cli_async(params))
1513
1533
  sys.exit(int(exit_code))
1534
+ except SchemaValidationError as e:
1535
+ # Log the error with full context
1536
+ logger.error("Schema validation error: %s", str(e))
1537
+ if e.context:
1538
+ logger.debug(
1539
+ "Error context: %s", json.dumps(e.context, indent=2)
1540
+ )
1541
+ # Re-raise to preserve error chain and exit code
1542
+ raise
1543
+ except (CLIError, InvalidJSONError, SchemaFileError) as e:
1544
+ handle_error(e)
1545
+ sys.exit(
1546
+ e.exit_code
1547
+ if hasattr(e, "exit_code")
1548
+ else ExitCode.INTERNAL_ERROR
1549
+ )
1550
+ except click.UsageError as e:
1551
+ handle_error(e)
1552
+ sys.exit(ExitCode.USAGE_ERROR)
1553
+ except Exception as e:
1554
+ handle_error(e)
1555
+ sys.exit(ExitCode.INTERNAL_ERROR)
1514
1556
  finally:
1515
1557
  loop.close()
1516
-
1517
- except (
1518
- CLIError,
1519
- InvalidJSONError,
1520
- SchemaFileError,
1521
- SchemaValidationError,
1522
- ) as e:
1523
- handle_error(e)
1524
- sys.exit(
1525
- e.exit_code if hasattr(e, "exit_code") else ExitCode.INTERNAL_ERROR
1526
- )
1527
- except click.UsageError as e:
1528
- handle_error(e)
1529
- sys.exit(ExitCode.USAGE_ERROR)
1530
- except Exception as e:
1531
- handle_error(e)
1532
- sys.exit(ExitCode.INTERNAL_ERROR)
1558
+ except KeyboardInterrupt:
1559
+ logger.info("Operation cancelled by user")
1560
+ raise
1533
1561
 
1534
1562
 
1535
1563
  # Remove the old @create_click_command() decorator and cli function definition
@@ -1582,6 +1610,7 @@ async def validate_inputs(
1582
1610
 
1583
1611
  Raises:
1584
1612
  CLIError: For various validation errors
1613
+ SchemaValidationError: When schema is invalid
1585
1614
  """
1586
1615
  logger.debug("=== Input Validation Phase ===")
1587
1616
  security_manager = validate_security_manager(
@@ -1593,10 +1622,22 @@ async def validate_inputs(
1593
1622
  task_template = validate_task_template(
1594
1623
  args.get("task"), args.get("task_file")
1595
1624
  )
1625
+
1626
+ # Load and validate schema
1596
1627
  logger.debug("Validating schema from %s", args["schema_file"])
1597
- schema = validate_schema_file(
1598
- args["schema_file"], args.get("verbose", False)
1599
- )
1628
+ try:
1629
+ schema = validate_schema_file(
1630
+ args["schema_file"], args.get("verbose", False)
1631
+ )
1632
+
1633
+ # Validate schema structure before any model creation
1634
+ validate_json_schema(
1635
+ schema
1636
+ ) # This will raise SchemaValidationError if invalid
1637
+ except SchemaValidationError as e:
1638
+ logger.error("Schema validation error: %s", str(e))
1639
+ raise # Re-raise the SchemaValidationError to preserve the error chain
1640
+
1600
1641
  template_context = await create_template_context_from_args(
1601
1642
  args, security_manager
1602
1643
  )
@@ -1675,6 +1716,7 @@ async def validate_model_and_schema(
1675
1716
  ModelCreationError,
1676
1717
  ) as e:
1677
1718
  logger.error("Schema error: %s", str(e))
1719
+ # Pass through the error without additional wrapping
1678
1720
  raise
1679
1721
 
1680
1722
  if not supports_structured_output(args["model"]):
@@ -1820,19 +1862,21 @@ async def execute_model(
1820
1862
  async def run_cli_async(args: CLIParams) -> ExitCode:
1821
1863
  """Async wrapper for CLI operations.
1822
1864
 
1865
+ Args:
1866
+ args: CLI parameters.
1867
+
1823
1868
  Returns:
1824
- Exit code to return from the CLI
1869
+ Exit code.
1825
1870
 
1826
1871
  Raises:
1827
- CLIError: For various error conditions
1828
- KeyboardInterrupt: When operation is cancelled by user
1872
+ CLIError: For errors during CLI operations.
1829
1873
  """
1830
1874
  try:
1831
1875
  # 0. Model Parameter Validation
1832
1876
  logger.debug("=== Model Parameter Validation ===")
1833
1877
  params = await validate_model_params(args)
1834
1878
 
1835
- # 1. Input Validation Phase
1879
+ # 1. Input Validation Phase (includes schema validation)
1836
1880
  security_manager, task_template, schema, template_context, env = (
1837
1881
  await validate_inputs(args)
1838
1882
  )
@@ -1849,15 +1893,12 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
1849
1893
  )
1850
1894
  )
1851
1895
 
1852
- # 4. Dry Run Output Phase
1896
+ # 4. Dry Run Output Phase - Moved after all validations
1853
1897
  if args.get("dry_run", False):
1854
1898
  logger.info("\n=== Dry Run Summary ===")
1899
+ # Only log success if we got this far (no validation errors)
1855
1900
  logger.info("✓ Template rendered successfully")
1856
1901
  logger.info("✓ Schema validation passed")
1857
- logger.info("✓ Model compatibility validated")
1858
- logger.info(
1859
- f"✓ Token count: {total_tokens}/{registry.get_capabilities(args['model']).context_window}"
1860
- )
1861
1902
 
1862
1903
  if args.get("verbose", False):
1863
1904
  logger.info("\nSystem Prompt:")
@@ -1867,6 +1908,7 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
1867
1908
  logger.info("-" * 40)
1868
1909
  logger.info(user_prompt)
1869
1910
 
1911
+ # Return success only if we got here (no validation errors)
1870
1912
  return ExitCode.SUCCESS
1871
1913
 
1872
1914
  # 5. Execution Phase
@@ -1877,6 +1919,10 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
1877
1919
  except KeyboardInterrupt:
1878
1920
  logger.info("Operation cancelled by user")
1879
1921
  raise
1922
+ except SchemaValidationError as e:
1923
+ # Ensure schema validation errors are properly propagated with the correct exit code
1924
+ logger.error("Schema validation error: %s", str(e))
1925
+ raise # Re-raise the SchemaValidationError to preserve the error chain
1880
1926
  except Exception as e:
1881
1927
  if isinstance(e, CLIError):
1882
1928
  raise # Let our custom errors propagate
ostruct/cli/errors.py CHANGED
@@ -323,60 +323,6 @@ class SchemaFileError(CLIError):
323
323
  return self.context.get("schema_path")
324
324
 
325
325
 
326
- class SchemaValidationError(CLIError):
327
- """Error raised when a schema fails validation."""
328
-
329
- def __init__(
330
- self,
331
- message: str,
332
- context: Optional[Dict[str, Any]] = None,
333
- ):
334
- context = context or {}
335
-
336
- # Format error message with tips
337
- formatted_message = [message]
338
-
339
- if "path" in context:
340
- formatted_message.append(f"\nLocation: {context['path']}")
341
-
342
- if "found" in context:
343
- formatted_message.append(f"Found: {context['found']}")
344
-
345
- if "count" in context:
346
- formatted_message.append(f"Count: {context['count']}")
347
-
348
- if "missing_required" in context:
349
- formatted_message.append(
350
- f"Missing required: {context['missing_required']}"
351
- )
352
-
353
- if "extra_required" in context:
354
- formatted_message.append(
355
- f"Extra required: {context['extra_required']}"
356
- )
357
-
358
- if "prohibited_used" in context:
359
- formatted_message.append(
360
- f"Prohibited keywords used: {context['prohibited_used']}"
361
- )
362
-
363
- if "tips" in context:
364
- formatted_message.append("\nHow to fix:")
365
- for tip in context["tips"]:
366
- if isinstance(tip, dict):
367
- # Format JSON example
368
- formatted_message.append("Example schema:")
369
- formatted_message.append(json.dumps(tip, indent=2))
370
- else:
371
- formatted_message.append(f"- {tip}")
372
-
373
- super().__init__(
374
- "\n".join(formatted_message),
375
- context=context,
376
- exit_code=ExitCode.SCHEMA_ERROR,
377
- )
378
-
379
-
380
326
  class ModelCreationError(CLIError):
381
327
  """Base class for model creation errors."""
382
328
 
@@ -496,6 +442,67 @@ class OpenAIClientError(CLIError):
496
442
  super().__init__(message, exit_code=exit_code, context=context)
497
443
 
498
444
 
445
+ class SchemaValidationError(ModelCreationError):
446
+ """Raised when schema validation fails."""
447
+
448
+ def __init__(
449
+ self,
450
+ message: str,
451
+ context: Optional[Dict[str, Any]] = None,
452
+ exit_code: ExitCode = ExitCode.SCHEMA_ERROR,
453
+ ):
454
+ context = context or {}
455
+ # Preserve validation type for error handling
456
+ context.setdefault("validation_type", "schema")
457
+
458
+ # Format error message with tips
459
+ formatted_message = []
460
+
461
+ if "path" in context:
462
+ formatted_message.append(f"\nLocation: {context['path']}")
463
+
464
+ if "found" in context:
465
+ formatted_message.append(f"Found: {context['found']}")
466
+
467
+ if "reference" in context:
468
+ formatted_message.append(f"Reference: {context['reference']}")
469
+
470
+ if "count" in context:
471
+ formatted_message.append(f"Count: {context['count']}")
472
+
473
+ if "missing_required" in context:
474
+ formatted_message.append(
475
+ f"Missing required: {context['missing_required']}"
476
+ )
477
+
478
+ if "extra_required" in context:
479
+ formatted_message.append(
480
+ f"Extra required: {context['extra_required']}"
481
+ )
482
+
483
+ if "prohibited_used" in context:
484
+ formatted_message.append(
485
+ f"Prohibited keywords used: {context['prohibited_used']}"
486
+ )
487
+
488
+ if "tips" in context:
489
+ formatted_message.append("\nHow to fix:")
490
+ for tip in context["tips"]:
491
+ if isinstance(tip, dict):
492
+ # Format JSON example
493
+ formatted_message.append("Example schema:")
494
+ formatted_message.append(json.dumps(tip, indent=2))
495
+ else:
496
+ formatted_message.append(f"- {tip}")
497
+
498
+ # Combine message with details
499
+ final_message = message
500
+ if formatted_message:
501
+ final_message += "\n" + "\n".join(formatted_message)
502
+
503
+ super().__init__(final_message, context=context, exit_code=exit_code)
504
+
505
+
499
506
  # Export public API
500
507
  __all__ = [
501
508
  "VariableError",
@@ -40,6 +40,7 @@ from .errors import (
40
40
  NestedModelError,
41
41
  SchemaValidationError,
42
42
  )
43
+ from .exit_codes import ExitCode
43
44
 
44
45
  logger = logging.getLogger(__name__)
45
46
 
@@ -297,90 +298,26 @@ def create_dynamic_model(
297
298
  show_schema: bool = False,
298
299
  debug_validation: bool = False,
299
300
  ) -> Type[BaseModel]:
300
- """Create a Pydantic model from a JSON schema.
301
+ """Create a Pydantic model from a JSON Schema.
301
302
 
302
303
  Args:
303
- schema: JSON schema to create model from
304
- base_name: Name for the model class
305
- show_schema: Whether to show the generated model schema
306
- debug_validation: Whether to show detailed validation errors
304
+ schema: JSON Schema to create model from
305
+ base_name: Base name for the model class
306
+ show_schema: Whether to show the generated schema
307
+ debug_validation: Whether to show debug validation info
307
308
 
308
309
  Returns:
309
- Type[BaseModel]: The generated Pydantic model class
310
+ Generated Pydantic model class
310
311
 
311
312
  Raises:
312
- ModelValidationError: If the schema is invalid
313
- SchemaValidationError: If the schema violates OpenAI requirements
313
+ SchemaValidationError: If schema validation fails
314
+ ModelCreationError: If model creation fails
314
315
  """
315
- if debug_validation:
316
- logger.info("Creating dynamic model from schema:")
317
- logger.info(json.dumps(schema, indent=2))
318
-
319
316
  try:
320
- # Handle our wrapper format if present
321
- if "schema" in schema:
322
- if debug_validation:
323
- logger.info("Found schema wrapper, extracting inner schema")
324
- logger.info(
325
- "Original schema: %s", json.dumps(schema, indent=2)
326
- )
327
- inner_schema = schema["schema"]
328
- if not isinstance(inner_schema, dict):
329
- if debug_validation:
330
- logger.info(
331
- "Inner schema must be a dictionary, got %s",
332
- type(inner_schema),
333
- )
334
- raise SchemaValidationError(
335
- "Inner schema must be a dictionary"
336
- )
337
- if debug_validation:
338
- logger.info("Using inner schema:")
339
- logger.info(json.dumps(inner_schema, indent=2))
340
- schema = inner_schema
341
-
342
- # Validate against OpenAI requirements
343
- from .schema_validation import validate_openai_schema
344
-
345
- validate_openai_schema(schema)
346
-
347
- # Create model configuration
348
- config = ConfigDict(
349
- title=schema.get("title", base_name),
350
- extra="forbid", # OpenAI requires additionalProperties: false
351
- validate_default=True,
352
- use_enum_values=True,
353
- arbitrary_types_allowed=True,
354
- json_schema_extra={
355
- k: v
356
- for k, v in schema.items()
357
- if k
358
- not in {
359
- "type",
360
- "properties",
361
- "required",
362
- "title",
363
- "description",
364
- "additionalProperties",
365
- "readOnly",
366
- }
367
- },
368
- )
317
+ # Validate schema structure before model creation
318
+ from .template_utils import validate_json_schema
369
319
 
370
- if debug_validation:
371
- logger.info("Created model configuration:")
372
- logger.info(" Title: %s", config.get("title"))
373
- logger.info(" Extra: %s", config.get("extra"))
374
- logger.info(
375
- " Validate Default: %s", config.get("validate_default")
376
- )
377
- logger.info(" Use Enum Values: %s", config.get("use_enum_values"))
378
- logger.info(
379
- " Arbitrary Types: %s", config.get("arbitrary_types_allowed")
380
- )
381
- logger.info(
382
- " JSON Schema Extra: %s", config.get("json_schema_extra")
383
- )
320
+ validate_json_schema(schema)
384
321
 
385
322
  # Process schema properties into fields
386
323
  properties = schema.get("properties", {})
@@ -438,23 +375,25 @@ def create_dynamic_model(
438
375
  )
439
376
  for name, (field_type, field) in field_definitions.items()
440
377
  }
441
- model: Type[BaseModel] = create_model(
442
- base_name, __config__=config, **field_defs
443
- )
444
378
 
445
- # Set the model config after creation
446
- model.model_config = config
379
+ # Create model class
380
+ model = create_model(base_name, __base__=BaseModel, **field_defs)
447
381
 
448
- if debug_validation:
449
- logger.info("Successfully created model: %s", model.__name__)
450
- logger.info("Model config: %s", dict(model.model_config))
382
+ # Set model config
383
+ model.model_config = ConfigDict(
384
+ title=schema.get("title", base_name),
385
+ extra="forbid",
386
+ )
387
+
388
+ if show_schema:
451
389
  logger.info(
452
- "Model schema: %s",
390
+ "Generated schema for %s:\n%s",
391
+ base_name,
453
392
  json.dumps(model.model_json_schema(), indent=2),
454
393
  )
455
394
 
456
- # Validate the model's JSON schema
457
395
  try:
396
+ # Validate model schema
458
397
  model.model_json_schema()
459
398
  except ValidationError as e:
460
399
  validation_errors = (
@@ -467,18 +406,52 @@ def create_dynamic_model(
467
406
  logger.error(" Error type: %s", type(e).__name__)
468
407
  logger.error(" Error message: %s", str(e))
469
408
  raise ModelValidationError(base_name, validation_errors)
409
+ except KeyError as e:
410
+ # Handle Pydantic schema generation errors, particularly for recursive references
411
+ error_msg = str(e).strip(
412
+ "'\""
413
+ ) # Strip quotes from KeyError message
414
+ if error_msg.startswith("#/definitions/"):
415
+ context = {
416
+ "schema_path": schema.get("$id", "unknown"),
417
+ "reference": error_msg,
418
+ "found": "circular reference or missing definition",
419
+ "tips": [
420
+ "Add explicit $ref definitions for recursive structures",
421
+ "Use Pydantic's deferred annotations with typing.Self",
422
+ "Limit recursion depth with max_depth validator",
423
+ "Flatten nested structures using reference IDs",
424
+ ],
425
+ }
470
426
 
471
- return model
427
+ error_msg = (
428
+ f"Invalid schema reference: {error_msg}\n"
429
+ "Detected circular reference or missing definition.\n"
430
+ "Solutions:\n"
431
+ "1. Add missing $ref definitions to your schema\n"
432
+ "2. Use explicit ID references instead of nested objects\n"
433
+ "3. Implement depth limits for recursive structures"
434
+ )
472
435
 
473
- except SchemaValidationError as e:
474
- # Always log basic error info
475
- logger.error("Schema validation error: %s", str(e))
436
+ if debug_validation:
437
+ logger.error("Schema reference error:")
438
+ logger.error(" Error type: %s", type(e).__name__)
439
+ logger.error(" Error message: %s", error_msg)
476
440
 
477
- # Log additional debug info if requested
478
- if debug_validation:
479
- logger.error(" Error type: %s", type(e).__name__)
480
- logger.error(" Error details: %s", str(e))
481
- # Always raise schema validation errors directly
441
+ raise SchemaValidationError(
442
+ error_msg, context=context, exit_code=ExitCode.SCHEMA_ERROR
443
+ ) from e
444
+
445
+ # For other KeyErrors, preserve the original error
446
+ raise ModelCreationError(
447
+ f"Failed to create model {base_name}",
448
+ context={"error": str(e)},
449
+ ) from e
450
+
451
+ return model
452
+
453
+ except SchemaValidationError:
454
+ # Re-raise schema validation errors without wrapping
482
455
  raise
483
456
 
484
457
  except Exception as e:
@@ -63,7 +63,7 @@ class PathSecurityError(SecurityErrorBase):
63
63
  @property
64
64
  def details(self) -> str:
65
65
  """Get the detailed explanation of the error."""
66
- return self.details
66
+ return str(self.context.get("details", ""))
67
67
 
68
68
  @classmethod
69
69
  def from_expanded_paths(
@@ -61,7 +61,7 @@ from .errors import PathSecurityError, SecurityErrorReasons
61
61
  # Patterns for path normalization and validation
62
62
  _UNICODE_SAFETY_PATTERN = re.compile(
63
63
  r"[\u0000-\u001F\u007F-\u009F\u2028-\u2029\u0085]" # Control chars and line separators
64
- r"|\.{2,}" # Directory traversal attempts
64
+ r"|(?:^|/)\.\.(?:/|$)" # Directory traversal attempts (only ".." as a path component)
65
65
  r"|[\u2024\u2025\uFE52\u2024\u2025\u2026\uFE19\uFE30\uFE52\uFF0E\uFF61]" # Alternative dots and separators
66
66
  )
67
67
  _BACKSLASH_PATTERN = re.compile(r"\\")