pysnap.report package

Reading and checking session reports recorded by the report utility.

Submodules

pysnap.report.archive module

Safe reading of report archives.

A report is an untrusted tar.gz file produced inside a student VM. The reader never extracts anything to disk: it streams the archive once, keeps only regular files with the names written by report and bounds both the compressed and the decompressed amount of data it is willing to process.

pysnap.report.archive.read_report_archive(path, limits=None)[source]

Read the known members of a report archive into memory.

Unknown names, links, devices, directories and path-traversal entries are skipped with a diagnostic; their content is never read.

Parameters:
  • path (str | Path) – Path to the report file.

  • limits (ReportLimits | None) – Resource limits, defaults to ReportLimits.

Returns:

Raw archive members.

Raises:

ReportFormatError – When the file is missing, too large, or is not a gzip-compressed tar archive.

Return type:

ReportArchive

pysnap.report.checkfile module

Check files: expected commands, output blocks and grading.

A check file is TOML:

[report]              # optional expectations, reported as warnings
task = 1
host = "first"

[[command]]
id = "addr"           # optional, referenced by ``of`` and ``after``
cmd = "ip a show <ETH-A>"
after = "other-id"    # optional, commands only
points = 1            # optional weight, default 1

[[output]]
of = "addr"           # optional: search only the output of that command
order = "any"         # optional: lines in any order (default: sequence)
min_count = 3         # optional: the block must occur this many times
text = '''
inet <IP-A>/<MASK> scope global <ETH-A>
'''

[grading]
total = 10
scale = [[90, "5"], [75, "4"], [50, "3"], [0, "2"]]

Parameters: <IP>, <ETH> and <MASK> match any value of their type; <IP-A>-style labels bind the first matched value and require it later; any other name such as <X> binds one word. <*> matches any text inside a line and a line holding only ... matches any number of lines. Whitespace is normalized on both sides.

exception pysnap.report.checkfile.CheckFileError[source]

Bases: PySnapError

Report a check file that cannot be used.

class pysnap.report.checkfile.Parameter(kind, name)[source]

Bases: object

Describe one <...> placeholder of a pattern.

Parameters:
  • kind (str) – "IP", "ETH", "MASK" or "WORD" for free names.

  • name (str | None) – Binding name, None for unlabeled typed parameters.

kind: str
name: str | None
class pysnap.report.checkfile.Pattern(source, parts)[source]

Bases: object

Represent one line pattern split into literal text and parameters.

Parameters:
  • source (str) – Pattern text as written, whitespace-normalized.

  • parts (tuple[str | Parameter | None, ...]) – Literal strings, Parameter objects and None for <*>, in order.

source: str
parts: tuple[str | Parameter | None, ...]
property names: tuple[str, ...][source]

Return the binding names used by the pattern.

match(text, bindings)[source]

Match a whitespace-normalized line.

Parameters:
  • text (str) – Normalized line.

  • bindings (dict[str, str]) – Values already bound by earlier matches.

Returns:

Bindings including new values, or None without match.

Return type:

dict[str, str] | None

class pysnap.report.checkfile.CommandCheck(number, id, pattern, after, points)[source]

Bases: object

Represent one [[command]] item.

Parameters:
  • number (int)

  • id (str | None)

  • pattern (Pattern)

  • after (str | None)

  • points (float)

number: int
id: str | None
pattern: Pattern
after: str | None
points: float
class pysnap.report.checkfile.OutputCheck(number, of, lines, order, min_count, points)[source]

Bases: object

Represent one [[output]] item.

Parameters:
  • lines (tuple[Pattern | None, ...]) – Line patterns; None stands for a ... gap.

  • number (int)

  • of (str | None)

  • order (str)

  • min_count (int)

  • points (float)

number: int
of: str | None
lines: tuple[Pattern | None, ...]
order: str
min_count: int
points: float
class pysnap.report.checkfile.GradeStep(threshold, mark)[source]

Bases: object

Represent one step of the grading scale.

Parameters:
  • threshold (float)

  • mark (str)

threshold: float
mark: str
class pysnap.report.checkfile.CheckSpec(source_name, expected_task, expected_host, commands, outputs, total, scale)[source]

Bases: object

Represent a loaded check file.

Parameters:
  • source_name (str)

  • expected_task (int | None)

  • expected_host (str | None)

  • commands (tuple[CommandCheck, ...])

  • outputs (tuple[OutputCheck, ...])

  • total (float)

  • scale (tuple[GradeStep, ...])

source_name: str
expected_task: int | None
expected_host: str | None
commands: tuple[CommandCheck, ...]
outputs: tuple[OutputCheck, ...]
total: float
scale: tuple[GradeStep, ...]
pysnap.report.checkfile.normalize_spacing(text)[source]

Trim a line and collapse whitespace runs to single spaces.

Parameters:

text (str) – Raw line.

Returns:

Normalized line.

Return type:

str

pysnap.report.checkfile.compile_pattern(text, where='pattern')[source]

Compile one line pattern.

Parameters:
  • text (str) – Pattern line.

  • where (str) – Location used in error messages.

Returns:

Compiled pattern.

Raises:

CheckFileError – For an unknown <NAME-label> form.

Return type:

Pattern

pysnap.report.checkfile.load_check(path)[source]

Load and validate a check file.

Parameters:

path (str | Path) – Path to the TOML check file.

Returns:

Validated check specification.

Raises:

