Source code for pysnap.report.models
"""Data contracts shared by the report reading modules."""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
import re
# ``report`` names its archives ``report.<NN>.<host>`` where ``NN`` is the
# task number printed with ``%02d``. The host part may itself contain dots.
_REPORT_NAME_PATTERN = re.compile(r"^report\.(?P<task>\d+)\.(?P<host>.+)$")
[docs]
@dataclass(frozen=True)
class ReportLimits:
"""Bound the resources spent on one untrusted report archive.
:param max_archive_bytes: Largest accepted compressed archive size.
:param max_member_bytes: Largest accepted size of one archive member.
:param max_unpacked_bytes: Largest amount of decompressed data read from
the archive, including skipped members and tar headers.
"""
max_archive_bytes: int = 16 * 1024 * 1024
max_member_bytes: int = 64 * 1024 * 1024
max_unpacked_bytes: int = 128 * 1024 * 1024
[docs]
@dataclass(frozen=True)
class ReportDiagnostic:
"""Describe one non-fatal problem found in a report.
:param code: Stable machine-readable problem identifier.
:param message: Human-readable explanation.
"""
code: str
message: str
[docs]
@dataclass(frozen=True)
class ReportName:
"""Represent the task number and host encoded in a report file name."""
task: int
host: str
[docs]
@classmethod
def parse(cls, file_name: str) -> ReportName | None:
"""Parse a ``report.<NN>.<host>`` file name.
:param file_name: Base name of the report file.
:returns: Parsed name or ``None`` when the name does not follow the
``report`` convention.
"""
match = _REPORT_NAME_PATTERN.match(file_name)
if match is None:
return None
return cls(task=int(match["task"]), host=match["host"])
[docs]
@dataclass(frozen=True)
class ReportArchive:
"""Hold the raw members read from a report archive.
:param members: Known member names mapped to their content.
:param mtimes: Known member names mapped to their archive modification
time in seconds since the epoch.
:param diagnostics: Problems found while reading the archive.
"""
members: dict[str, bytes]
mtimes: dict[str, int]
diagnostics: tuple[ReportDiagnostic, ...] = ()
[docs]
@dataclass(frozen=True)
class TimingEvent:
"""Represent one input or output chunk of the recorded session.
:param kind: ``"I"`` for keyboard input or ``"O"`` for terminal output.
:param delay: Seconds elapsed since the previous chunk.
:param time: Seconds elapsed since the start of the recording.
:param data: Raw bytes of the chunk.
"""
kind: str
delay: float
time: float
data: bytes
[docs]
@dataclass(frozen=True)
class Recording:
"""Represent a parsed session report.
:param source_name: Base name of the report file.
:param name: Task and host parsed from the file name, when available.
:param headers: Raw ``H`` header values from the timing log.
:param start_time: Recording start reported by ``script``.
:param duration: Recording duration in seconds reported by ``script``.
:param exit_code: Exit code of the recorded shell.
:param columns: Terminal width at recording start.
:param lines: Terminal height at recording start.
:param events: Input and output chunks in recording order.
:param input_bytes: Complete recorded keyboard input.
:param output_bytes: Complete recorded terminal output.
:param cpu_info: Content of ``CPU.txt`` (``lscpu`` output).
:param mtimes: Archive modification times of the known members.
:param diagnostics: Non-fatal problems found in the report.
"""
source_name: str
name: ReportName | None
headers: dict[str, str]
start_time: datetime | None
duration: float | None
exit_code: int | None
columns: int | None
lines: int | None
events: tuple[TimingEvent, ...]
input_bytes: bytes
output_bytes: bytes
cpu_info: str = ""
mtimes: dict[str, int] = field(default_factory=dict)
diagnostics: tuple[ReportDiagnostic, ...] = ()
[docs]
@dataclass(frozen=True)
class CellStyle:
"""Represent the visual attributes of rendered terminal text.
Colors keep the ``pyte`` notation: ``"default"``, an ANSI color name such
as ``"red"`` or ``"brightred"``, or a six-digit hex value.
"""
fg: str = "default"
bg: str = "default"
bold: bool = False
italics: bool = False
underscore: bool = False
strikethrough: bool = False
reverse: bool = False
PLAIN_STYLE = CellStyle()
[docs]
@dataclass(frozen=True)
class StyledRun:
"""Represent consecutive characters that share one style."""
text: str
style: CellStyle = PLAIN_STYLE
[docs]
@dataclass(frozen=True)
class TranscriptLine:
"""Represent one rendered line of the session transcript."""
runs: tuple[StyledRun, ...] = ()
[docs]
@property
def text(self) -> str:
"""Return the plain text of the line."""
return "".join(run.text for run in self.runs)
[docs]
@dataclass(frozen=True)
class PromptInfo:
"""Represent the parts of the prompt installed by ``report``.
``report`` sets ``PS1="[\\u@NN-HOST \\W]# "``.
"""
user: str
task: int
host: str
directory: str
[docs]
@dataclass(frozen=True)
class CommandRecord:
"""Represent one input line submitted with Enter.
:param index: Zero-based position among all recorded commands.
:param time: Recording time of the Enter key press in seconds.
:param line: Transcript line where the input starts.
:param column: Column where the input starts, i.e. the prompt width.
:param prompt: Text left of the input on its first line.
:param text: Submitted input as displayed by the shell.
:param prompt_info: Parsed ``report`` prompt, ``None`` for other prompts
such as ``vtysh`` or a remote shell.
:param first_event: Index of the first input event of this line.
:param enter_event: Index of the input event that contained Enter.
:param output_start: First transcript line after the input.
:param output_end: Transcript line after the last output line.
"""
index: int
time: float
line: int
column: int
prompt: str
text: str
prompt_info: PromptInfo | None
first_event: int
enter_event: int
output_start: int
output_end: int
[docs]
@dataclass(frozen=True)
class Transcript:
"""Represent the rendered session: final text and recognized commands."""
lines: tuple[TranscriptLine, ...]
commands: tuple[CommandRecord, ...]
diagnostics: tuple[ReportDiagnostic, ...] = ()