|
1 | | -"""File operation executors - CreateFile, ReadFiles, RenderTemplate, EditFile. |
| 1 | +"""File operation executors - CreateFile, ReadFiles, EditFile. |
2 | 2 |
|
3 | 3 | Architecture: |
4 | 4 | - Execute returns output directly (no Result wrapper) |
|
15 | 15 | from typing import Any, ClassVar, Literal |
16 | 16 |
|
17 | 17 | import yaml |
18 | | -from jinja2 import StrictUndefined |
19 | | -from jinja2.sandbox import SandboxedEnvironment |
20 | 18 | from pydantic import BaseModel, Field, computed_field, field_validator |
21 | 19 |
|
22 | 20 | from .block import BlockInput, BlockOutput |
@@ -547,150 +545,6 @@ async def execute( # type: ignore[override] |
547 | 545 | ) |
548 | 546 |
|
549 | 547 |
|
550 | | -# ============================================================================ |
551 | | -# RenderTemplate Executor |
552 | | -# ============================================================================ |
553 | | - |
554 | | - |
555 | | -class RenderTemplateInput(BlockInput): |
556 | | - """Input model for RenderTemplate executor.""" |
557 | | - |
558 | | - template: str = Field(description="Jinja2 template string") |
559 | | - variables: dict[str, Any] = Field( |
560 | | - default_factory=dict, |
561 | | - description="Variables to substitute in template", |
562 | | - ) |
563 | | - output_path: str | None = Field( |
564 | | - default=None, |
565 | | - description="Optional file path to write rendered content", |
566 | | - ) |
567 | | - encoding: str = Field(default="utf-8", description="Text encoding for output file") |
568 | | - overwrite: bool | str = Field( |
569 | | - default=True, |
570 | | - description="Whether to overwrite existing output file (or interpolation string)", |
571 | | - ) |
572 | | - create_parents: bool | str = Field( |
573 | | - default=True, |
574 | | - description="Create parent directories for output file (or interpolation string)", |
575 | | - ) |
576 | | - |
577 | | - # Validators for boolean fields with interpolation support |
578 | | - _validate_overwrite = field_validator("overwrite", mode="before")( |
579 | | - interpolatable_boolean_validator() |
580 | | - ) |
581 | | - _validate_create_parents = field_validator("create_parents", mode="before")( |
582 | | - interpolatable_boolean_validator() |
583 | | - ) |
584 | | - |
585 | | - |
586 | | -class RenderTemplateOutput(BlockOutput): |
587 | | - """Output model for RenderTemplate executor. |
588 | | -
|
589 | | - All fields have defaults to support graceful degradation when template rendering fails. |
590 | | - A default-constructed instance represents a failed/crashed rendering operation. |
591 | | - """ |
592 | | - |
593 | | - content: str = Field( |
594 | | - default="", |
595 | | - description="Rendered template content (empty string if failed)", |
596 | | - ) |
597 | | - output_path: str | None = Field( |
598 | | - default=None, |
599 | | - description="Absolute path to output file (None if not specified or failed)", |
600 | | - ) |
601 | | - size_bytes: int | None = Field( |
602 | | - default=None, |
603 | | - description="Output file size in bytes (None if not written or failed)", |
604 | | - ) |
605 | | - |
606 | | - |
607 | | -class RenderTemplateExecutor(BlockExecutor): |
608 | | - """ |
609 | | - Jinja2 template rendering executor. |
610 | | -
|
611 | | - Architecture (ADR-006): |
612 | | - - Returns RenderTemplateOutput directly |
613 | | - - Raises TemplateSyntaxError, UndefinedError for template issues |
614 | | - - Raises exceptions for file write failures |
615 | | - """ |
616 | | - |
617 | | - type_name: ClassVar[str] = "RenderTemplate" |
618 | | - input_type: ClassVar[type[BlockInput]] = RenderTemplateInput |
619 | | - output_type: ClassVar[type[BlockOutput]] = RenderTemplateOutput |
620 | | - |
621 | | - security_level: ClassVar[ExecutorSecurityLevel] = ExecutorSecurityLevel.TRUSTED |
622 | | - capabilities: ClassVar[ExecutorCapabilities] = ExecutorCapabilities( |
623 | | - can_read_files=True, |
624 | | - can_write_files=True, |
625 | | - ) |
626 | | - |
627 | | - async def execute( # type: ignore[override] |
628 | | - self, inputs: RenderTemplateInput, context: Execution |
629 | | - ) -> RenderTemplateOutput: |
630 | | - """RenderTemplate Jinja2 template. |
631 | | -
|
632 | | - Returns: |
633 | | - RenderTemplateOutput with rendered content and optional file path |
634 | | -
|
635 | | - Raises: |
636 | | - TemplateSyntaxError: Invalid template syntax |
637 | | - UndefinedError: Undefined variable in template |
638 | | - ValueError: Invalid output path |
639 | | - FileExistsError: Output file exists and overwrite=False |
640 | | - Exception: Other errors |
641 | | - """ |
642 | | - # Resolve interpolatable fields to their actual types |
643 | | - overwrite = resolve_interpolatable_boolean(inputs.overwrite, "overwrite") |
644 | | - create_parents = resolve_interpolatable_boolean(inputs.create_parents, "create_parents") |
645 | | - |
646 | | - # RenderTemplate template (exceptions bubble up) |
647 | | - env = SandboxedEnvironment(undefined=StrictUndefined, autoescape=False) |
648 | | - template = env.from_string(inputs.template) |
649 | | - rendered = template.render(**inputs.variables) |
650 | | - |
651 | | - # Write to file if output_path specified |
652 | | - output_path_str: str | None = None |
653 | | - size_bytes: int | None = None |
654 | | - |
655 | | - if inputs.output_path: |
656 | | - # Resolve path |
657 | | - path_result = PathResolver.resolve_and_validate( |
658 | | - inputs.output_path, allow_traversal=True |
659 | | - ) |
660 | | - if not path_result.is_success: |
661 | | - raise ValueError(f"Invalid output_path: {path_result.error}") |
662 | | - |
663 | | - # Type narrowing: is_success guarantees value is not None |
664 | | - assert path_result.value is not None |
665 | | - file_path = path_result.value |
666 | | - |
667 | | - # Check overwrite protection |
668 | | - if file_path.exists() and not overwrite: |
669 | | - raise FileExistsError(f"Output file exists and overwrite=False: {file_path}") |
670 | | - |
671 | | - # Write file using utility |
672 | | - write_result = FileOperations.write_text( |
673 | | - path=file_path, |
674 | | - content=rendered, |
675 | | - encoding=inputs.encoding, |
676 | | - mode=None, |
677 | | - create_parents=create_parents, |
678 | | - ) |
679 | | - |
680 | | - if not write_result.is_success: |
681 | | - raise OSError(write_result.error) |
682 | | - |
683 | | - output_path_str = str(file_path) |
684 | | - size_bytes = write_result.value |
685 | | - |
686 | | - # Build output |
687 | | - return RenderTemplateOutput( |
688 | | - content=rendered, |
689 | | - output_path=output_path_str, |
690 | | - size_bytes=size_bytes, |
691 | | - ) |
692 | | - |
693 | | - |
694 | 548 | # ============================================================================ |
695 | 549 | # EditFile Executor |
696 | 550 | # ============================================================================ |
|
0 commit comments