CheckFileError – When the file is missing, is not valid TOML or does not follow the check file format.

Return type:

CheckSpec

pysnap.report.checkfile.parse_check(data, source_name)[source]

Validate parsed check file data.

Parameters:
  • data (dict[str, Any]) – Parsed TOML document.

  • source_name (str) – File name used in messages.

Returns:

Validated check specification.

Raises:

CheckFileError – When the data does not follow the format.

Return type:

CheckSpec

pysnap.report.compare module

Pairwise comparison of session reports.

pysnap report compare looks for signs that reports of different students share their origin. Every pair of the given reports is compared; deciding which reports belong to the same student is left to the teacher. The signals only point at pairs worth a look and never decide anything.

Strong signals:

  • S1 the report files are identical (SHA-256 of the archives);

  • S2 the recorded output is identical (SHA-256 of the output body);

  • S3 both outputs show the same MAC address, except the all-zero and broadcast addresses and group (multicast) addresses;

  • S4 the same START_TIME (to the second) and DURATION (to the microsecond).

Medium signals:

  • M1 at least 20 identical consecutive keyboard input delays, compared to the microsecond: a copied or edited timing log;

  • M2 values that are random in practice: at least three consecutive ping round-trip times (time=... ms) in the same order, or at least three shared tcpdump timestamps with microseconds. ping prints only three significant digits, so single values often match by chance, while a run in the same order does not;

  • M3 byte-identical CPU.txt (it includes the BogoMIPS value measured at boot);

  • M4 at least two shared erroneous commands, whose output reports command not found or No such file.

Weak signal:

  • W1 the same CPU model.

Signals implied by a stronger one of the same pair are not repeated: an identical file (S1) implies all others, and an identical CPU.txt (M3) implies the same CPU model (W1).

class pysnap.report.compare.CompareSignal(code, level, description)[source]

Bases: object

Represent one signal found for a pair of reports.

Parameters:
  • code (str) – Signal code such as "S3".

  • level (str) – "strong", "medium" or "weak".

  • description (str) – Human-readable description with the shared values.

code: str
level: str
description: str
class pysnap.report.compare.PairResult(first, second, signals)[source]

Bases: object

Represent the comparison of two reports.

Parameters:
  • first (str) – Path of the first report as found.

  • second (str) – Path of the second report as found.

  • signals (tuple[CompareSignal, ...]) – Signals found, strongest first.

first: str
second: str
signals: tuple[CompareSignal, ...]
property level: int[source]

Return the rank of the strongest signal, len(LEVELS) for none.

class pysnap.report.compare.CompareResult(reports, pairs, warnings)[source]

Bases: object

Represent the comparison of a set of reports.

Parameters:
  • reports (tuple[str, ...]) – Paths of the compared reports.

  • pairs (tuple[PairResult, ...]) – Pairs with signals, strongest first, then pairs without.

  • warnings (tuple[str, ...]) – Paths that were skipped and why.

reports: tuple[str, ...]
pairs: tuple[PairResult, ...]
warnings: tuple[str, ...]
class pysnap.report.compare.ReportFingerprint(path, file_sha256, output_sha256, macs, start, delay_windows, ping_times, tcpdump_times, error_commands, cpu_info, cpu_model)[source]

Bases: object

Hold the values of one report that the signals compare.

Parameters:
  • path (str) – Path of the report as found.

  • file_sha256 (str) – SHA-256 of the report file.

  • output_sha256 (str | None) – SHA-256 of the output body, None when empty.

  • macs (frozenset[str]) – Unicast MAC addresses seen in the output.

  • start (tuple[str, int] | None) – START_TIME to the second and DURATION in microseconds, when both are known.

  • delay_windows (dict[int, int]) – Hash of every run of DELAY_RUN consecutive input delays in microseconds, mapped to its first position. Runs of one repeated delay are left out.

  • ping_times (tuple[str, ...]) – ping round-trip times in output order.

  • tcpdump_times (frozenset[str]) – tcpdump timestamps.

  • error_commands (frozenset[str]) – Commands whose output reports an error.

  • cpu_info (str) – Content of CPU.txt.

  • cpu_model (str | None) – CPU model from CPU.txt.

path: str
file_sha256: str
output_sha256: str | None
macs: frozenset[str]
start: tuple[str, int] | None
delay_windows: dict[int, int]
ping_times: tuple[str, ...]
tcpdump_times: frozenset[str]
error_commands: frozenset[str]
cpu_info: str
cpu_model: str | None
pysnap.report.compare.collect_report_paths(paths)[source]

Expand files and directories into the list of report files.

Directories are searched recursively for files named report.NN.HOST; files given explicitly are used whatever their name. A file reached twice is compared once.

Parameters:

paths (Iterable[str | Path]) – Files and directories.

Returns:

Report paths and warnings for paths that gave no reports.

Return type:

tuple[list[Path], list[str]]

pysnap.report.compare.fingerprint_report(path, recording, transcript)[source]

Collect the compared values of one report.

Parameters:
  • path (str | Path) – Path of the report file.

  • recording (Recording) – Parsed report.

  • transcript (Transcript) – Rendered report.

Returns:

Report fingerprint.

Return type:

ReportFingerprint

pysnap.report.compare.compare_pair(first, second)[source]

Compare two reports.

Parameters:
Returns:

The pair with its signals, strongest first.

Return type:

PairResult

pysnap.report.compare.compare_fingerprints(fingerprints)[source]

