Skip to main content

USA Swimming meet result (.cl2 / SDIF v3 and Hy-Tek .hy3) parser and analysis library

Project description

tunas

PyPI Python License: MIT

A Python library for parsing USA Swimming meet result files (.cl2 / SDIF v3 and Hy-Tek .hy3) into structured Python objects and performing offline qualifying time standard lookups.

📖 Documentation: https://ajoe2.github.io/tunas/

Installation

pip install tunas

Requires Python 3.12+. Currently, the runtime depends only on the Python standard library.

Quick Start

read_cl2 is the primary entry point. It parses file paths, directories, lists of paths, or text streams, yielding one MeetArchive per source file — each holding that file's meets and its own ParseReport:

from tunas import read_cl2

for archive in read_cl2("results.cl2"):
    for meet in archive.meets:
        print(f"{meet.name} ({meet.start_date})")
        for swim in meet.individual_swims:
            outcome = swim.time if swim.time is not None else swim.status.value
            print(f"  {swim.swimmer.full_name:<24} {swim.event.name:<16} {outcome}")

    if archive.report.warnings:
        print(f"{len(archive.report.warnings)} records flagged — inspect archive.report.warnings")

The reader is lazy, so a large directory is parsed one file at a time. To pull everything into memory, flatten the archives: meets = [m for arc in read_cl2(src) for m in arc.meets].

Hy-Tek .hy3 results parse the same way via read_hy3, producing the identical Meet object graph:

from tunas import read_hy3

(archive,) = read_hy3("Meet Results-Winter Champs-001.hy3")  # a single file -> one archive

Core Concepts

All parsed data is contained in independent Meet objects:

  • Meet: Owns swimmers, clubs, and results (accessible via meet.individual_swims and meet.relays, or filtered via meet.individual_swims_for(event) and meet.relays_for(event)). Carries metadata including name, dates, location, course, meet_type, host (MeetHost), and source_file (SourceFile for file-level provenance).
  • Swimmer: Scoped to one meet. Exposes full_name, swims, individual_swims, relay_swims, and swims_in(event). Includes identity (id_short/id_long), birthday, sex, citizenship, and optional contact/registration PII.
  • Club: Scoped to one meet and keyed by (team_code, lsc). Carries coach, entry_counts, address, and associated results and swimmers.
  • Swim: The uniform interface for individual swims (IndividualSwim) and relay legs (RelaySwim), exposing swimmer, time (Time or None), status (ResultStatus), session, event, date, meet, course, and splits. Scratches and disqualifications are preserved.
  • Event: A 90+ member enum of (distance, stroke, course), comparable in declaration order, with helpers (is_relay, leg_event, leg_strokes, Event.find). Filter with swimmer.swims_in(event) or meet.individual_swims_for(event).
  • Relays: Relay squads contain RelaySwim legs (legs) and alternates. Each leg reports its individual event, so it sorts alongside flat-start swims.
  • Time: Immutable centisecond value type — Time.parse("1:04.87"), ordering, addition/subtraction, and minute/second/hundredth/total_seconds accessors.
  • Split: Per-leg splits (distance, time, split_type) attach to the swim that produced them.
  • Scoping: Meets are independent and never merged; swimmers and clubs are scoped to their respective meet. Group by id_short (or id_long) to track athletes across meets.
from tunas import read_cl2, Event

meets = [m for arc in read_cl2("season/") for m in arc.meets]
for meet in meets:
    for swim in meet.swimmers[0].swims_in(Event.FREE_100_SCY):
        print(meet.name, swim.session.value, swim.time)

Offline Time Standards

USA Swimming motivational standards (B through AAAA, the bundled 2025–2028 cuts) are available locally with no setup or network access. Lookups are keyed by single-year age group (10 & under, 11-12, 13-14, 15-16, 17-18) and sex:

from tunas import qualifies_for, all_qualified, standard_time, Sex, Event, Time

# Fastest standard achieved (or None):
qualifies_for(Time.parse("1:05.23"), Event.FREE_100_SCY, age=12, sex=Sex.FEMALE)
# → TimeStandard.BB

# Every standard met, slowest first:
all_qualified(Time.parse("1:05.23"), Event.FREE_100_SCY, age=12, sex=Sex.FEMALE)
# → [TimeStandard.B, TimeStandard.BB]

