Skip to content

Parsing & Diagnostics

Detailed references for the parse functions and issue diagnostics.

read_cl2

read_cl2(
    source: Source,
    *,
    strict: bool = False,
    encoding: str = "cp1252",
    errors: str = "replace",
) -> Iterator[MeetArchive]

Parse .cl2 / SDIF v3 files, yielding one :class:MeetArchive per source.

Parsing is lazy: files are read and parsed one at a time as the iterator is consumed, so a large corpus is never held in memory all at once. To materialize everything, drain the iterator (e.g. meets = [m for arc in read_cl2(...) for m in arc.meets]).

Per the SDIF spec a "no time" is a blank field or a status code (NT/NS/DNF/DQ/SCR) — never 0.00 — so a missing time surfaces as time=None with a non-OK :class:~tunas.ResultStatus (Hy-Tek .hy3 instead uses a 0.00 sentinel; see :func:read_hy3).

Parameters:

Name Type Description Default
source Source

File path, directory (walked recursively for *.cl2), iterable of paths, or an open text stream. A stream yields exactly one archive (source="<stream>").

required
strict bool

If True, raises ParseError on the first recovered/skipped warning. Otherwise, parsing is lenient. Fatal M1 structural violations always raise.

False
encoding str

Text encoding to use when opening file paths.

'cp1252'
errors str

Error handling scheme for decoding errors.

'replace'

Yields:

Type Description
MeetArchive

class:MeetArchive objects in source order — one per file/stream.

Raises:

Type Description
ParseError

During iteration, on a fatal structural violation, or in strict mode on any parse warning. The earliest failing source raises first.

Source code in src/tunas/parser.py
def read_cl2(
    source: Source,
    *,
    strict: bool = False,
    encoding: str = "cp1252",
    errors: str = "replace",
) -> Iterator[MeetArchive]:
    """Parse `.cl2` / SDIF v3 files, yielding one :class:`MeetArchive` per source.

    Parsing is lazy: files are read and parsed one at a time as the iterator is
    consumed, so a large corpus is never held in memory all at once. To materialize
    everything, drain the iterator (e.g.
    ``meets = [m for arc in read_cl2(...) for m in arc.meets]``).

    Per the SDIF spec a "no time" is a blank field or a status code
    (``NT``/``NS``/``DNF``/``DQ``/``SCR``) — never ``0.00`` — so a missing time
    surfaces as ``time=None`` with a non-OK :class:`~tunas.ResultStatus` (Hy-Tek
    `.hy3` instead uses a ``0.00`` sentinel; see :func:`read_hy3`).

    Args:
        source: File path, directory (walked recursively for `*.cl2`), iterable of paths,
            or an open text stream. A stream yields exactly one archive (``source="<stream>"``).
        strict: If True, raises ParseError on the first recovered/skipped warning.
            Otherwise, parsing is lenient. Fatal M1 structural violations always raise.
        encoding: Text encoding to use when opening file paths.
        errors: Error handling scheme for decoding errors.

    Yields:
        :class:`MeetArchive` objects in source order — one per file/stream.

    Raises:
        ParseError: During iteration, on a fatal structural violation, or in strict
            mode on any parse warning. The earliest failing source raises first.
    """
    return _read(source, _Cl2Engine, ".cl2", strict=strict, encoding=encoding, errors=errors)

read_hy3

read_hy3(
    source: Source,
    *,
    strict: bool = False,
    encoding: str = "cp1252",
    errors: str = "replace",
) -> Iterator[MeetArchive]

Parse Hy-Tek .hy3 result files, yielding one :class:MeetArchive per source.

Only fields confirmed by the reverse-engineered .hy3 specification are parsed into the returned Meet object graph. Iteration and ordering semantics are identical to :func:read_cl2.

Hy-Tek encodes a "no time" entry as the sentinel 0.00, normalized here to time=None (SDIF instead uses a blank field or an NT code; see :func:read_cl2).

Parameters:

Name Type Description Default
source Source

File path, directory (walked recursively for *.hy3), iterable of paths, or an open text stream. A stream yields exactly one archive (source="<stream>").

required
strict bool

If True, raises ParseError on the first recovered/skipped warning. Otherwise, parsing is lenient. Fatal M1 structural violations always raise.

False
encoding str

Text encoding to use when opening file paths.

'cp1252'
errors str

Error handling scheme for decoding errors.

'replace'

Yields:

Type Description
MeetArchive

class:MeetArchive objects in source order — one per file/stream.

Raises:

Type Description
ParseError

During iteration, on a fatal structural violation, or in strict mode on any parse warning. The earliest failing source raises first.