Compare every pair of reports.

Parameters:

fingerprints (Sequence[ReportFingerprint]) – Reports in input order.

Returns:

Pairs with signals, strongest first, then pairs without.

Return type:

tuple[PairResult, …]

pysnap.report.compare.compare_reports(paths, limits=None)[source]

Load, fingerprint and compare reports.

Parameters:
  • paths (Iterable[str | Path]) – Report files and directories.

  • limits (ReportLimits | None) – Resource limits for reading each report.

Returns:

Comparison result; reports is empty when none could be read.

Return type:

CompareResult

pysnap.report.control module

Control channel between pysnap report extract and pysnap connect.

VirtualBox serves one client on a UART1 TCP port, so while pysnap connect is attached only that process can talk to the console. The connect process listens on 127.0.0.1 on a random port and stores the port and a random secret in its session record, which only the current user can read. pysnap report extract sends one JSON request line with the secret and the quoted file path; the connect process runs the transfer protocol of pysnap.report.extract on its own serial connection and answers with one JSON line holding the file as base64 and its checksum, or an error message.

class pysnap.report.control.ControlServer(run_transfer)[source]

Bases: object

Serve transfer requests for one pysnap connect session.

Parameters:

run_transfer (TransferRunner)

async start()[source]

Listen on a random local port.

Returns:

The port number.

Return type:

int

async close()[source]

Stop listening.

Return type:

None

pysnap.report.control.request_transfer(port, token, remote_path, *, max_bytes=16777216, connect_timeout=5.0)[source]

Ask an attached pysnap connect process to transfer a file.

Parameters:
  • port (int) – Control port from the session record.

  • token (str) – Control secret from the session record.

  • remote_path (str) – Quoted shell path of the file.

  • max_bytes (int) – Largest accepted file size.

  • connect_timeout (float) – Seconds to wait for the control port.

Returns:

File content and its SHA-256 checksum.

Raises:

ExtractError – When the session does not answer or the transfer fails.

Return type:

tuple[bytes, str]

pysnap.report.extract module

Copy a report file from a running VM through its serial console or ssh.

A VM reached through ssh gets a separate ssh session that runs one command: it prints the sha256sum of the file and then the file itself. The student’s terminal is not touched, so this works while pysnap connect is attached. Every other VM uses its serial console.

The serial transfer uses only the guest shell and coreutils:

  1. The VM must be running and expose UART1 as a TCP server. VirtualBox serves one client, so when pysnap connect is attached the transfer is delegated to that process through its control channel (pysnap.report.control), which runs the same protocol.

  2. PySnap wakes the console with Enter and refuses to continue when the prompt installed by report is visible: a recording is running and every further command would end up in the student’s report.

  3. echo PYSNAP_$((20+22)) must answer PYSNAP_42. The echoed command line cannot contain that text, so the answer proves a working shell and not a login screen or a full-screen program.

  4. One command prints a begin marker, the file as one base64 line, its sha256sum and an end marker. The markers are split in the command so that its echo never looks like a marker.

  5. PySnap decodes the data, compares the checksum, writes the file atomically and clears the guest screen.

Service commands start with a space, so shells with HISTCONTROL set to ignorespace or ignoreboth keep them out of the history.

exception pysnap.report.extract.ExtractError[source]

Bases: PySnapError

Report a file transfer from a VM that could not be completed.

class pysnap.report.extract.ExtractResult(vm_name, remote_path, destination, size, sha256)[source]

Bases: object

Describe a completed transfer.

Parameters:
  • vm_name (str) – Source VM.

  • remote_path (str) – Shell path of the file inside the VM.

  • destination (Path) – File written on the host.

  • size (int) – Number of bytes transferred.

  • sha256 (str) – Checksum confirmed by the guest and the host.

vm_name: str
remote_path: str
destination: Path
size: int
sha256: str
pysnap.report.extract.extract_file(service, vm_name, name, *, destination=None, force=False, connector=None, connect_timeout=10.0, shell_timeout=5.0, idle_timeout=10.0, max_bytes=16777216)[source]

Copy one file from a running VM to the host.

Parameters:
  • service – PySnap service used to inspect the VM and its sessions.

  • vm_name (str) – VM to copy from.

  • name (str) – File name in the root home directory, or a path with /.

  • destination (Path | None) – Host path; defaults to the file name in the current directory.

  • force (bool) – Whether an existing destination may be replaced.

  • connector (Callable[[str, int, float], socket] | None) – Opens the serial TCP socket; for tests.

  • connect_timeout (float) – Seconds to wait for the serial TCP port.

  • shell_timeout (float) – Seconds to wait for the shell check answer.

  • idle_timeout (float) – Seconds without data after which the transfer fails.

  • max_bytes (int) – Largest accepted file size.

Returns:

Transfer details.

Raises:

ExtractError – When any precondition or the transfer fails.

Return type:

ExtractResult

pysnap.report.extract.run_transfer(console, vm_name, remote_path, *, shell_timeout=5.0, idle_timeout=10.0, max_bytes=16777216)[source]

Run the transfer protocol on a serial console.

Parameters:
  • console (ConsoleIO) – Console transport.

  • vm_name (str) – VM name used in messages.

  • remote_path (str) – Quoted shell path of the file.

  • shell_timeout (float) – Seconds to wait for the shell check answer.

  • idle_timeout (float) – Seconds without data after which the transfer fails.

  • max_bytes (int) – Largest accepted file size.

