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.5.0.tar.gz (94.8 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.5.0-py3-none-any.whl (97.5 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for tunas-0.5.0.tar.gz
Algorithm Hash digest
SHA256 1c7b2cccd98adc979d2b82d1330b0472c153c0ccce364262f9cab40b98507897
MD5 ac7daccf1fa2545d3161278a3b849a29
BLAKE2b-256 8245d53c32c5095d3443a982f951d09e082455841ca4095b0c1accb1faec97bd

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: tunas-0.5.0-py3-none-any.whl
  • Upload date:
  • Size: 97.5 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.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 5dac1c2baf94af88c4591ea30f5e71e6626134f8f886796ef8f4b66c2e897262
MD5 72eadc1719c36fc49f9e9e6f2748855a
BLAKE2b-256 7dd533d6d2eefe63b68ac0c354f3a6601e6d2b326db9b0e783b156c791b1f557

See more details on using hashes here.

Provenance

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