ostruct-cli 0.7.2__py3-none-any.whl → 0.8.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.
Files changed (46) hide show
  1. ostruct/cli/__init__.py +21 -3
  2. ostruct/cli/base_errors.py +1 -1
  3. ostruct/cli/cli.py +66 -1983
  4. ostruct/cli/click_options.py +460 -28
  5. ostruct/cli/code_interpreter.py +238 -0
  6. ostruct/cli/commands/__init__.py +32 -0
  7. ostruct/cli/commands/list_models.py +128 -0
  8. ostruct/cli/commands/quick_ref.py +50 -0
  9. ostruct/cli/commands/run.py +137 -0
  10. ostruct/cli/commands/update_registry.py +71 -0
  11. ostruct/cli/config.py +277 -0
  12. ostruct/cli/cost_estimation.py +134 -0
  13. ostruct/cli/errors.py +310 -6
  14. ostruct/cli/exit_codes.py +1 -0
  15. ostruct/cli/explicit_file_processor.py +548 -0
  16. ostruct/cli/field_utils.py +69 -0
  17. ostruct/cli/file_info.py +42 -9
  18. ostruct/cli/file_list.py +301 -102
  19. ostruct/cli/file_search.py +455 -0
  20. ostruct/cli/file_utils.py +47 -13
  21. ostruct/cli/mcp_integration.py +541 -0
  22. ostruct/cli/model_creation.py +150 -1
  23. ostruct/cli/model_validation.py +204 -0
  24. ostruct/cli/progress_reporting.py +398 -0
  25. ostruct/cli/registry_updates.py +14 -9
  26. ostruct/cli/runner.py +1418 -0
  27. ostruct/cli/schema_utils.py +113 -0
  28. ostruct/cli/services.py +626 -0
  29. ostruct/cli/template_debug.py +748 -0
  30. ostruct/cli/template_debug_help.py +162 -0
  31. ostruct/cli/template_env.py +15 -6
  32. ostruct/cli/template_filters.py +55 -3
  33. ostruct/cli/template_optimizer.py +474 -0
  34. ostruct/cli/template_processor.py +1080 -0
  35. ostruct/cli/template_rendering.py +69 -34
  36. ostruct/cli/token_validation.py +286 -0
  37. ostruct/cli/types.py +78 -0
  38. ostruct/cli/unattended_operation.py +269 -0
  39. ostruct/cli/validators.py +386 -3
  40. {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/LICENSE +2 -0
  41. ostruct_cli-0.8.0.dist-info/METADATA +633 -0
  42. ostruct_cli-0.8.0.dist-info/RECORD +69 -0
  43. {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/WHEEL +1 -1
  44. ostruct_cli-0.7.2.dist-info/METADATA +0 -370
  45. ostruct_cli-0.7.2.dist-info/RECORD +0 -45
  46. {ostruct_cli-0.7.2.dist-info → ostruct_cli-0.8.0.dist-info}/entry_points.txt +0 -0
ostruct/cli/errors.py CHANGED
@@ -4,6 +4,8 @@ import json
4
4
  import logging
5
5
  from typing import Any, Dict, List, Optional
6
6
 
7
+ from openai import OpenAIError
8
+
7
9
  from .base_errors import CLIError, OstructFileNotFoundError
8
10
  from .exit_codes import ExitCode
9
11
  from .security.base import SecurityErrorBase
@@ -30,6 +32,27 @@ class VariableValueError(VariableError):
30
32
  pass
31
33
 
32
34
 
35
+ class DuplicateFileMappingError(VariableError):
36
+ """Raised when duplicate file mappings are detected."""
37
+
38
+ def __init__(
39
+ self,
40
+ message: str,
41
+ context: Optional[Dict[str, Any]] = None,
42
+ ) -> None:
43
+ """Initialize error.
44
+
45
+ Args:
46
+ message: Error message
47
+ context: Additional error context
48
+ """
49
+ super().__init__(
50
+ message,
51
+ context=context,
52
+ exit_code=ExitCode.USAGE_ERROR,
53
+ )
54
+
55
+
33
56
  class InvalidJSONError(CLIError):
34
57
  """Error raised when JSON is invalid."""
35
58
 
@@ -426,12 +449,89 @@ class InvalidResponseFormatError(CLIError):
426
449
  )
427
450
 
428
451
 
429
- class OpenAIClientError(CLIError):
430
- """Exception raised when there's an error with the OpenAI client.
452
+ # Tool-specific error classes (T3.1)
431
453
 
432
- This is a wrapper around openai_structured's OpenAIClientError to maintain
433
- compatibility with our CLI error handling.
434
- """
454
+
455
+ class FileSearchError(CLIError):
456
+ """File Search tool failures with retry guidance."""
457
+
458
+ def __init__(
459
+ self,
460
+ message: str,
461
+ exit_code: ExitCode = ExitCode.API_ERROR,
462
+ context: Optional[Dict[str, Any]] = None,
463
+ ):
464
+ super().__init__(message, exit_code=exit_code, context=context)
465
+
466
+
467
+ class FileSearchUploadError(FileSearchError):
468
+ """File upload to vector store failed."""
469
+
470
+ pass
471
+
472
+
473
+ class MCPConnectionError(CLIError):
474
+ """MCP server connection failures."""
475
+
476
+ def __init__(
477
+ self,
478
+ message: str,
479
+ exit_code: ExitCode = ExitCode.API_ERROR,
480
+ context: Optional[Dict[str, Any]] = None,
481
+ ):
482
+ super().__init__(message, exit_code=exit_code, context=context)
483
+
484
+
485
+ class ContainerExpiredError(CLIError):
486
+ """Code Interpreter container expired (20-minute limit)."""
487
+
488
+ def __init__(
489
+ self,
490
+ message: str,
491
+ exit_code: ExitCode = ExitCode.API_ERROR,
492
+ context: Optional[Dict[str, Any]] = None,
493
+ ):
494
+ super().__init__(message, exit_code=exit_code, context=context)
495
+
496
+
497
+ class UnattendedOperationTimeoutError(CLIError):
498
+ """Operation timed out during unattended execution."""
499
+
500
+ def __init__(
501
+ self,
502
+ message: str,
503
+ exit_code: ExitCode = ExitCode.OPERATION_TIMEOUT,
504
+ context: Optional[Dict[str, Any]] = None,
505
+ ):
506
+ super().__init__(message, exit_code=exit_code, context=context)
507
+
508
+
509
+ class PromptTooLargeError(CLIError):
510
+ """Prompt exceeds context window limits."""
511
+
512
+ def __init__(
513
+ self,
514
+ message: str,
515
+ exit_code: ExitCode = ExitCode.VALIDATION_ERROR,
516
+ context: Optional[Dict[str, Any]] = None,
517
+ ):
518
+ super().__init__(message, exit_code=exit_code, context=context)
519
+
520
+
521
+ class AuthenticationError(CLIError):
522
+ """API authentication failures."""
523
+
524
+ def __init__(
525
+ self,
526
+ message: str,
527
+ exit_code: ExitCode = ExitCode.API_ERROR,
528
+ context: Optional[Dict[str, Any]] = None,
529
+ ):
530
+ super().__init__(message, exit_code=exit_code, context=context)
531
+
532
+
533
+ class RateLimitError(CLIError):
534
+ """API rate limiting errors."""
435
535
 
436
536
  def __init__(
437
537
  self,
@@ -442,6 +542,133 @@ class OpenAIClientError(CLIError):
442
542
  super().__init__(message, exit_code=exit_code, context=context)
443
543
 
444
544
 
545
+ class APIError(CLIError):
546
+ """Generic API errors."""
547
+
548
+ def __init__(
549
+ self,
550
+ message: str,
551
+ exit_code: ExitCode = ExitCode.API_ERROR,
552
+ context: Optional[Dict[str, Any]] = None,
553
+ ):
554
+ super().__init__(message, exit_code=exit_code, context=context)
555
+
556
+
557
+ # API Error Mapping (T3.1)
558
+
559
+
560
+ class APIErrorMapper:
561
+ """Maps OpenAI SDK errors to ostruct-specific errors with actionable guidance."""
562
+
563
+ @staticmethod
564
+ def map_openai_error(error: OpenAIError) -> CLIError:
565
+ """Map OpenAI SDK errors to ostruct errors (validated patterns).
566
+
567
+ Args:
568
+ error: OpenAI SDK error to map
569
+
570
+ Returns:
571
+ Appropriate ostruct error with actionable guidance
572
+ """
573
+ error_msg = str(error).lower()
574
+
575
+ # Context window errors (confirmed pattern)
576
+ if (
577
+ "context_length_exceeded" in error_msg
578
+ or "maximum context length" in error_msg
579
+ ):
580
+ return PromptTooLargeError(
581
+ f"Prompt exceeds model context window (128,000 token limit). "
582
+ f"Tip: Use explicit file routing (-fc for code, -fs for docs, -ft for config). "
583
+ f"Original error: {error}"
584
+ )
585
+
586
+ # Authentication errors (confirmed pattern)
587
+ if "invalid_api_key" in error_msg or "incorrect api key" in error_msg:
588
+ return AuthenticationError(
589
+ f"Invalid OpenAI API key. Please check your OPENAI_API_KEY environment variable. "
590
+ f"Original error: {error}"
591
+ )
592
+
593
+ # Rate limiting (standard pattern)
594
+ if "rate_limit" in error_msg:
595
+ return RateLimitError(
596
+ f"OpenAI API rate limit exceeded. Please wait and try again. "
597
+ f"Original error: {error}"
598
+ )
599
+
600
+ # Schema validation errors (Responses API specific)
601
+ if "invalid schema for response_format" in error_msg:
602
+ return SchemaValidationError(
603
+ f"Schema validation failed for Responses API. "
604
+ f"Ensure your schema is compatible with strict mode. "
605
+ f"Original error: {error}"
606
+ )
607
+
608
+ # Container expiration errors (Code Interpreter specific)
609
+ if "container" in error_msg and (
610
+ "expired" in error_msg or "timeout" in error_msg
611
+ ):
612
+ return ContainerExpiredError(
613
+ f"Code Interpreter container expired (20-minute runtime limit, 2-minute idle timeout). "
614
+ f"Please retry your request. Original error: {error}"
615
+ )
616
+
617
+ # File Search errors
618
+ if "vector_store" in error_msg or "file_search" in error_msg:
619
+ return FileSearchError(
620
+ f"File Search operation failed. This can be intermittent - consider retrying. "
621
+ f"Original error: {error}"
622
+ )
623
+
624
+ # MCP connection errors
625
+ if "mcp" in error_msg or "model context protocol" in error_msg:
626
+ return MCPConnectionError(
627
+ f"MCP server connection failed. Check server URL and network connectivity. "
628
+ f"Original error: {error}"
629
+ )
630
+
631
+ # Generic API error
632
+ return APIError(f"OpenAI API error: {error}")
633
+
634
+ @staticmethod
635
+ def map_tool_error(tool_name: str, error: Exception) -> CLIError:
636
+ """Map tool-specific errors to ostruct errors.
637
+
638
+ Args:
639
+ tool_name: Name of the tool that failed
640
+ error: The original error
641
+
642
+ Returns:
643
+ Appropriate ostruct error with tool-specific guidance
644
+ """
645
+ error_msg = str(error).lower()
646
+
647
+ if tool_name == "file-search":
648
+ if "upload" in error_msg or "vector_store" in error_msg:
649
+ return FileSearchUploadError(
650
+ f"File Search upload failed: {error}. "
651
+ f"This can be intermittent - retry with --file-search-retry-count option."
652
+ )
653
+ return FileSearchError(f"File Search error: {error}")
654
+
655
+ elif tool_name == "code-interpreter":
656
+ if "container" in error_msg:
657
+ return ContainerExpiredError(
658
+ f"Code Interpreter container error: {error}. "
659
+ f"Container has 20-minute runtime and 2-minute idle limits."
660
+ )
661
+ return APIError(f"Code Interpreter error: {error}")
662
+
663
+ elif tool_name == "mcp":
664
+ return MCPConnectionError(
665
+ f"MCP server error: {error}. "
666
+ f"Check server connectivity and require_approval='never' setting."
667
+ )
668
+
669
+ return APIError(f"{tool_name} error: {error}")
670
+
671
+
445
672
  class SchemaValidationError(ModelCreationError):
446
673
  """Raised when schema validation fails."""
447
674
 
@@ -503,9 +730,86 @@ class SchemaValidationError(ModelCreationError):
503
730
  super().__init__(final_message, context=context, exit_code=exit_code)
504
731
 
505
732
 
733
+ def handle_error(e: Exception) -> None:
734
+ """Handle CLI errors and display appropriate messages.
735
+
736
+ Maintains specific error type handling while reducing duplication.
737
+ Provides enhanced debug logging for CLI errors.
738
+ """
739
+ import sys
740
+
741
+ import click
742
+
743
+ # 1. Determine error type and message
744
+ if isinstance(e, SchemaValidationError):
745
+ msg = str(e) # Already formatted in SchemaValidationError
746
+ exit_code = e.exit_code
747
+ elif isinstance(e, ModelCreationError):
748
+ # Unwrap ModelCreationError that might wrap SchemaValidationError
749
+ if isinstance(e.__cause__, SchemaValidationError):
750
+ return handle_error(e.__cause__)
751
+ msg = f"Model creation error: {str(e)}"
752
+ exit_code = ExitCode.SCHEMA_ERROR
753
+ elif isinstance(e, click.UsageError):
754
+ msg = f"Usage error: {str(e)}"
755
+ exit_code = ExitCode.USAGE_ERROR
756
+ elif isinstance(e, SchemaFileError):
757
+ msg = str(e) # Use existing __str__ formatting
758
+ exit_code = ExitCode.SCHEMA_ERROR
759
+ elif isinstance(e, (InvalidJSONError, json.JSONDecodeError)):
760
+ msg = f"Invalid JSON error: {str(e)}"
761
+ exit_code = ExitCode.DATA_ERROR
762
+ elif isinstance(e, CLIError):
763
+ msg = str(e) # Use existing __str__ formatting
764
+ exit_code = ExitCode(e.exit_code) # Convert int to ExitCode
765
+ else:
766
+ msg = f"Unexpected error: {str(e)}"
767
+ exit_code = ExitCode.INTERNAL_ERROR
768
+
769
+ # 2. Debug logging
770
+ if isinstance(e, CLIError) and logger.isEnabledFor(logging.DEBUG):
771
+ # Format context fields with lowercase keys and simple values
772
+ context_str = ""
773
+ if hasattr(e, "context") and e.context:
774
+ for key, value in sorted(e.context.items()):
775
+ if key not in {
776
+ "timestamp",
777
+ "host",
778
+ "version",
779
+ "python_version",
780
+ }:
781
+ if isinstance(value, dict):
782
+ context_str += (
783
+ f"{key.lower()}:\n{json.dumps(value, indent=2)}\n"
784
+ )
785
+ else:
786
+ context_str += f"{key.lower()}: {value}\n"
787
+
788
+ logger.debug(
789
+ f"Error details:\nType: {type(e).__name__}\n{context_str.rstrip()}"
790
+ )
791
+ elif not isinstance(
792
+ e,
793
+ (
794
+ click.UsageError,
795
+ DuplicateFileMappingError,
796
+ VariableNameError,
797
+ VariableValueError,
798
+ ),
799
+ ):
800
+ logger.error(msg, exc_info=True)
801
+
802
+ # 3. User output
803
+ click.secho(msg, fg="red", err=True)
804
+ sys.exit(exit_code)
805
+
806
+
506
807
  # Export public API
507
808
  __all__ = [
508
809
  "VariableError",
810
+ "VariableNameError",
811
+ "VariableValueError",
812
+ "DuplicateFileMappingError",
509
813
  "PathError",
510
814
  "PathSecurityError",
511
815
  "OstructFileNotFoundError",
@@ -522,5 +826,5 @@ __all__ = [
522
826
  "APIResponseError",
523
827
  "EmptyResponseError",
524
828
  "InvalidResponseFormatError",
525
- "OpenAIClientError",
829
+ "handle_error",
526
830
  ]
ostruct/cli/exit_codes.py CHANGED
@@ -16,3 +16,4 @@ class ExitCode(IntEnum):
16
16
  UNKNOWN_ERROR = 7
17
17
  SECURITY_ERROR = 8
18
18
  FILE_ERROR = 9
19
+ OPERATION_TIMEOUT = 10