Returns:

File content and its SHA-256 checksum.

Raises:

ExtractError – When the console is not usable or the transfer fails.

Return type:

tuple[bytes, str]

pysnap.report.extract.ssh_transfer(service, vm_name, remote_path, *, max_bytes=16777216, timeout=120.0)[source]

Copy a file through a separate ssh session with the PySnap key.

Parameters:
  • service – PySnap service; gives the ssh target and the key store.

  • vm_name (str) – VM to copy from.

  • remote_path (str) – Quoted shell path of the file.

  • max_bytes (int) – Largest accepted file size.

  • timeout (float) – Seconds the whole transfer may take.

Returns:

File content and its SHA-256 checksum.

Raises:

ExtractError – When ssh fails or the file cannot be read.

Return type:

tuple[bytes, str]

pysnap.report.extract.remote_shell_path(name)[source]

Return the quoted shell path of a file in the VM.

Parameters:

name (str) – File name, looked up in the home directory, or a path.

Returns:

Shell expression for the path.

Raises:

ExtractError – For empty names or names with control characters.

Return type:

str

class pysnap.report.extract.ConsoleIO(*args, **kwargs)[source]

Bases: Protocol

Console transport used by run_transfer().

buffer: bytes
send(data)[source]

Send bytes and start a new response buffer.

Parameters:

data (bytes)

Return type:

None

exchange(data, *, quiet, limit)[source]

Send bytes and collect output until it stays quiet.

Parameters:
  • data (bytes)

  • quiet (float)

  • limit (float)

Return type:

bytes

read_until(done, timeout, *, idle=False)[source]

Collect output until done holds.

Parameters:
  • done (Callable[[bytes], bool])

  • timeout (float)

  • idle (bool)

Return type:

bool

class pysnap.report.extract.SocketConsole(connection)[source]

Bases: _BufferedConsole

Console on a blocking serial TCP socket.

Parameters:

connection (socket.socket)

class pysnap.report.extract.QueueConsole(write)[source]

Bases: _BufferedConsole

Console fed by another thread, such as the pysnap connect loop.

The owner calls feed() with guest output and close() when the serial connection ends; write must be safe to call from the transfer thread.

Parameters:

write (Callable[[bytes], None])

feed(data)[source]

Hand guest output to the transfer.

Parameters:

data (bytes) – Received bytes.

Return type:

None

close()[source]

Signal that the serial connection has ended.

Return type:

None

pysnap.report.highlight module

Highlighting of prompts and entered commands in a transcript.

The report prompt is recorded without colors, so PySnap styles its parts like a typical colored bash prompt and shows entered commands in bold. The result is a list of styled runs per line that any front end can draw.

pysnap.report.highlight.highlight_transcript(transcript)[source]

Return the transcript lines with highlighted prompts and commands.

Lines without a command keep the styles produced by the guest.

Parameters:

transcript (Transcript) – Rendered session.

Returns:

Styled runs for every transcript line.

Return type:

list[tuple[StyledRun, …]]

pysnap.report.matcher module

Match a check file against a rendered report and grade the result.

Items are checked in file order: all [[command]] items, then all [[output]] items, so labels bound by commands reach the output blocks. A backtracking search chooses the matches: for every item it tries the candidates in report order and also the possibility that the item fails, and keeps the assignment with the most points; ties keep the earliest choices, so the result is deterministic. A failing item therefore never takes the labels of later items down with it.

class pysnap.report.matcher.ItemResult(kind, number, label, pattern, passed, points, command=None, lines=(), count=0, reason=None, closest=None)[source]

Bases: object

Represent the outcome of one check item.

Parameters:
  • kind (str) – "command" or "output".

  • number (int) – Position among the items of its kind, starting at 1.

  • label (str) – Item id or its first pattern line.

  • pattern (str) – Pattern text shown to the reader.

  • passed (bool) – Whether the item passed.

  • points (float) – Weight of the item.

  • command (int | None) – Index of the matched report command, for commands.

  • lines (tuple[int, ...]) – Transcript lines of the first match, for outputs.

  • count (int) – Number of block occurrences found, for outputs.

  • reason (str | None) – Explanation of a failure.

  • closest (str | None) – Closest entered command for a failed command item.

kind: str
number: int
label: str
pattern: str
passed: bool
points: float
command: int | None = None
lines: tuple[int, ...] = ()
count: int = 0
reason: str | None = None
closest: str | None = None
class pysnap.report.matcher.CheckResult(check_name, items, bindings, earned_weight, total_weight, percent, points, total_points, mark, warnings, search_exhausted=False)[source]

Bases: object

Represent the graded result of a check file.

Parameters:
  • check_name (str)

  • items (tuple[ItemResult, ...])

  • bindings (dict[str, str])

  • earned_weight (float)

  • total_weight (float)

  • percent (float)

  • points (float)

  • total_points (float)

  • mark (str | None)

  • warnings (tuple[str, ...])

  • search_exhausted (bool)

check_name: str
items: tuple[ItemResult, ...]
bindings: dict[str, str]
earned_weight: float
total_weight: float
percent: float
points: float
total_points: float
mark: str | None
warnings: tuple[str, ...]
search_exhausted: bool = False
pysnap.report.matcher.run_check(spec, transcript, *, report_name=None, budget=200000)[source]

Check a transcript against a check file and grade it.

