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, parallel-ready execution: Readers yield one MeetArchive per file lazily, so large corpora parse one file at a time with bounded memory. max_workers dispatches files across a thread pool behind an order-preserving look-ahead window (output identical to sequential), parsing in genuine parallel on free-threaded Python.
  • 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.3.1.tar.gz (88.9 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.3.1-py3-none-any.whl (93.3 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for tunas-0.3.1.tar.gz
Algorithm Hash digest
SHA256 57f082247be5034cfecc51d1689f7cb8eaf6eee9a8983924c93fbc60707b9f30
MD5 dbf1ca424c5ba877d637aa730981002f
BLAKE2b-256 619228f3c995ed4c0245711ace636b4188b7806e692779d29b12492b2c9f65a6

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunas-0.3.1.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.3.1-py3-none-any.whl.

File metadata

  • Download URL: tunas-0.3.1-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.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 713228de4280563c2738cb678beba2e01c6370723e8a0dff7bf01742469a77b9
MD5 86df630c210c9035eba2a3c04fe6d441
BLAKE2b-256 625867cc51c2dfa8d5b115d44430fcef0058a216721bccb7827dfa599fc70012

See more details on using hashes here.

Provenance

The following attestation bundles were made for tunas-0.3.1-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