Server side implementation for interactive entry edits
This commit is contained in:
74
sia/entry/__init__.py
Normal file
74
sia/entry/__init__.py
Normal file
@@ -0,0 +1,74 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Callable, List
|
||||
|
||||
class Entry(ABC):
|
||||
"""
|
||||
Abstract base class for all entry types in the working memory.
|
||||
Provides observable pattern functionality.
|
||||
"""
|
||||
|
||||
def __init__(self, id: str):
|
||||
"""
|
||||
Initialize a new entry with provided id and timestamp.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
"""
|
||||
self._id = id
|
||||
self._change_handlers: List[Callable[['Entry'], None]] = []
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
"""Get entry's unique identifier."""
|
||||
return self._id
|
||||
|
||||
def add_change_handler(self, handler: Callable[['Entry'], None]) -> None:
|
||||
"""Add a callback for entry changes."""
|
||||
if handler not in self._change_handlers:
|
||||
self._change_handlers.append(handler)
|
||||
|
||||
def notify_change(self) -> None:
|
||||
"""Notify all handlers of entry state change."""
|
||||
for handler in self._change_handlers:
|
||||
handler(self)
|
||||
|
||||
@abstractmethod
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Update the entry's state.
|
||||
Must be implemented by concrete classes.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this entry's context.
|
||||
Must be implemented by concrete classes.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
pass
|
||||
|
||||
def serialize(self) -> dict[str, str]:
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
pass
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up any resources used by this entry.
|
||||
Should be overridden by classes that need cleanup.
|
||||
Default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset the entry as if update was never called.
|
||||
Default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
162
sia/entry/background_entry.py
Normal file
162
sia/entry/background_entry.py
Normal file
@@ -0,0 +1,162 @@
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional
|
||||
|
||||
from . import Entry
|
||||
|
||||
class BackgroundEntry(Entry):
|
||||
"""
|
||||
Entry type for long-running background processes.
|
||||
|
||||
Attributes:
|
||||
script: The script/command to execute
|
||||
stdout: Captured standard output
|
||||
stderr: Captured standard error
|
||||
process: The running subprocess.Popen instance
|
||||
exit_code: Exit code when process completes
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
script: str,
|
||||
):
|
||||
"""
|
||||
Initialize a new background entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
script: The script/command to execute
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.script = script
|
||||
self._process: Optional[subprocess.Popen] = None
|
||||
self.stdout = ""
|
||||
self.stderr = ""
|
||||
self.exit_code = None
|
||||
|
||||
@property
|
||||
def pid(self) -> Optional[int]:
|
||||
"""Get the process ID (None if not running)."""
|
||||
return self._process.pid if self._process is not None else None
|
||||
|
||||
def cleanup(self) -> None:
|
||||
"""
|
||||
Clean up the background process if it's still running.
|
||||
Ensures process is terminated and file handles are closed.
|
||||
"""
|
||||
if self._process is not None:
|
||||
try:
|
||||
if self._process.stdout:
|
||||
self._process.stdout.close()
|
||||
if self._process.stderr:
|
||||
self._process.stderr.close()
|
||||
if self._process.poll() is None:
|
||||
self._process.terminate()
|
||||
try:
|
||||
self._process.wait(timeout=1.0)
|
||||
except subprocess.TimeoutExpired:
|
||||
self._process.kill()
|
||||
self._process.wait()
|
||||
except:
|
||||
pass # Ignore cleanup errors
|
||||
finally:
|
||||
self._process = None
|
||||
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Cleans up existing process and resets output buffers.
|
||||
"""
|
||||
self.cleanup()
|
||||
self.stdout = ""
|
||||
self.stderr = ""
|
||||
self.exit_code = None
|
||||
self.notify_change()
|
||||
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Start the process if not running and collect any new output.
|
||||
Updates stdout and stderr with any new output.
|
||||
"""
|
||||
if self._process is None and self.exit_code is None:
|
||||
self._process = subprocess.Popen(
|
||||
self.script,
|
||||
shell=True,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
bufsize=1 # Line buffered
|
||||
)
|
||||
self.notify_change()
|
||||
return
|
||||
|
||||
if self._process is None:
|
||||
return
|
||||
|
||||
exit_code = self._process.poll()
|
||||
if exit_code is not None:
|
||||
try:
|
||||
remaining_out, remaining_err = self._process.communicate(timeout=0.1)
|
||||
self.stdout += remaining_out
|
||||
self.stderr += remaining_err
|
||||
except subprocess.TimeoutExpired:
|
||||
pass # Process didn't finish communicating, try again next update
|
||||
|
||||
self.exit_code = exit_code
|
||||
self.cleanup()
|
||||
self.notify_change()
|
||||
return
|
||||
|
||||
if self._process.stdout:
|
||||
while True:
|
||||
try:
|
||||
line = self._process.stdout.readline()
|
||||
if not line:
|
||||
break
|
||||
self.stdout += line
|
||||
except:
|
||||
break
|
||||
|
||||
if self._process.stderr:
|
||||
while True:
|
||||
try:
|
||||
line = self._process.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
self.stderr += line
|
||||
except:
|
||||
break
|
||||
self.notify_change()
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this background entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("background", {"id": self._id})
|
||||
if self._process is not None:
|
||||
element.set("pid", str(self._process.pid))
|
||||
elif self.exit_code is not None:
|
||||
element.set("exit_code", str(self.exit_code))
|
||||
element.text = self.script
|
||||
stdout_elem = ET.SubElement(element, "stdout")
|
||||
stdout_elem.text = self.stdout
|
||||
stderr_elem = ET.SubElement(element, "stderr")
|
||||
stderr_elem.text = self.stderr
|
||||
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "background",
|
||||
"id": self._id,
|
||||
"script": self.script,
|
||||
"stdout": self.stdout,
|
||||
"stderr": self.stderr,
|
||||
"exit_code": self.exit_code,
|
||||
}
|
||||
120
sia/entry/entry_factory.py
Normal file
120
sia/entry/entry_factory.py
Normal file
@@ -0,0 +1,120 @@
|
||||
from typing import Optional
|
||||
|
||||
from . import Entry
|
||||
from ..io_buffer import IOBuffer
|
||||
from .background_entry import BackgroundEntry
|
||||
from .parse_error_entry import ParseErrorEntry
|
||||
from .read_entry import ReadEntry
|
||||
from .reasoning_entry import ReasoningEntry
|
||||
from .repeat_entry import RepeatEntry
|
||||
from .single_entry import SingleEntry
|
||||
from .write_entry import WriteEntry
|
||||
|
||||
class EntryFactory:
|
||||
def create_entry(data: dict, io_buffer: IOBuffer) -> Entry:
|
||||
entry_type = data.get("type")
|
||||
entry_id = data.get("id")
|
||||
|
||||
if entry_type == "background":
|
||||
return BackgroundEntry(entry_id, data["script"])
|
||||
|
||||
elif entry_type == "read_stdin":
|
||||
return ReadEntry(entry_id, io_buffer)
|
||||
|
||||
elif entry_type == "reasoning":
|
||||
return ReasoningEntry(entry_id, data["content"])
|
||||
|
||||
elif entry_type == "repeat":
|
||||
return RepeatEntry(
|
||||
entry_id,
|
||||
data["script"],
|
||||
data.get("timeout"),
|
||||
data.get("limit")
|
||||
)
|
||||
|
||||
elif entry_type == "single":
|
||||
return SingleEntry(
|
||||
entry_id,
|
||||
data["script"],
|
||||
data.get("timeout"),
|
||||
data.get("limit")
|
||||
)
|
||||
|
||||
elif entry_type == "write":
|
||||
return WriteEntry(entry_id, data["content"], io_buffer)
|
||||
|
||||
raise ValueError(f"Unknown entry type: {entry_type}")
|
||||
|
||||
def update_entry(entry: Entry, data: dict):
|
||||
if isinstance(entry, Entry):
|
||||
if "id" in data:
|
||||
entry.id = data["id"]
|
||||
|
||||
if isinstance(entry, SingleEntry):
|
||||
if "script" in data:
|
||||
entry.script = data["script"]
|
||||
if "timeout" in data:
|
||||
entry.timeout = float(data["timeout"]) if data["timeout"] else None
|
||||
if "limit" in data:
|
||||
entry.limit = int(data["limit"]) if data["limit"] else None
|
||||
if "stdout" in data:
|
||||
entry.stdout = data["stdout"]
|
||||
if "stderr" in data:
|
||||
entry.stderr = data["stderr"]
|
||||
if "exit_code" in data:
|
||||
entry.exit_code = int(data["exit_code"]) if data["exit_code"] else None
|
||||
if "executed" in data:
|
||||
entry.executed = bool(data["executed"])
|
||||
if "timed_out" in data:
|
||||
entry.timed_out = bool(data["timed_out"])
|
||||
|
||||
if isinstance(entry, RepeatEntry):
|
||||
if "script" in data:
|
||||
entry.script = data["script"]
|
||||
if "timeout" in data:
|
||||
entry.timeout = float(data["timeout"]) if data["timeout"] else None
|
||||
if "limit" in data:
|
||||
entry.limit = int(data["limit"]) if data["limit"] else None
|
||||
if "stdout" in data:
|
||||
entry.stdout = data["stdout"]
|
||||
if "stderr" in data:
|
||||
entry.stderr = data["stderr"]
|
||||
if "exit_code" in data:
|
||||
entry.exit_code = int(data["exit_code"]) if data["exit_code"] else None
|
||||
if "executed" in data:
|
||||
entry.executed = bool(data["executed"])
|
||||
if "timed_out" in data:
|
||||
entry.timed_out = bool(data["timed_out"])
|
||||
|
||||
elif isinstance(entry, BackgroundEntry):
|
||||
if "script" in data:
|
||||
entry.script = data["script"]
|
||||
if "stdout" in data:
|
||||
entry.stdout = data["stdout"]
|
||||
if "stderr" in data:
|
||||
entry.stderr = data["stderr"]
|
||||
if "exit_code" in data:
|
||||
entry.exit_code = int(data["exit_code"]) if data["exit_code"] else None
|
||||
|
||||
elif isinstance(entry, ParseErrorEntry):
|
||||
if "content" in data:
|
||||
entry.content = data["content"]
|
||||
if "error" in data:
|
||||
entry.error = data["error"]
|
||||
|
||||
elif isinstance(entry, ReadEntry):
|
||||
if "content" in data:
|
||||
entry.content = data["content"]
|
||||
if "read" in data:
|
||||
entry.read = bool(data["read"])
|
||||
|
||||
elif isinstance(entry, ReasoningEntry):
|
||||
if "content" in data:
|
||||
entry.content = data["content"]
|
||||
|
||||
elif isinstance(entry, WriteEntry):
|
||||
if "content" in data:
|
||||
entry.content = data["content"]
|
||||
if "written" in data:
|
||||
entry.written = bool(data["written"])
|
||||
entry.notify_change()
|
||||
55
sia/entry/parse_error_entry.py
Normal file
55
sia/entry/parse_error_entry.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from . import Entry
|
||||
|
||||
class ParseErrorEntry(Entry):
|
||||
"""
|
||||
Entry type for parse and validation errors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
content: str,
|
||||
error: str,
|
||||
):
|
||||
"""
|
||||
Initialize a new parse error entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
content: Original content that failed to parse
|
||||
error: Error message describing the failure
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.content = content
|
||||
self.error = error
|
||||
|
||||
def update(self) -> None:
|
||||
pass
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this parse error entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("parse_error", {"id": self._id})
|
||||
error_elem = ET.SubElement(element, "error")
|
||||
error_elem.text = self.error
|
||||
content_elem = ET.SubElement(element, "content")
|
||||
content_elem.text = self.content
|
||||
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "parse_error",
|
||||
"id": self._id,
|
||||
"content": self.content,
|
||||
"error": self.error,
|
||||
}
|
||||
60
sia/entry/read_entry.py
Normal file
60
sia/entry/read_entry.py
Normal file
@@ -0,0 +1,60 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from . import Entry
|
||||
from ..io_buffer import IOBuffer
|
||||
|
||||
class ReadEntry(Entry):
|
||||
"""
|
||||
Entry type for reading content from standard input.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
io_buffer: IOBuffer,
|
||||
):
|
||||
"""
|
||||
Initialize a new read entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
io_buffer: Buffer to use for IO operations
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.content: str = "",
|
||||
self.read = False
|
||||
self._io_buffer = io_buffer
|
||||
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Read from stdin if not already read.
|
||||
Uses the provided IO buffer for the actual read operation.
|
||||
"""
|
||||
if not self.read:
|
||||
self.content = self._io_buffer.read()
|
||||
self.read = True
|
||||
self.notify_change()
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this read entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("read_stdin", {"id": self._id})
|
||||
if self.read:
|
||||
element.text = self.content
|
||||
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "read_stdin",
|
||||
"id": self._id,
|
||||
"content": self.content,
|
||||
"read": self.read,
|
||||
}
|
||||
50
sia/entry/reasoning_entry.py
Normal file
50
sia/entry/reasoning_entry.py
Normal file
@@ -0,0 +1,50 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from . import Entry
|
||||
|
||||
class ReasoningEntry(Entry):
|
||||
"""
|
||||
Entry type for agent reasoning steps.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
content: str,
|
||||
):
|
||||
"""
|
||||
Initialize a new reasoning entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
timestamp: Creation timestamp for this entry
|
||||
content: The reasoning text
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.content = content
|
||||
|
||||
def update(self) -> None:
|
||||
"""No update needed for reasoning entries."""
|
||||
pass
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this reasoning entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("reasoning", {"id": self._id})
|
||||
element.text = self.content
|
||||
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "reasoning",
|
||||
"id": self._id,
|
||||
"content": self.content,
|
||||
}
|
||||
105
sia/entry/repeat_entry.py
Normal file
105
sia/entry/repeat_entry.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional
|
||||
|
||||
from . import Entry
|
||||
|
||||
class RepeatEntry(Entry):
|
||||
"""
|
||||
Entry type for scripts that are executed on every update.
|
||||
"""
|
||||
|
||||
default_timeout = 1
|
||||
default_limit = 1024
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
script: str,
|
||||
timeout: Optional[float] = None,
|
||||
limit: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Initialize a new repeat entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
script: The script/command to execute
|
||||
timeout: Maximum time to wait for script execution
|
||||
limit: Maximum number of characters to capture from stdout/stderr
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.script = script
|
||||
self.timeout = timeout
|
||||
self.limit = limit
|
||||
self.stdout: str = "",
|
||||
self.stderr: str = "",
|
||||
self.exit_code: Optional[int] = None,
|
||||
self.timed_out: bool = False
|
||||
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Execute the script and update the output.
|
||||
Captures stdout, stderr and exit code from each execution.
|
||||
"""
|
||||
try:
|
||||
process = subprocess.run(
|
||||
self.script,
|
||||
timeout=(self.timeout or self.default_timeout),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
self.stdout = process.stdout
|
||||
self.stderr = process.stderr
|
||||
self.exit_code = process.returncode
|
||||
self._timed_out = False
|
||||
except subprocess.TimeoutExpired as e:
|
||||
self._timed_out = True
|
||||
self.notify_change()
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this repeat entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("repeat", {"id": self.id})
|
||||
if self.timeout:
|
||||
element.set("timeout", str(self.timeout))
|
||||
element.text = self.script
|
||||
if self._timed_out:
|
||||
element.set("timed_out", "true")
|
||||
elif self.exit_code is not None:
|
||||
element.set("exit_code", str(self.exit_code))
|
||||
if self.limit:
|
||||
element.set("limit", str(self.limit))
|
||||
if len(self.stdout) > (self.limit or self.default_limit):
|
||||
element.set("stdout_truncated", "true")
|
||||
element.set("stdout_length", str(len(self.stdout)))
|
||||
stdout_elem = ET.SubElement(element, "stdout")
|
||||
stdout_elem.text = self.stdout[:(self.limit or self.default_limit)]
|
||||
if len(self.stderr) > (self.limit or self.default_limit):
|
||||
element.set("stderr_truncated", "true")
|
||||
element.set("stderr_length", str(len(self.stderr)))
|
||||
stderr_elem = ET.SubElement(element, "stderr")
|
||||
stderr_elem.text = self.stderr[:(self.limit or self.default_limit)]
|
||||
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "repeat",
|
||||
"id": self.id,
|
||||
"script": self.script,
|
||||
"timeout": self.timeout,
|
||||
"limit": self.limit,
|
||||
"stdout": self.stdout,
|
||||
"stderr": self.stderr,
|
||||
"exit_code": self.exit_code,
|
||||
"timed_out": self._timed_out
|
||||
}
|
||||
114
sia/entry/single_entry.py
Normal file
114
sia/entry/single_entry.py
Normal file
@@ -0,0 +1,114 @@
|
||||
import subprocess
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import Optional
|
||||
|
||||
from . import Entry
|
||||
|
||||
class SingleEntry(Entry):
|
||||
"""
|
||||
Entry type for one-time script executions.
|
||||
"""
|
||||
|
||||
default_timeout = 1
|
||||
default_limit = 1024
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
script: str,
|
||||
timeout: Optional[float] = None,
|
||||
limit: Optional[int] = None,
|
||||
):
|
||||
"""
|
||||
Initialize a new single shot entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
script: The script/command to execute
|
||||
timeout: Maximum time to wait for script execution
|
||||
limit: Maximum number of characters to capture from stdout/stderr
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.script = script
|
||||
self.timeout = timeout
|
||||
self.limit = limit
|
||||
self.stdout: str = "",
|
||||
self.stderr: str = "",
|
||||
self.exit_code: Optional[int] = None
|
||||
self.executed: bool = False,
|
||||
self.timed_out: bool = False
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset execution state to allow running again."""
|
||||
self.executed = False
|
||||
self.timed_out = False
|
||||
self.stdout = ""
|
||||
self.stderr = ""
|
||||
self.exit_code = None
|
||||
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Execute the script if not already executed.
|
||||
Captures stdout, stderr and exit code.
|
||||
"""
|
||||
if self.executed:
|
||||
return
|
||||
self.executed = True
|
||||
try:
|
||||
process = subprocess.run(
|
||||
self.script,
|
||||
timeout=(self.timeout or self.default_timeout),
|
||||
shell=True,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
self.stdout = process.stdout
|
||||
self.stderr = process.stderr
|
||||
self.exit_code = process.returncode
|
||||
except subprocess.TimeoutExpired as e:
|
||||
self.timed_out = True
|
||||
self.notify_change()
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this single shot entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("single", {"id": self.id})
|
||||
if self.timeout:
|
||||
element.set("timeout", str(self.timeout))
|
||||
element.text = self.script
|
||||
if self.timed_out:
|
||||
element.set("timed_out", "true")
|
||||
elif self.executed:
|
||||
element.set("exit_code", str(self.exit_code))
|
||||
if self.limit:
|
||||
element.set("limit", str(self.limit))
|
||||
if len(self.stdout) > (self.limit or self.default_limit):
|
||||
element.set("stdout_truncated", "true")
|
||||
element.set("stdout_length", str(len(self.stdout)))
|
||||
stdout_elem = ET.SubElement(element, "stdout")
|
||||
stdout_elem.text = self.stdout[:(self.limit or self.default_limit)]
|
||||
if len(self.stderr) > (self.limit or self.default_limit):
|
||||
element.set("stderr_truncated", "true")
|
||||
element.set("stderr_length", str(len(self.stderr)))
|
||||
stderr_elem = ET.SubElement(element, "stderr")
|
||||
stderr_elem.text = self.stderr[:(self.limit or self.default_limit)]
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "single",
|
||||
"id": self.id,
|
||||
"script": self.script,
|
||||
"timeout": self.timeout,
|
||||
"limit": self.limit,
|
||||
"stdout": self.stdout,
|
||||
"stderr": self.stderr,
|
||||
"exit_code": self.exit_code,
|
||||
}
|
||||
59
sia/entry/write_entry.py
Normal file
59
sia/entry/write_entry.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from . import Entry
|
||||
from ..io_buffer import IOBuffer
|
||||
|
||||
class WriteEntry(Entry):
|
||||
"""
|
||||
Entry type for writing content to standard output.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
id: str,
|
||||
content: str,
|
||||
io_buffer: IOBuffer,
|
||||
):
|
||||
"""
|
||||
Initialize a new write entry.
|
||||
|
||||
Args:
|
||||
id: Unique identifier for this entry
|
||||
content: Text to write to stdout
|
||||
io_buffer: Buffer to use for IO operations
|
||||
"""
|
||||
super().__init__(id)
|
||||
self.content = content
|
||||
self.written: bool = False
|
||||
self._io_buffer = io_buffer
|
||||
|
||||
def update(self) -> None:
|
||||
"""
|
||||
Write the content to stdout if not already written.
|
||||
Uses the provided IO buffer for the actual write operation.
|
||||
"""
|
||||
if not self.written:
|
||||
self._io_buffer.write(self.content)
|
||||
self.written = True
|
||||
self.notify_change()
|
||||
|
||||
def generate_context(self) -> ET.Element:
|
||||
"""
|
||||
Generate an XML Element representing this write entry.
|
||||
|
||||
Returns:
|
||||
ET.Element: XML element containing the entry's data
|
||||
"""
|
||||
element = ET.Element("write_stdout", {"id": self.id})
|
||||
element.text = self.content
|
||||
return element
|
||||
|
||||
def serialize(self):
|
||||
"""
|
||||
Collect all public attributes of the entry into a dictionary.
|
||||
"""
|
||||
return {
|
||||
"type": "write",
|
||||
"id": self._id,
|
||||
"content": self.content,
|
||||
}
|
||||
Reference in New Issue
Block a user