Parameters:
  • spec (CheckSpec) – Loaded check file.

  • transcript (Transcript) – Rendered report.

  • report_name (ReportName | None) – Task and host from the report file name, used for the [report] expectations.

  • budget (int) – Largest number of search steps before the best assignment found so far is used.

Returns:

Graded result.

Return type:

CheckResult

pysnap.report.models module

Data contracts shared by the report reading modules.

class pysnap.report.models.ReportLimits(max_archive_bytes=16777216, max_member_bytes=67108864, max_unpacked_bytes=134217728)[source]

Bases: object

Bound the resources spent on one untrusted report archive.

Parameters:
  • max_archive_bytes (int) – Largest accepted compressed archive size.

  • max_member_bytes (int) – Largest accepted size of one archive member.

  • max_unpacked_bytes (int) – Largest amount of decompressed data read from the archive, including skipped members and tar headers.

max_archive_bytes: int = 16777216
max_member_bytes: int = 67108864
max_unpacked_bytes: int = 134217728
class pysnap.report.models.ReportDiagnostic(code, message)[source]

Bases: object

Describe one non-fatal problem found in a report.

Parameters:
  • code (str) – Stable machine-readable problem identifier.

  • message (str) – Human-readable explanation.

code: str
message: str
class pysnap.report.models.ReportName(task, host)[source]

Bases: object

Represent the task number and host encoded in a report file name.

Parameters:
  • task (int)

  • host (str)

task: int
host: str
classmethod parse(file_name)[source]

Parse a report.<NN>.<host> file name.

Parameters:

file_name (str) – Base name of the report file.

Returns:

Parsed name or None when the name does not follow the report convention.

Return type:

ReportName | None

class pysnap.report.models.ReportArchive(members, mtimes, diagnostics=())[source]

Bases: object

Hold the raw members read from a report archive.

Parameters:
  • members (dict[str, bytes]) – Known member names mapped to their content.

  • mtimes (dict[str, int]) – Known member names mapped to their archive modification time in seconds since the epoch.

  • diagnostics (tuple[ReportDiagnostic, ...]) – Problems found while reading the archive.

members: dict[str, bytes]
mtimes: dict[str, int]
diagnostics: tuple[ReportDiagnostic, ...] = ()
class pysnap.report.models.TimingEvent(kind, delay, time, data)[source]

Bases: object

Represent one input or output chunk of the recorded session.

Parameters:
  • kind (str) – "I" for keyboard input or "O" for terminal output.

  • delay (float) – Seconds elapsed since the previous chunk.

  • time (float) – Seconds elapsed since the start of the recording.

  • data (bytes) – Raw bytes of the chunk.

kind: str
delay: float
time: float
data: bytes
class pysnap.report.models.Recording(source_name, name, headers, start_time, duration, exit_code, columns, lines, events, input_bytes, output_bytes, cpu_info='', mtimes=<factory>, diagnostics=())[source]

Bases: object

Represent a parsed session report.

Parameters:
  • source_name (str) – Base name of the report file.

  • name (ReportName | None) – Task and host parsed from the file name, when available.

  • headers (dict[str, str]) – Raw H header values from the timing log.

  • start_time (datetime | None) – Recording start reported by script.

  • duration (float | None) – Recording duration in seconds reported by script.

  • exit_code (int | None) – Exit code of the recorded shell.

  • columns (int | None) – Terminal width at recording start.

  • lines (int | None) – Terminal height at recording start.

  • events (tuple[TimingEvent, ...]) – Input and output chunks in recording order.

  • input_bytes (bytes) – Complete recorded keyboard input.

  • output_bytes (bytes) – Complete recorded terminal output.

  • cpu_info (str) – Content of CPU.txt (lscpu output).

  • mtimes (dict[str, int]) – Archive modification times of the known members.

  • diagnostics (tuple[ReportDiagnostic, ...]) – 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]
diagnostics: tuple[ReportDiagnostic, ...] = ()
class pysnap.report.models.CellStyle(fg='default', bg='default', bold=False, italics=False, underscore=False, strikethrough=False, reverse=False)[source]

Bases: object

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.

Parameters:
  • fg (str)

  • bg (str)

  • bold (bool)

  • italics (bool)

  • underscore (bool)

  • strikethrough (bool)

  • reverse (bool)

fg: str = 'default'
bg: str = 'default'
bold: bool = False
italics: bool = False
underscore: bool = False
strikethrough: bool = False
reverse: bool = False
class pysnap.report.models.StyledRun(text, style=CellStyle(fg='default', bg='default', bold=False, italics=False, underscore=False, strikethrough=False, reverse=False))[source]

Bases: object

Represent consecutive characters that share one style.

Parameters:
text: str
style: CellStyle = CellStyle(fg='default', bg='default', bold=False, italics=False, underscore=False, strikethrough=False, reverse=False)
class pysnap.report.models.TranscriptLine(runs=())[source]

Bases: object

Represent one rendered line of the session transcript.

Parameters:

runs (tuple[StyledRun, ...])

runs: tuple[StyledRun, ...] = ()
property text: str[source]

Return the plain text of the line.

class pysnap.report.models.PromptInfo(user, task, host, directory)[source]

Bases: object

Represent the parts of the prompt installed by report.

report sets PS1="[\u@NN-HOST \W]# ".

Parameters:
  • user (str)

  • task (int)

  • host (str)

  • directory (str)