Source code in src/tunas/parser.py
def read_hy3(
    source: Source,
    *,
    strict: bool = False,
    encoding: str = "cp1252",
    errors: str = "replace",
) -> Iterator[MeetArchive]:
    """Parse Hy-Tek `.hy3` result files, yielding one :class:`MeetArchive` per source.

    Only fields confirmed by the reverse-engineered `.hy3` specification are parsed
    into the returned `Meet` object graph. Iteration and ordering semantics are
    identical to :func:`read_cl2`.

    Hy-Tek encodes a "no time" entry as the sentinel ``0.00``, normalized here to
    ``time=None`` (SDIF instead uses a blank field or an ``NT`` code; see
    :func:`read_cl2`).

    Args:
        source: File path, directory (walked recursively for `*.hy3`), iterable of paths,
            or an open text stream. A stream yields exactly one archive (``source="<stream>"``).
        strict: If True, raises ParseError on the first recovered/skipped warning.
            Otherwise, parsing is lenient. Fatal M1 structural violations always raise.
        encoding: Text encoding to use when opening file paths.
        errors: Error handling scheme for decoding errors.

    Yields:
        :class:`MeetArchive` objects in source order — one per file/stream.

    Raises:
        ParseError: During iteration, on a fatal structural violation, or in strict
            mode on any parse warning. The earliest failing source raises first.
    """
    return _read(source, _Hy3Engine, ".hy3", strict=strict, encoding=encoding, errors=errors)

MeetArchive dataclass

MeetArchive(
    source: str,
    meets: list[Meet] = list(),
    report: ParseReport = ParseReport(),
)

The parse result for a single source file (or stream).

A single .cl2/.hy3 file can legitimately hold more than one meet, so meets is a list. report carries the diagnostics and counts for this source only — readers yield one MeetArchive per file, never a merged one.

Attributes:

Name Type Description
source str

File path, or "" for an open text stream.

meets list[Meet]

List of Meet objects parsed from the source.

report ParseReport

Diagnostic and metric report for the parsed source.

ParseReport dataclass

ParseReport(
    warnings: list[ParseWarning] = list(),
    files_read: int = 0,
    meets_parsed: int = 0,
    swimmers_parsed: int = 0,
    individual_swims_parsed: int = 0,
    relays_parsed: int = 0,
    splits_parsed: int = 0,
    records_skipped: int = 0,
    fields_recovered: int = 0,
)

Metrics and warnings for a single parsed source (one file or stream).

Both :func:~tunas.read_cl2 and :func:~tunas.read_hy3 attach one report per :class:~tunas.MeetArchive; use :meth:merge to fold several into a corpus-wide total.

Attributes:

Name Type Description
warnings list[ParseWarning]

List of collected ParseWarning diagnostics.

files_read int

Count of processed files.

meets_parsed int

Count of successfully parsed meets.

swimmers_parsed int

Count of parsed athlete profiles.

individual_swims_parsed int

Count of individual swim results.

relays_parsed int

Count of parsed relay squads.

splits_parsed int

Count of split-time objects parsed.

records_skipped int

Count of records dropped entirely.

fields_recovered int

Count of recovered (nulled) optional fields.

has_warnings property

has_warnings: bool

True if any warnings were collected.

by_severity property

by_severity: dict[Severity, list[ParseWarning]]

Warnings grouped by Severity.

merge

merge(other: ParseReport) -> None

Fold another report into this report: append warnings and sum all counts.

Source code in src/tunas/_parser/diagnostics.py
def merge(self, other: ParseReport) -> None:
    """Fold another report into this report: append warnings and sum all counts."""
    self.warnings.extend(other.warnings)
    for f in fields(self):
        if f.name != "warnings":
            setattr(self, f.name, getattr(self, f.name) + getattr(other, f.name))

warnings_for

warnings_for(
    *,
    record_type: str | None = None,
    field: str | None = None,
    severity: Severity | None = None,
    kind: IssueKind | None = None,
) -> list[ParseWarning]

Warnings filtered by attributes.

Source code in src/tunas/_parser/diagnostics.py
def warnings_for(
    self,
    *,
    record_type: str | None = None,
    field: str | None = None,
    severity: Severity | None = None,
    kind: IssueKind | None = None,
) -> list[ParseWarning]:
    """Warnings filtered by attributes."""
    return [
        w
        for w in self.warnings
        if (record_type is None or w.record_type == record_type)
        and (field is None or w.field == field)
        and (severity is None or w.severity is severity)
        and (kind is None or w.kind is kind)
    ]

ParseWarning dataclass

ParseWarning(
    source: str,
    line_no: int,
    record_type: str | None,
    field: str | None,
    column: str | None,
    mandatory: str | None,
    severity: Severity,
    kind: IssueKind,
    reason: str,
    raw_line: str,
)

A single structured diagnostic.

Attributes:

Name Type Description
source str

Source file or stream label.

line_no int

1-based line number of the record.

record_type str | None

Coded record type (e.g. "D0", "F0").

field str | None

Name of the problematic field.

column str | None

SDIF "start/length" (e.g. "40/12").

mandatory str | None

Requirement level ("M1", "M2", "", "*", etc.).

severity Severity

Handling outcome (FATAL, SKIPPED, RECOVERED).

kind IssueKind

Classification of the issue (MISSING, MALFORMED, etc.).

reason str

Explanation of the validation failure.

raw_line str

Raw source line (truncated to 200 characters).

Severity

Bases: Enum

How a parse issue was handled.

IssueKind

Bases: Enum

What kind of data problem a warning describes.