ostruct-cli 0.6.1__py3-none-any.whl → 0.7.0__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/__init__.py +2 -0
- ostruct/cli/cli.py +200 -73
- ostruct/cli/errors.py +61 -54
- ostruct/cli/model_creation.py +67 -94
- ostruct/cli/registry_updates.py +162 -0
- ostruct/cli/security/errors.py +1 -1
- ostruct/cli/security/normalization.py +1 -1
- ostruct/cli/security/security_manager.py +48 -7
- ostruct/cli/template_extensions.py +32 -1
- ostruct/cli/template_utils.py +175 -16
- ostruct/cli/utils.py +3 -1
- ostruct/cli/validators.py +6 -2
- {ostruct_cli-0.6.1.dist-info → ostruct_cli-0.7.0.dist-info}/METADATA +71 -165
- {ostruct_cli-0.6.1.dist-info → ostruct_cli-0.7.0.dist-info}/RECORD +17 -16
- {ostruct_cli-0.6.1.dist-info → ostruct_cli-0.7.0.dist-info}/LICENSE +0 -0
- {ostruct_cli-0.6.1.dist-info → ostruct_cli-0.7.0.dist-info}/WHEEL +0 -0
- {ostruct_cli-0.6.1.dist-info → ostruct_cli-0.7.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/__init__.py
CHANGED
@@ -8,6 +8,7 @@ from .cli import (
|
|
8
8
|
validate_variable_mapping,
|
9
9
|
)
|
10
10
|
from .path_utils import validate_path_mapping
|
11
|
+
from .registry_updates import get_update_notification
|
11
12
|
|
12
13
|
__all__ = [
|
13
14
|
"ExitCode",
|
@@ -16,4 +17,5 @@ __all__ = [
|
|
16
17
|
"validate_schema_file",
|
17
18
|
"validate_task_template",
|
18
19
|
"validate_variable_mapping",
|
20
|
+
"get_update_notification",
|
19
21
|
]
|
ostruct/cli/cli.py
CHANGED
@@ -45,7 +45,10 @@ from openai_structured.errors import (
|
|
45
45
|
OpenAIClientError,
|
46
46
|
StreamBufferError,
|
47
47
|
)
|
48
|
-
from openai_structured.model_registry import
|
48
|
+
from openai_structured.model_registry import (
|
49
|
+
ModelRegistry,
|
50
|
+
RegistryUpdateStatus,
|
51
|
+
)
|
49
52
|
from pydantic import AnyUrl, BaseModel, EmailStr, Field
|
50
53
|
from pydantic.fields import FieldInfo as FieldInfoType
|
51
54
|
from pydantic.functional_validators import BeforeValidator
|
@@ -75,10 +78,15 @@ from .errors import (
|
|
75
78
|
from .file_utils import FileInfoList, collect_files
|
76
79
|
from .model_creation import _create_enum_type, create_dynamic_model
|
77
80
|
from .path_utils import validate_path_mapping
|
81
|
+
from .registry_updates import get_update_notification
|
78
82
|
from .security import SecurityManager
|
79
83
|
from .serialization import LogSerializer
|
80
84
|
from .template_env import create_jinja_env
|
81
|
-
from .template_utils import
|
85
|
+
from .template_utils import (
|
86
|
+
SystemPromptError,
|
87
|
+
render_template,
|
88
|
+
validate_json_schema,
|
89
|
+
)
|
82
90
|
from .token_utils import estimate_tokens_with_encoding
|
83
91
|
|
84
92
|
# Constants
|
@@ -831,7 +839,7 @@ def validate_schema_file(
|
|
831
839
|
logger.error(msg)
|
832
840
|
raise SchemaFileError(msg, schema_path=path)
|
833
841
|
except Exception as e:
|
834
|
-
if isinstance(e, InvalidJSONError):
|
842
|
+
if isinstance(e, (InvalidJSONError, SchemaValidationError)):
|
835
843
|
raise
|
836
844
|
msg = f"Failed to read schema file {path}: {e}"
|
837
845
|
logger.error(msg)
|
@@ -846,7 +854,13 @@ def validate_schema_file(
|
|
846
854
|
if not isinstance(schema, dict):
|
847
855
|
msg = f"Schema in {path} must be a JSON object"
|
848
856
|
logger.error(msg)
|
849
|
-
raise SchemaValidationError(
|
857
|
+
raise SchemaValidationError(
|
858
|
+
msg,
|
859
|
+
context={
|
860
|
+
"validation_type": "schema",
|
861
|
+
"schema_path": path,
|
862
|
+
},
|
863
|
+
)
|
850
864
|
|
851
865
|
# Validate schema structure
|
852
866
|
if "schema" in schema:
|
@@ -856,7 +870,13 @@ def validate_schema_file(
|
|
856
870
|
if not isinstance(inner_schema, dict):
|
857
871
|
msg = f"Inner schema in {path} must be a JSON object"
|
858
872
|
logger.error(msg)
|
859
|
-
raise SchemaValidationError(
|
873
|
+
raise SchemaValidationError(
|
874
|
+
msg,
|
875
|
+
context={
|
876
|
+
"validation_type": "schema",
|
877
|
+
"schema_path": path,
|
878
|
+
},
|
879
|
+
)
|
860
880
|
if verbose:
|
861
881
|
logger.debug("Inner schema validated successfully")
|
862
882
|
logger.debug(
|
@@ -871,7 +891,20 @@ def validate_schema_file(
|
|
871
891
|
if "type" not in schema.get("schema", schema):
|
872
892
|
msg = f"Schema in {path} must specify a type"
|
873
893
|
logger.error(msg)
|
874
|
-
raise SchemaValidationError(
|
894
|
+
raise SchemaValidationError(
|
895
|
+
msg,
|
896
|
+
context={
|
897
|
+
"validation_type": "schema",
|
898
|
+
"schema_path": path,
|
899
|
+
},
|
900
|
+
)
|
901
|
+
|
902
|
+
# Validate schema against JSON Schema spec
|
903
|
+
try:
|
904
|
+
validate_json_schema(schema)
|
905
|
+
except SchemaValidationError as e:
|
906
|
+
logger.error("Schema validation error: %s", str(e))
|
907
|
+
raise # Re-raise to preserve error chain
|
875
908
|
|
876
909
|
# Return the full schema including wrapper
|
877
910
|
return schema
|
@@ -1225,19 +1258,24 @@ def handle_error(e: Exception) -> None:
|
|
1225
1258
|
Provides enhanced debug logging for CLI errors.
|
1226
1259
|
"""
|
1227
1260
|
# 1. Determine error type and message
|
1228
|
-
if isinstance(e,
|
1261
|
+
if isinstance(e, SchemaValidationError):
|
1262
|
+
msg = str(e) # Already formatted in SchemaValidationError
|
1263
|
+
exit_code = e.exit_code
|
1264
|
+
elif isinstance(e, ModelCreationError):
|
1265
|
+
# Unwrap ModelCreationError that might wrap SchemaValidationError
|
1266
|
+
if isinstance(e.__cause__, SchemaValidationError):
|
1267
|
+
return handle_error(e.__cause__)
|
1268
|
+
msg = f"Model creation error: {str(e)}"
|
1269
|
+
exit_code = ExitCode.SCHEMA_ERROR
|
1270
|
+
elif isinstance(e, click.UsageError):
|
1229
1271
|
msg = f"Usage error: {str(e)}"
|
1230
1272
|
exit_code = ExitCode.USAGE_ERROR
|
1231
1273
|
elif isinstance(e, SchemaFileError):
|
1232
|
-
# Preserve specific schema error handling
|
1233
1274
|
msg = str(e) # Use existing __str__ formatting
|
1234
1275
|
exit_code = ExitCode.SCHEMA_ERROR
|
1235
1276
|
elif isinstance(e, (InvalidJSONError, json.JSONDecodeError)):
|
1236
1277
|
msg = f"Invalid JSON error: {str(e)}"
|
1237
1278
|
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
1279
|
elif isinstance(e, CLIError):
|
1242
1280
|
msg = str(e) # Use existing __str__ formatting
|
1243
1281
|
exit_code = ExitCode(e.exit_code) # Convert int to ExitCode
|
@@ -1249,7 +1287,7 @@ def handle_error(e: Exception) -> None:
|
|
1249
1287
|
if isinstance(e, CLIError) and logger.isEnabledFor(logging.DEBUG):
|
1250
1288
|
# Format context fields with lowercase keys and simple values
|
1251
1289
|
context_str = ""
|
1252
|
-
if hasattr(e, "context"):
|
1290
|
+
if hasattr(e, "context") and e.context:
|
1253
1291
|
for key, value in sorted(e.context.items()):
|
1254
1292
|
if key not in {
|
1255
1293
|
"timestamp",
|
@@ -1257,13 +1295,18 @@ def handle_error(e: Exception) -> None:
|
|
1257
1295
|
"version",
|
1258
1296
|
"python_version",
|
1259
1297
|
}:
|
1260
|
-
|
1298
|
+
if isinstance(value, dict):
|
1299
|
+
context_str += (
|
1300
|
+
f"{key.lower()}:\n{json.dumps(value, indent=2)}\n"
|
1301
|
+
)
|
1302
|
+
else:
|
1303
|
+
context_str += f"{key.lower()}: {value}\n"
|
1261
1304
|
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1305
|
+
logger.debug(
|
1306
|
+
"Error details:\n"
|
1307
|
+
f"Type: {type(e).__name__}\n"
|
1308
|
+
f"{context_str.rstrip()}"
|
1309
|
+
)
|
1267
1310
|
elif not isinstance(e, click.UsageError):
|
1268
1311
|
logger.error(msg, exc_info=True)
|
1269
1312
|
else:
|
@@ -1451,7 +1494,14 @@ def cli() -> None:
|
|
1451
1494
|
|
1452
1495
|
ostruct run task.j2 schema.json -J config='{"env":"prod"}' -m o3-mini
|
1453
1496
|
"""
|
1454
|
-
|
1497
|
+
# Check for registry updates in a non-intrusive way
|
1498
|
+
try:
|
1499
|
+
update_message = get_update_notification()
|
1500
|
+
if update_message:
|
1501
|
+
click.secho(f"Note: {update_message}", fg="blue", err=True)
|
1502
|
+
except Exception:
|
1503
|
+
# Ensure any errors don't affect normal operation
|
1504
|
+
pass
|
1455
1505
|
|
1456
1506
|
|
1457
1507
|
@cli.command()
|
@@ -1467,30 +1517,11 @@ def run(
|
|
1467
1517
|
) -> None:
|
1468
1518
|
"""Run a structured task with template and schema.
|
1469
1519
|
|
1470
|
-
|
1471
|
-
|
1472
|
-
|
1473
|
-
|
1474
|
-
|
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
|
1520
|
+
Args:
|
1521
|
+
ctx: Click context
|
1522
|
+
task_template: Path to task template file
|
1523
|
+
schema_file: Path to schema file
|
1524
|
+
**kwargs: Additional CLI options
|
1494
1525
|
"""
|
1495
1526
|
try:
|
1496
1527
|
# Convert Click parameters to typed dict
|
@@ -1511,29 +1542,107 @@ def run(
|
|
1511
1542
|
try:
|
1512
1543
|
exit_code = loop.run_until_complete(run_cli_async(params))
|
1513
1544
|
sys.exit(int(exit_code))
|
1545
|
+
except SchemaValidationError as e:
|
1546
|
+
# Log the error with full context
|
1547
|
+
logger.error("Schema validation error: %s", str(e))
|
1548
|
+
if e.context:
|
1549
|
+
logger.debug(
|
1550
|
+
"Error context: %s", json.dumps(e.context, indent=2)
|
1551
|
+
)
|
1552
|
+
# Re-raise to preserve error chain and exit code
|
1553
|
+
raise
|
1554
|
+
except (CLIError, InvalidJSONError, SchemaFileError) as e:
|
1555
|
+
handle_error(e)
|
1556
|
+
sys.exit(
|
1557
|
+
e.exit_code
|
1558
|
+
if hasattr(e, "exit_code")
|
1559
|
+
else ExitCode.INTERNAL_ERROR
|
1560
|
+
)
|
1561
|
+
except click.UsageError as e:
|
1562
|
+
handle_error(e)
|
1563
|
+
sys.exit(ExitCode.USAGE_ERROR)
|
1564
|
+
except Exception as e:
|
1565
|
+
handle_error(e)
|
1566
|
+
sys.exit(ExitCode.INTERNAL_ERROR)
|
1514
1567
|
finally:
|
1515
1568
|
loop.close()
|
1569
|
+
except KeyboardInterrupt:
|
1570
|
+
logger.info("Operation cancelled by user")
|
1571
|
+
raise
|
1516
1572
|
|
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)
|
1533
1573
|
|
1574
|
+
@cli.command("update-registry")
|
1575
|
+
@click.option(
|
1576
|
+
"--url",
|
1577
|
+
help="URL to fetch the registry from. Defaults to official repository.",
|
1578
|
+
default=None,
|
1579
|
+
)
|
1580
|
+
@click.option(
|
1581
|
+
"--force",
|
1582
|
+
is_flag=True,
|
1583
|
+
help="Force update even if the registry is already up to date.",
|
1584
|
+
default=False,
|
1585
|
+
)
|
1586
|
+
def update_registry(url: Optional[str] = None, force: bool = False) -> None:
|
1587
|
+
"""Update the model registry with the latest model definitions.
|
1588
|
+
|
1589
|
+
This command fetches the latest model registry from the official repository
|
1590
|
+
or a custom URL if provided, and updates the local registry file.
|
1591
|
+
|
1592
|
+
Example:
|
1593
|
+
ostruct update-registry
|
1594
|
+
ostruct update-registry --url https://example.com/models.yml
|
1595
|
+
"""
|
1596
|
+
try:
|
1597
|
+
registry = ModelRegistry()
|
1598
|
+
|
1599
|
+
# Show current registry config path
|
1600
|
+
config_path = registry._config_path
|
1601
|
+
click.echo(f"Current registry file: {config_path}")
|
1602
|
+
|
1603
|
+
if force:
|
1604
|
+
click.echo("Forcing registry update...")
|
1605
|
+
success = registry.refresh_from_remote(url)
|
1606
|
+
if success:
|
1607
|
+
click.echo("✅ Registry successfully updated!")
|
1608
|
+
else:
|
1609
|
+
click.echo(
|
1610
|
+
"❌ Failed to update registry. See logs for details."
|
1611
|
+
)
|
1612
|
+
sys.exit(ExitCode.SUCCESS.value)
|
1613
|
+
|
1614
|
+
if config_path is None or not os.path.exists(config_path):
|
1615
|
+
click.echo("Registry file not found. Creating new one...")
|
1616
|
+
success = registry.refresh_from_remote(url)
|
1617
|
+
if success:
|
1618
|
+
click.echo("✅ Registry successfully created!")
|
1619
|
+
else:
|
1620
|
+
click.echo(
|
1621
|
+
"❌ Failed to create registry. See logs for details."
|
1622
|
+
)
|
1623
|
+
sys.exit(ExitCode.SUCCESS.value)
|
1624
|
+
|
1625
|
+
# Use the built-in update checking functionality
|
1626
|
+
click.echo("Checking for updates...")
|
1627
|
+
update_result = registry.check_for_updates()
|
1534
1628
|
|
1535
|
-
|
1536
|
-
|
1629
|
+
if update_result.status == RegistryUpdateStatus.UPDATE_AVAILABLE:
|
1630
|
+
click.echo(
|
1631
|
+
f"{click.style('✓', fg='green')} {update_result.message}"
|
1632
|
+
)
|
1633
|
+
exit_code = ExitCode.SUCCESS
|
1634
|
+
elif update_result.status == RegistryUpdateStatus.ALREADY_CURRENT:
|
1635
|
+
click.echo(
|
1636
|
+
f"{click.style('✓', fg='green')} Registry is up to date"
|
1637
|
+
)
|
1638
|
+
exit_code = ExitCode.SUCCESS
|
1639
|
+
else:
|
1640
|
+
click.echo("❓ Unable to determine if updates are available.")
|
1641
|
+
|
1642
|
+
sys.exit(exit_code)
|
1643
|
+
except Exception as e:
|
1644
|
+
click.echo(f"❌ Error updating registry: {str(e)}")
|
1645
|
+
sys.exit(ExitCode.API_ERROR.value)
|
1537
1646
|
|
1538
1647
|
|
1539
1648
|
async def validate_model_params(args: CLIParams) -> Dict[str, Any]:
|
@@ -1582,6 +1691,7 @@ async def validate_inputs(
|
|
1582
1691
|
|
1583
1692
|
Raises:
|
1584
1693
|
CLIError: For various validation errors
|
1694
|
+
SchemaValidationError: When schema is invalid
|
1585
1695
|
"""
|
1586
1696
|
logger.debug("=== Input Validation Phase ===")
|
1587
1697
|
security_manager = validate_security_manager(
|
@@ -1593,10 +1703,22 @@ async def validate_inputs(
|
|
1593
1703
|
task_template = validate_task_template(
|
1594
1704
|
args.get("task"), args.get("task_file")
|
1595
1705
|
)
|
1706
|
+
|
1707
|
+
# Load and validate schema
|
1596
1708
|
logger.debug("Validating schema from %s", args["schema_file"])
|
1597
|
-
|
1598
|
-
|
1599
|
-
|
1709
|
+
try:
|
1710
|
+
schema = validate_schema_file(
|
1711
|
+
args["schema_file"], args.get("verbose", False)
|
1712
|
+
)
|
1713
|
+
|
1714
|
+
# Validate schema structure before any model creation
|
1715
|
+
validate_json_schema(
|
1716
|
+
schema
|
1717
|
+
) # This will raise SchemaValidationError if invalid
|
1718
|
+
except SchemaValidationError as e:
|
1719
|
+
logger.error("Schema validation error: %s", str(e))
|
1720
|
+
raise # Re-raise the SchemaValidationError to preserve the error chain
|
1721
|
+
|
1600
1722
|
template_context = await create_template_context_from_args(
|
1601
1723
|
args, security_manager
|
1602
1724
|
)
|
@@ -1675,6 +1797,7 @@ async def validate_model_and_schema(
|
|
1675
1797
|
ModelCreationError,
|
1676
1798
|
) as e:
|
1677
1799
|
logger.error("Schema error: %s", str(e))
|
1800
|
+
# Pass through the error without additional wrapping
|
1678
1801
|
raise
|
1679
1802
|
|
1680
1803
|
if not supports_structured_output(args["model"]):
|
@@ -1820,19 +1943,21 @@ async def execute_model(
|
|
1820
1943
|
async def run_cli_async(args: CLIParams) -> ExitCode:
|
1821
1944
|
"""Async wrapper for CLI operations.
|
1822
1945
|
|
1946
|
+
Args:
|
1947
|
+
args: CLI parameters.
|
1948
|
+
|
1823
1949
|
Returns:
|
1824
|
-
Exit code
|
1950
|
+
Exit code.
|
1825
1951
|
|
1826
1952
|
Raises:
|
1827
|
-
CLIError: For
|
1828
|
-
KeyboardInterrupt: When operation is cancelled by user
|
1953
|
+
CLIError: For errors during CLI operations.
|
1829
1954
|
"""
|
1830
1955
|
try:
|
1831
1956
|
# 0. Model Parameter Validation
|
1832
1957
|
logger.debug("=== Model Parameter Validation ===")
|
1833
1958
|
params = await validate_model_params(args)
|
1834
1959
|
|
1835
|
-
# 1. Input Validation Phase
|
1960
|
+
# 1. Input Validation Phase (includes schema validation)
|
1836
1961
|
security_manager, task_template, schema, template_context, env = (
|
1837
1962
|
await validate_inputs(args)
|
1838
1963
|
)
|
@@ -1849,15 +1974,12 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
|
|
1849
1974
|
)
|
1850
1975
|
)
|
1851
1976
|
|
1852
|
-
# 4. Dry Run Output Phase
|
1977
|
+
# 4. Dry Run Output Phase - Moved after all validations
|
1853
1978
|
if args.get("dry_run", False):
|
1854
1979
|
logger.info("\n=== Dry Run Summary ===")
|
1980
|
+
# Only log success if we got this far (no validation errors)
|
1855
1981
|
logger.info("✓ Template rendered successfully")
|
1856
1982
|
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
1983
|
|
1862
1984
|
if args.get("verbose", False):
|
1863
1985
|
logger.info("\nSystem Prompt:")
|
@@ -1867,6 +1989,7 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
|
|
1867
1989
|
logger.info("-" * 40)
|
1868
1990
|
logger.info(user_prompt)
|
1869
1991
|
|
1992
|
+
# Return success only if we got here (no validation errors)
|
1870
1993
|
return ExitCode.SUCCESS
|
1871
1994
|
|
1872
1995
|
# 5. Execution Phase
|
@@ -1877,6 +2000,10 @@ async def run_cli_async(args: CLIParams) -> ExitCode:
|
|
1877
2000
|
except KeyboardInterrupt:
|
1878
2001
|
logger.info("Operation cancelled by user")
|
1879
2002
|
raise
|
2003
|
+
except SchemaValidationError as e:
|
2004
|
+
# Ensure schema validation errors are properly propagated with the correct exit code
|
2005
|
+
logger.error("Schema validation error: %s", str(e))
|
2006
|
+
raise # Re-raise the SchemaValidationError to preserve the error chain
|
1880
2007
|
except Exception as e:
|
1881
2008
|
if isinstance(e, CLIError):
|
1882
2009
|
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",
|