user: str
task: int
host: str
directory: str
class pysnap.report.models.CommandRecord(index, time, line, column, prompt, text, prompt_info, first_event, enter_event, output_start, output_end)[source]

Bases: object

Represent one input line submitted with Enter.

Parameters:
  • index (int) – Zero-based position among all recorded commands.

  • time (float) – Recording time of the Enter key press in seconds.

  • line (int) – Transcript line where the input starts.

  • column (int) – Column where the input starts, i.e. the prompt width.

  • prompt (str) – Text left of the input on its first line.

  • text (str) – Submitted input as displayed by the shell.

  • prompt_info (PromptInfo | None) – Parsed report prompt, None for other prompts such as vtysh or a remote shell.

  • first_event (int) – Index of the first input event of this line.

  • enter_event (int) – Index of the input event that contained Enter.

  • output_start (int) – First transcript line after the input.

  • output_end (int) – 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
class pysnap.report.models.Transcript(lines, commands, diagnostics=())[source]

Bases: object

Represent the rendered session: final text and recognized commands.

Parameters:
lines: tuple[TranscriptLine, ...]
commands: tuple[CommandRecord, ...]
diagnostics: tuple[ReportDiagnostic, ...] = ()

pysnap.report.player module

Timed replay of a recording for pysnap report show.

The player runs without any user interface: it owns a terminal emulator of the recorded size and moves it to any moment of the recording. Playback uses a compressed timeline in which every pause is limited to max_delay seconds, like scriptreplay -m. Seeking backwards replays from the start, which is fast because the output is only parsed, never drawn.

class pysnap.report.player.ReplayPlayer(recording, transcript, *, max_delay=1.0, max_screen_size=1000, history=10000)[source]

Bases: object

Move a terminal emulator along the timeline of a recording.

Parameters:
  • recording (Recording)

  • transcript (Transcript)

  • max_delay (float | None)

  • max_screen_size (int)

  • history (int)

property finished: bool[source]

Return whether the player stands at the end of the recording.

property current_command: int[source]

Return how many commands have started up to the current moment.

seek(time)[source]

Show the screen as it was at a moment of the compressed timeline.

Parameters:

time (float) – Target moment in seconds, clamped to the recording.

Return type:

None

next_command_time()[source]

Return the start of the first command after the current moment.

Return type:

float | None

previous_command_time()[source]

Return the start of the command before the current one, or 0.

Return type:

float

class pysnap.report.player.PlaybackController(player, speed=1.0)[source]

Bases: object

Hold the playback state driven by the show key bindings.

Parameters:
property speed: float[source]

Return the current playback speed.

tick(seconds)[source]

Advance playback by real elapsed time.

Parameters:

seconds (float) – Real seconds since the previous tick.

Return type:

None

toggle()[source]

Pause or resume; resuming at the end starts over.

Return type:

None

faster()[source]

Select the next higher speed.

Return type:

None

slower()[source]

Select the next lower speed.

Return type:

None

next_command()[source]

Jump to the start of the next command, or to the end.

Return type:

None

previous_command()[source]

Jump to the start of the previous command, or to the start.

Return type:

None

to_start()[source]

Jump to the start of the recording.

Return type:

None

to_end()[source]

Jump to the end of the recording and pause.

Return type:

None

pysnap.report.recording module

Parsing of the script recording stored in a report archive.

report runs script -I IN.txt -O OUT.txt -B BOTH.txt -T TIME.txt. TIME.txt uses the multi-stream timing format of util-linux script: one entry per line, <type> <delay> <value>, where H lines carry headers, I/O lines carry the byte size of an input or output chunk and S lines carry signals such as terminal resizes. The byte counts address the stream files after their Script started on ... line and before their Script done on ... trailer.

pysnap.report.recording.load_report(path, limits=None)[source]

Load and parse a report file.

Parameters:
  • path (str | Path) – Path to the report file.

  • limits (ReportLimits | None) – Resource limits, defaults to ReportLimits.

Returns:

Parsed recording.

Raises:

ReportFormatError – When the file is not a usable report.

Return type:

Recording

pysnap.report.recording.parse_recording(archive, source_name)[source]

Parse the members of a report archive.

Inconsistencies are reported as diagnostics; only a report without a timing log or an output log is rejected.

Parameters:
  • archive (ReportArchive) – Raw archive members.

  • source_name (str) – Base name of the report file.

Returns:

Parsed recording.

Raises:

ReportFormatError – When TIME.txt or OUT.txt is missing or the timing log uses the unsupported single-stream format.

Return type:

Recording

pysnap.report.render module

Replay a recording through pyte to obtain text and typed commands.

The rendered transcript is what a reader would have seen: every line that scrolled off the screen or was wiped by clear, followed by the final screen. Command lines are recovered from the screen rather than from the raw keystrokes, because the keystrokes contain line editing (Backspace, cursor keys, history recall) that only the shell resolves.

A command is captured when Enter was pressed and the shell moves to the next line: the output chunk after Enter is fed up to its first line feed, and the logical input line is read from the screen at that moment. The input starts where the cursor stood at the first keystroke of the line, so prompts of other programs (vtysh, a remote shell) are handled the same way as the prompt installed by report.

pysnap.report.render.render_transcript(recording, *, max_lines=200000, max_screen_size=1000)[source]

Replay a recording and return its transcript and commands.

Parameters:
  • recording (Recording) – Parsed recording.

  • max_lines (int) – Largest number of transcript lines to keep.

  • max_screen_size (int) – Largest accepted screen width and height.

