USA Swimming meet result (.cl2 / SDIF v3 and Hy-Tek .hy3) parser and analysis library
Project description
tunas
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: Ownsswimmers,clubs, andresults(accessible viameet.individual_swimsandmeet.relays, or filtered viameet.individual_swims_for(event)andmeet.relays_for(event)). Carries metadata including name, dates, location,course,meet_type,host(MeetHost), andsource_file(SourceFilefor file-level provenance).Swimmer: Scoped to one meet. Exposesfull_name,swims,individual_swims,relay_swims, andswims_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). Carriescoach,entry_counts, address, and associated results and swimmers.Swim: The uniform interface for individual swims (IndividualSwim) and relay legs (RelaySwim), exposingswimmer,time(TimeorNone),status(ResultStatus),session,event,date,meet,course, andsplits. 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 withswimmer.swims_in(event)ormeet.individual_swims_for(event).- Relays:
Relaysquads containRelaySwimlegs (legs) andalternates. 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, andminute/second/hundredth/total_secondsaccessors.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(orid_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 (
A0–G0,Z0), including relays, relay alternates, and per-leg splits. Registration (D1/D2) and demographic (D3) records populate optional PII fields; qualifying-time records (J0–J2) surface as warnings. - Hy-Tek
.hy3support:read_hy3parses the reverse-engineered.hy3results format (confirmed fields only) into the sameMeetobject 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 becomeNone; 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(withseverity,kind, column, and raw line);strict=Truefails 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
MeetArchiveper 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; passesmypy --strict.
Documentation
- Getting Started
- Parsing & Errors
- Data Model
- Cookbook / Recipes
- SDIF
.cl2format reference - Hy-Tek
.hy3format reference - API Reference
- Architecture & Design
- Changelog
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
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1c7b2cccd98adc979d2b82d1330b0472c153c0ccce364262f9cab40b98507897
|
|
| MD5 |
ac7daccf1fa2545d3161278a3b849a29
|
|
| BLAKE2b-256 |
8245d53c32c5095d3443a982f951d09e082455841ca4095b0c1accb1faec97bd
|
Provenance
The following attestation bundles were made for tunas-0.5.0.tar.gz:
Publisher:
publish.yml on ajoe2/tunas
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tunas-0.5.0.tar.gz -
Subject digest:
1c7b2cccd98adc979d2b82d1330b0472c153c0ccce364262f9cab40b98507897 - Sigstore transparency entry: 1672502498
- Sigstore integration time:
-
Permalink:
ajoe2/tunas@5be7c324b14c6421b05424390390cc94d6445f7e -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/ajoe2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5be7c324b14c6421b05424390390cc94d6445f7e -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5dac1c2baf94af88c4591ea30f5e71e6626134f8f886796ef8f4b66c2e897262
|
|
| MD5 |
72eadc1719c36fc49f9e9e6f2748855a
|
|
| BLAKE2b-256 |
7dd533d6d2eefe63b68ac0c354f3a6601e6d2b326db9b0e783b156c791b1f557
|
Provenance
The following attestation bundles were made for tunas-0.5.0-py3-none-any.whl:
Publisher:
publish.yml on ajoe2/tunas
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tunas-0.5.0-py3-none-any.whl -
Subject digest:
5dac1c2baf94af88c4591ea30f5e71e6626134f8f886796ef8f4b66c2e897262 - Sigstore transparency entry: 1672502541
- Sigstore integration time:
-
Permalink:
ajoe2/tunas@5be7c324b14c6421b05424390390cc94d6445f7e -
Branch / Tag:
refs/tags/v0.5.0 - Owner: https://github.com/ajoe2
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@5be7c324b14c6421b05424390390cc94d6445f7e -
Trigger Event:
release
-
Statement type: