Skip to content

Data model

read_cl2 yields one MeetArchive per file, each wrapping a list of self-contained Meet objects. This page explains the object graph and its scoping rules. For field-by-field reference, see the API Reference.

Object graph

Inheritance — two small base types sit above the concrete results:

Swim (one swimmer's swim)           MeetResult (a row in Meet.results)
├── IndividualSwim                  ├── IndividualSwim
└── RelaySwim                       └── Relay

Containment — each Meet owns its results, swimmers, and clubs:

list[Meet]                                ← one Meet per .cl2 file
└── Meet
    ├── results   : list[MeetResult]      ← IndividualSwim or Relay
    │   ├── IndividualSwim
    │   │   └── swimmer  : Swimmer
    │   └── Relay
    │       ├── legs       : list[RelaySwim]    ← the ≤4 swum legs (LEG_1–LEG_4)
    │       │   └── RelaySwim
    │       │       ├── swimmer : Swimmer
    │       │       └── relay   : Relay         ← back-reference
    │       └── alternates : list[RelaySwim]    ← rostered but did not swim
    ├── swimmers  : list[Swimmer]
    │   └── Swimmer
    │       ├── swims    : list[IndividualSwim | RelaySwim]   ← common base: Swim
    │       └── club     : Club | None
    └── clubs     : list[Club]
        └── Club
            ├── results  : list[MeetResult]
            └── swimmers : list[Swimmer]

The parser wires every reference (including back-references like RelaySwim.relay) in a single pass, so traversals are plain attribute reads.

Because those back-references make the graph cyclic, each aggregate defines a concise repr()/str() that summarises its collections by count and names related objects by a short label (a club's team_code, a swimmer's full_name) rather than expanding them. Printing a Meet therefore stays a single line and never walks the whole graph:

>>> meet
Meet(name='Champs', organization=USS, start_date=2025-01-01, clubs=12, swimmers=480, results=2310)
>>> print(meet)
Champs (2025-01-01)

Two ways to look at a result

The same row can be viewed through two lenses, which is why there are two base classes:

  • MeetResult is a row in Meet.results — an IndividualSwim or a Relay. It holds the shared event/place/time context but no swimmer or splits.
  • Swim is one swimmer's swim — an IndividualSwim or a RelaySwim. It is the common type behind Swimmer.swims, giving both kinds a uniform interface (swimmer, time, status, session, event, date, meet, course, splits, is_relay_leg).

An IndividualSwim is both — it appears in Meet.results and in its swimmer's swims. A Relay is only a MeetResult (a squad, not a swim); its individual legs are the RelaySwims.

Relay legs and their events

A Relay carries its swimmers as RelaySwim legs. Swimmers who raced are in legs (ordered LEG_1LEG_4); rostered swimmers who did not race are in alternates and are excluded from Swimmer.swims. The squad's splits are whole-relay cumulative marks on Relay.splits (e.g. 50/100/150/200 for a 4×50), populated identically by both readers. Each leg's RelaySwim.splits is derived from that row — the marks swum during the leg, re-based to the leg start so the leg reads like a flat-start swim (distances from 0, cumulative time from the leg's takeoff). It is empty when there is nothing to derive (the relay carries no splits, or the slot is an alternate), so it keeps the uniform Swim.splits type:

relay.splits          # [Split(50, 31.60), Split(100, 1:10.63), Split(150, 1:40.63), Split(200, 2:16.27)]
relay.legs[1].splits  # [Split(50, 39.03)]  — leg 2's own 50, = 1:10.63 − 31.60
relay.alternates[0].splits   # []

A leg reports the individual event it amounts to, so it sorts alongside flat-start swims:

relay.event              # Event.FREE_400_RELAY_SCY  (the squad event)
relay.legs[0].event      # Event.FREE_100_SCY        (the leg's individual event)

This is why swimmer.swims_in(Event.FREE_100_SCY) returns both 100-free flat-starts and matching relay legs.

Identity and scoping

Meets are independent and never merged; exporting the same meet twice yields two unrelated Meet objects. Swimmers are grouped within a meet by id_short (falling back to id_long). Fusing never relies on names or birthdays to avoid incorrect merges. The same athlete in multiple meets is represented by separate Swimmer instances; cross-meet tracking is handled in application code (see Recipes).

Clubs are scoped within a meet and keyed by (team_code, lsc). Unattached entries (team code ending in UN, or a name containing "unattached") create no Club instance, and the swimmer's club is set to None.

Times and outcomes

A timed swim has status == ResultStatus.OK and a Time. Non-time outcomes (NT, NS, DNF, DQ, SCR) have time = None, except for course-X disqualifications, which preserve the swum time with status == DQ. Every swim is preserved to facilitate complete analysis.

ResultStatus values: OK (official time), NT (no time), NS (no show), DNF (did not finish), DQ (disqualified), SCR (scratch).

Field reference by type

The API reference lists every field with its type; the tables below summarize what each type carries and which SDIF record populates it. Aggregates (Meet, Club, Swimmer, result types) are mutable, slotted dataclasses with identity equality; value types are frozen and hashable.

.hy3-only fields

The following fields are populated only by read_hy3 (they remain None or default under read_cl2):

  • SourceFile: hy3_file_type, created_time, licensee
  • Meet: venue, age_up_date, sanction_number
  • Club: email
  • MeetResult: dq_code, dq_reason, converted_seed_time, converted_seed_course, backup_times
  • Relay: splits

Meet — and its metadata

A Meet represents one B1 block and all elements beneath it.

Field Type Notes
name, start_date str, date Required (M1).
end_date, city, state, country, postal_code, address_one, address_two optional Location. state is a State; country is a Country.
organization Organization \| None ORG code (USS, Masters, NCAA, ...). None when the source carries no org code (.hy3 always; .cl2 when the field is blank).
course Course \| None Default course (B1 col 150).
altitude int \| None Pool altitude in feet.
meet_type MeetType \| None Almost always blank in real .cl2.
host MeetHost \| None From B2 (see below).
source_file SourceFile \| None File-level provenance, shared by all meets in one file.
results, swimmers, clubs lists Contents of the meet.

Convenience accessors: meet.individual_swims, meet.relays, meet.individual_swims_for(event), meet.relays_for(event).

MeetHost (frozen) holds name, address_one, address_two, city, state, postal_code, country, phone. Real .cl2 files never emit B2, so host is usually None (host details live in the sibling .hy3 file).

SourceFile (frozen) captures the file header (SDIF A0/Z0 or Hy-Tek A1): path, file_type (FileType), sdif_version, software_name, software_version, contact_name, contact_phone, created (date), submitted_by_lsc (LSC), and notes. For .hy3 files, it also carries hy3_file_type (Hy3FileType), created_time, and licensee.

Club — and entry counts

A Club is keyed within a meet by (team_code, lsc). Beyond identity (team_code, lsc, full_name, abbreviated_name, short_name) it carries organization, the address fields, region (Region), coach, coach_phone, entry_counts, and its own results/swimmers (plus club.individual_swims / club.relays).

ClubEntryCounts (frozen, from C2) records the team's declared totals: num_individual_swims, num_athletes, num_relay_entries, num_relay_name_records, num_split_records. Real .cl2 files never emit C2, so this is usually None.

Swimmer — identity and PII

A Swimmer is scoped to one meet.

Field Type Notes
first_name, last_name, sex str, str, Sex Required.
middle_initial, preferred_first_name str \| None preferred_first_name comes from D3.
id_short str \| None 12-char USS# — the primary identity key.
id_long str \| None 14-char modern SWIMS ID (from D3); identity fallback.
birthday date \| None Often blank ("WODOB" privacy exports).
citizenship Citizenship \| Country \| None A non-country code or a country (see below).
club Club \| None None when the swimmer is unattached.
contact SwimmerContact \| None PII; absent in results files.
registration SwimmerRegistration \| None Sensitive PII; absent in results files.
swims list[IndividualSwim \| RelaySwim] Counting swims (relay alternates excluded).

Accessors: full_name, individual_swims, relay_swims, swims_in(event).

SwimmerContact (frozen, from D1/D2): address, city, state, postal_code, country, region, alt_mailing_name, phone_primary, phone_secondary.

SwimmerRegistration (frozen, from D1/D2/D3): member_status (MemberStatus), registration_date, season (Season), ethnicity_primary/ethnicity_secondary (Ethnicity), affiliations (frozenset[Affiliation]), old_member_number, fina_other_federation, admin_info.

Citizenship is Citizenship | Country

SDIF's CITIZEN code is either one of two non-country codes — Citizenship.DUAL (2AL) or Citizenship.FOREIGN (FGN) — or any FINA Country code. So Swimmer.citizenship and RelaySwim.citizenship are typed Citizenship | Country | None.

Result rows: IndividualSwim, Relay, RelaySwim

Every row in Meet.results (an IndividualSwim or a Relay) shares the MeetResult base:

Field Type Notes
meet, club Meet, Club \| None Back-references.
event Event The (distance, stroke, course) event.
event_sex Sex M/F/X (mixed).
event_min_age, event_max_age int \| None Event age range.
session Session PRELIMS/FINALS/SWIM_OFFS.
status ResultStatus Outcome.
time, date Time \| None, date \| None
seed_time, seed_course Time \| None, Course \| None
heat, lane, rank int \| None rank is the place.
points float \| None Scored at finals.
event_number str \| None Meet program event number.
event_min_time_class, event_max_time_class EventTimeClass \| None Almost always blank.

IndividualSwim adds swimmer, swimmer_age_class, attach_status (AttachStatus), and splits; its course derives from the event and is_relay_leg is always False.

Relay adds relay_letter (the A/B/… squad designator), total_age, legs, and alternates. A Relay is not a Swim — its swims are the legs.

RelaySwim is one swimmer's leg or roster slot: swimmer (None if unidentified), relay (back-reference), order (RelayLegOrder), time, status, takeoff_time (relay-exchange reaction, in hundredths of a second), course, swimmer_age_class, citizenship, and splits. Its event, date, meet, and session are derived from the parent relay, and is_relay_leg is always True.

Split

Split (frozen) is one segment from a G0 record: distance (cumulative distance from the start, e.g. 50/100/150), time (Time \| None), and split_type (SplitType.INTERVAL or CUMULATIVE). Splits attach to the swim (individual or relay leg) that produced them.

Times

Time is an immutable, hashable value type storing centiseconds: int (an unbounded integer, so there is no float drift):

from tunas import Time

t = Time.parse("1:04.87")     # accepts "[M:]SS.HH"; whitespace tolerated
t.centiseconds                # 6487
t.minute, t.second, t.hundredth   # (1, 4, 87)
t.total_seconds               # 64.87
str(t)                        # "1:04.87"  (round-trips with Time.parse)

Time.parse("23.45") < t       # True — faster times sort first
t - Time.parse("4.87")        # Time(...) for "1:00.00"

Faster times compare as less than slower ones, so min(...)/sorted(...) give the fastest first. Addition and subtraction return a new Time; subtracting a larger time from a smaller one raises ValueError, as does constructing a negative Time.

Enumerations

Every categorical field is an enum whose value is the raw SDIF code, so a byte from the file resolves directly (Sex("F"), Course("3"), Stroke("5")); an unknown code raises ValueError. The full members are in the enums reference and geography reference; in brief:

Enum Models Notes
Sex swimmer / event sex MALE, FEMALE, MIXED (events only).
Stroke stroke .display() gives "Free", "IM", "Medley Relay", …
Course pool course SCM/SCY/LCM; parser also accepts the alpha forms S/Y/L.
Session session PRELIMS, FINALS, SWIM_OFFS.
ResultStatus swim outcome OK, NT, NS, DNF, DQ, SCR.
AttachStatus club attachment ATTACHED, UNATTACHED.
RelayLegOrder relay leg position LEG_1LEG_4, ALTERNATE, NOT_SWUM.
SplitType split kind INTERVAL, CUMULATIVE.
MeetType meet type Invitational, championship, dual, …
Organization sanctioning org USS, Masters, NCAA, YMCA, …
FileType transmission type MEET_RESULTS, TOP_16, …
Region USA Swimming region Regions 1–14.
EventTimeClass time-standard class Novice…AAAA, Junior, Senior.
MemberStatus, Season, Ethnicity, Affiliation, Citizenship registration / demographics Populate PII value types.
LSC, State, Country geography Two-letter LSC, USPS state, FINA country.

Example: traversing the model

from tunas import read_cl2, Event

meets = [m for arc in read_cl2("results/") for m in arc.meets]

for meet in meets:
    print(f"== {meet.name} ({meet.start_date}) ==")
    for swimmer in meet.swimmers:
        club = swimmer.club.abbreviated_name if swimmer.club else "UN"
        for swim in swimmer.swims_in(Event.FREE_100_SCY):
            outcome = swim.time if swim.time is not None else swim.status.value
            print(f"  {swimmer.full_name:<25} {club:>5} {swim.session.value} {outcome}")