Returns:

Rendered transcript.

Return type:

Transcript

pysnap.report.render.parse_resize(data)[source]

Parse a SIGWINCH ROWS=<n> COLS=<n> signal entry.

Parameters:

data (bytes) – Signal description from an S timing entry.

Returns:

(rows, columns) or None for other or invalid signals.

Return type:

tuple[int, int] | None

pysnap.report.render.parse_prompt(prompt)[source]

Parse the prompt installed by report.

Parameters:

prompt (str) – Prompt text without trailing spaces.

Returns:

Prompt parts or None for any other prompt.

Return type:

PromptInfo | None

pysnap.report.stats module

Report information and statistics for pysnap report check.

Every value comes directly from the report: the script headers, the archive member times, CPU.txt, the timed keystrokes and the rendered transcript.

class pysnap.report.stats.CommandStats(index, text, pause, typing_time, keys, backspaces, from_history, interrupted, foreign_prompt)[source]

Bases: object

Represent the statistics of one entered command.

Parameters:
  • index (int) – Zero-based command position.

  • text (str) – Entered command.

  • pause (float) – Seconds from the last output before the command to its first keystroke.

  • typing_time (float) – Seconds from the first keystroke to Enter.

  • keys (int) – Keystrokes from the first key through Enter.

  • backspaces (int) – Backspace keystrokes among keys.

  • from_history (bool) – Whether history keys (Up, Down, Ctrl-R) were used.

  • interrupted (bool) – Whether Ctrl-C was typed while it ran.

  • foreign_prompt (bool) – Whether it was typed at a prompt other than the one installed by report.

index: int
text: str
pause: float
typing_time: float
keys: int
backspaces: int
from_history: bool
interrupted: bool
foreign_prompt: bool
class pysnap.report.stats.PasteEvent(time, text, command)[source]

Bases: object

Represent an input chunk that looks like pasted text.

Parameters:
  • time (float) – Recording time of the chunk in seconds.

  • text (str) – Printable text of the chunk.

  • command (int | None) – Index of the command the chunk belongs to, if any.

time: float
text: str
command: int | None
class pysnap.report.stats.MemberTimeCheck(member, expected, difference, within_tolerance)[source]

Bases: object

Compare the archive time of a member with the recording time.

Parameters:
  • member (str) – Archive member name.

  • expected (str) – "start" or "end" of the recording.

  • difference (float) – Member time minus expected time in seconds.

  • within_tolerance (bool) – Whether difference is within the tolerance.

member: str
expected: str
difference: float
within_tolerance: bool
class pysnap.report.stats.ReportStats(source_name, file_task, file_host, prompt_identities, start_time, end_time, duration, exit_code, tty, term, columns, lines, cpu_model, hypervisor, commands, unique_commands, pause_min, pause_median, pause_max, longest_pause_command, typing_speed, backspace_share, pastes, ip_addresses, mac_addresses, member_times, diagnostics, warnings)[source]

Bases: object

Represent the information block of pysnap report check.

Parameters:
  • source_name (str)

  • file_task (int | None)

  • file_host (str | None)

  • prompt_identities (tuple[tuple[int, str], ...])

  • start_time (datetime | None)

  • end_time (datetime | None)

  • duration (float | None)

  • exit_code (int | None)

  • tty (str | None)

  • term (str | None)

  • columns (int | None)

  • lines (int | None)

  • cpu_model (str | None)

  • hypervisor (str | None)

  • commands (tuple[CommandStats, ...])

  • unique_commands (int)

  • pause_min (float | None)

  • pause_median (float | None)

  • pause_max (float | None)

  • longest_pause_command (str | None)

  • typing_speed (float | None)

  • backspace_share (float | None)

  • pastes (tuple[PasteEvent, ...])

  • ip_addresses (tuple[str, ...])

  • mac_addresses (tuple[str, ...])

  • member_times (tuple[MemberTimeCheck, ...])

  • diagnostics (tuple[ReportDiagnostic, ...])

  • warnings (tuple[str, ...])

source_name: str
file_task: int | None
file_host: str | None
prompt_identities: tuple[tuple[int, str], ...]
start_time: datetime | None
end_time: datetime | None
duration: float | None
exit_code: int | None
tty: str | None
term: str | None
columns: int | None
lines: int | None
cpu_model: str | None
hypervisor: str | None
commands: tuple[CommandStats, ...]
unique_commands: int
pause_min: float | None
pause_median: float | None
pause_max: float | None
longest_pause_command: str | None
typing_speed: float | None
backspace_share: float | None
pastes: tuple[PasteEvent, ...]
ip_addresses: tuple[str, ...]
mac_addresses: tuple[str, ...]
member_times: tuple[MemberTimeCheck, ...]
diagnostics: tuple[ReportDiagnostic, ...]
warnings: tuple[str, ...]
pysnap.report.stats.compute_stats(recording, transcript, *, paste_min_characters=5, mtime_tolerance=2.0)[source]

Compute the information block of a report.

Parameters:
  • recording (Recording) – Parsed recording.

  • transcript (Transcript) – Rendered transcript.

  • paste_min_characters (int) – Printable characters in one input chunk from which the chunk counts as pasted.

  • mtime_tolerance (float) – Accepted difference in seconds between archive member times and the recording start or end.

Returns:

Report statistics.

Return type:

ReportStats