# The cutoff time for a given standard (or None if undefined):
standard_time(TimeStandard.AAAA, Event.FREE_100_SCY, age=12, sex=Sex.FEMALE)
# → Time(...)

Standards are defined for MALE/FEMALE only; passing Sex.MIXED raises ValueError.

Error Handling

Parsing is lenient by default to recover from common exporter bugs; warnings are collected in a ParseReport.

for archive in read_cl2("messy/"):
    for w in archive.report.warnings:
        print(f"{w.source}:{w.line_no} [{w.severity.value}] {w.record_type}: {w.reason}")

Use strict=True to fail fast and raise ParseError on the first warning (surfaced as the iterator is consumed). Structural violations always raise.

Features

  • Complete SDIF v3 coverage: Parses every meet-results record type (A0G0, Z0), including relays, relay alternates, and per-leg splits. Registration (D1/D2) and demographic (D3) records populate optional PII fields; qualifying-time records (J0J2) surface as warnings.
  • Hy-Tek .hy3 support: read_hy3 parses the reverse-engineered .hy3 results format (confirmed fields only) into the same Meet object graph, capturing data SDIF omits — disqualification codes/reasons, converted seed times, and meet sanction numbers.
  • Clean object model: Slotted dataclasses with pre-wired object references (including back-references) and zero global state. Value types (Time, Split, MeetHost, …) are frozen and hashable.
  • Zero data loss: All entered swims are kept — including non-time outcomes (scratches, DQs, no-shows via ResultStatus). Missing optional fields become None; raw line contents are preserved on validation failures.
  • Lenient by default, strict on demand: Recovers from common exporter bugs and reports each issue as a structured ParseWarning (with severity, kind, column, and raw line); strict=True fails fast on the first problem.
  • Offline standards: Local O(1) lookup of USA Swimming B through AAAA motivational cuts, bundled as JSON — no setup or network.
  • Robust decoding: Defaults to CP-1252 (to preserve column alignment and accented names), tolerates BOMs, short/long lines, and mixed line endings.
  • Streaming execution: Readers yield one MeetArchive per file lazily and in source order, so large corpora parse one file at a time with bounded memory regardless of corpus size.
  • Type-safe: Fully type-hinted and marked py.typed; passes mypy --strict.

Documentation

Full docs site: https://ajoe2.github.io/tunas/

Development

Managed with uv:

uv sync                                      # Setup environment (incl. dev deps)
uv run pytest                                # Run offline test suite
uv run pytest --cov=tunas                    # Run with coverage check (95% gate)
uv run ruff check && uv run ruff format      # Lint and format
uv run mypy src/tunas                        # Type-check (strict)
uv run mkdocs serve                          # Preview the docs site locally

The test suite is fully self-contained and offline — real-world coverage comes from committed "golden" .cl2 files plus hand-verified expected-state JSON under tests/data/, so no data download is needed.

See CONTRIBUTING.md for the test layout, golden-file regeneration, and the release process.

Status

tunas is in alpha. The public API is stable, but subject to revision before 1.0.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tunas-0.4.0.tar.gz (89.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tunas-0.4.0-py3-none-any.whl (93.3 kB view details)

Uploaded Python 3

File details

Details for the file tunas-0.4.0.tar.gz.

File metadata

  • Download URL: tunas-0.4.0.tar.gz
  • Upload date:
  • Size: 89.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunas-0.4.0.tar.gz
Algorithm Hash digest
SHA256 70800dce6e6dd07e57d541e14993cb839378b97a389de065fd41583627a9f60a
MD5 2d7606b34e8a608b7d5647fb0647c12a
BLAKE2b-256 911cc95397b1222c89c76190910e5040bfc7d1df3f24f33c31c74ed034e13acd

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunas-0.4.0.tar.gz:

Publisher: publish.yml on ajoe2/tunas

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tunas-0.4.0-py3-none-any.whl.

File metadata

  • Download URL: tunas-0.4.0-py3-none-any.whl
  • Upload date:
  • Size: 93.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for tunas-0.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e42b92a68e823b20aed7abf36903925d8f4dcd560a354de15e0e45a4272f08b2
MD5 ec0eb23d62a7b814418096355907b94f
BLAKE2b-256 7a10838434c5cb6fc82cce95f0dc242df90a6cbd214141e7c0c546f80d043353

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunas-0.4.0-py3-none-any.whl:

Publisher: publish.yml on ajoe2/tunas

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page