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.6.0.tar.gz (95.5 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.6.0-py3-none-any.whl (97.8 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for tunas-0.6.0.tar.gz
Algorithm Hash digest
SHA256 f263e478018199768b54bc72a8071706d26e8340efc509a96eda57b742450c53
MD5 b3ea08e4fabff1314f5064bdea9b8fff
BLAKE2b-256 26546489bb398a6b95e873f9bc8862128f55cb0dd7eda9487bc538bc419628e0

See more details on using hashes here.

Provenance

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

File metadata

  • Download URL: tunas-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 97.8 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.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4dc63a5d9e22e42b3f279c4c463f6ca413d6a8cfb0312f3bc60ad8ada4ff7a2a
MD5 800bb0910fdad43dc69897354047745f
BLAKE2b-256 52b547646298ed908675639e6f6f82936e99bcd27ae999111254cf8acd7e577e

See more details on using hashes here.

Provenance

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