pysnap.report.stats.split_keys(data)[source]

Split raw keyboard input into keystrokes.

Escape sequences such as cursor keys count as one keystroke; a chunk may hold several keystrokes because the serial console groups fast typing.

Parameters:

data (bytes) – Raw input bytes.

Returns:

One item per keystroke.

Return type:

list[bytes]

pysnap.report.stats.transcript_output_text(transcript)[source]

Return the transcript text without the lines of entered commands.

Parameters:

transcript (Transcript) – Rendered report.

Returns:

Output lines joined with newlines.

Return type:

str

pysnap.report.stats.parse_cpu_info(cpu_info)[source]

Parse lscpu output into a field mapping.

Parameters:

cpu_info (str) – Content of CPU.txt.

Returns:

Field names mapped to their first value.

Return type:

dict[str, str]

pysnap.report.viewer module

Interactive viewers for reports: the text pager and the show player.

Both viewers draw only styled text produced by PySnap itself (transcript runs or the pyte screen of the player), so nothing from a report reaches the reader’s terminal as a raw control sequence.

class pysnap.report.viewer.PagerState(line_count, height=1, *, line_width=0, width=80)[source]

Bases: object

Track the scroll position of the text pager.

Parameters:
  • line_count (int)

  • height (int)

  • line_width (int)

  • width (int)

property max_offset: int[source]

Return the offset that shows the last page.

set_height(height)[source]

Adapt to a new window height.

Parameters:

height (int) – Number of visible lines.

Return type:

None

property max_column: int[source]

Return the column offset that shows the end of the longest line.

set_width(width)[source]

Adapt to a new window width.

Parameters:

width (int) – Number of visible columns.

Return type:

None

scroll_right()[source]

Shift the view right by half the window width, like less.

Return type:

None

scroll_left()[source]

Shift the view left by half the window width, like less.

Return type:

None

scroll(lines)[source]

Scroll by a number of lines; negative values scroll up.

Parameters:

lines (int) – Line count to move.

Return type:

None

page_down()[source]

Scroll one page down.

Return type:

None

page_up()[source]

Scroll one page up.

Return type:

None

to_top()[source]

Show the first page.

Return type:

None

to_bottom()[source]

Show the last page.

Return type:

None

visible_range()[source]

Return the indices of the visible lines.

Return type:

range

pysnap.report.viewer.clip_runs(runs, column, width)[source]

Cut a line to the visible columns, marking hidden text like less -S.

A < in the first visible column means text continues to the left, a > in the last visible column means text continues to the right.

Parameters:
  • runs (Sequence[StyledRun]) – Styled runs of one line.

  • column (int) – First visible column.

  • width (int) – Number of visible columns.

Returns:

Styled runs of the visible part.

Return type:

list[StyledRun]

pysnap.report.viewer.style_runs_to_fragments(runs)[source]

Convert styled runs into prompt_toolkit fragments.

Parameters:

runs (Sequence[StyledRun]) – Styled runs of one line.

Returns:

(style, text) fragments.

Return type:

list[tuple[str, str]]

pysnap.report.viewer.cell_style_to_prompt_toolkit(style)[source]

Translate a CellStyle into a prompt_toolkit style string.

Parameters:

style (CellStyle) – Cell style.

Returns:

prompt_toolkit style string.

Return type:

str

pysnap.report.viewer.run_pager(title, lines)[source]

Show styled lines in a full-screen scrollable pager.

Long lines are cut at the window edge like less -S; < and > mark hidden text. Keys: Up/Down or k/j scroll a line, PageUp/PageDown, b and Space scroll a page, g/Home and G/End jump to the start or end, Left/Right shift the view by half the window width, q or Ctrl-Q quit; the mouse wheel scrolls on Linux.

Parameters:
  • title (str) – Name shown in the status line.

  • lines (Sequence[Sequence[StyledRun]]) – Styled runs of every line.

Return type:

None

pysnap.report.viewer.screen_viewport(rows, cursor_row, visible_rows)[source]

Choose the screen rows shown when the window is lower than the screen.

The viewport keeps the cursor row visible and prefers the bottom of the screen, where new output appears.

Parameters:
  • rows (int) – Number of rows of the recorded screen.

  • cursor_row (int) – Row of the cursor.

  • visible_rows (int) – Number of rows that fit into the window.

Returns:

Indices of the rows to show.

Return type:

range

pysnap.report.viewer.split_rows(fragments)[source]

Split screen fragments into rows at their "\n" separators.

Parameters:

fragments (list[tuple[str, str]]) – Fragments of a whole screen.

Returns:

Fragments per row.

Return type:

list[list[tuple[str, str]]]

pysnap.report.viewer.format_player_status(controller, title, clipped)[source]

Build the status line of the show player.

Parameters:
  • controller (PlaybackController) – Playback state.

  • title (str) – Report name.

  • clipped (bool) – Whether the window is smaller than the recorded screen.

Returns:

Status text.

Return type:

str

pysnap.report.viewer.run_player(controller, title)[source]

Play a recording in a full-screen window.

Keys follow the show decision: Space pauses and resumes, + and - change the speed, n/p jump between commands, Home/End jump to the start or end, q or Ctrl-Q quit. While paused, Alt+Up/Alt+Down and the mouse wheel scroll the screen history.

Parameters:
  • controller (PlaybackController) – Playback state with its player.

  • title (str) – Report name shown in the status line.

Return type:

None