Two small command-line tools for youth/summer swim league board members and volunteers who get a SwimTopia .hy3 meet-results file and need to:
- Import results into TeamUnify — but TeamUnify only wants your own team's results, in
.sd3(SDIF v3) format, and its importer has a few undocumented ways to silently drop data.hy3_to_sd3.pyconverts and automatically verifies every field against the source before telling you it's safe to upload. - Print/share a results report —
make_meet_report.pygenerates a dense, multi-column, Meet-Manager-style PDF covering both teams, ranked fastest-to-slowest per event, with optional classification against your league's own time standards (Gold/Silver/Bronze, B/BB/A/AA, or whatever tiers you use).
Built out of a real dual-meet results wrangling session — see "Why this exists" below for the specific TeamUnify import bugs this works around.
git clone https://github.com/hbentel/swim-meet-tools.git
cd swim-meet-tools
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# hytek_parser's own metadata claims attrs<22, which is stale in practice --
# everything works fine with a modern attrs (needed by the `sdif` library).
# Installing in two steps avoids pip's resolver treating that as a hard
# conflict; see "Known issues" below.
pip install hytek_parser sdif reportlab
pip install -U "attrs>=23.1.0"
pip install -e . --no-depsWindows, no command line needed: see windows/ — double-click setup_windows.bat once, then drag your .hy3 file onto Convert to SD3.bat or Generate PDF Report.bat.
hy3-to-sd3 "Results 2026-07-08 Home vs Away.hy3" ABC XYArgs: the .hy3 file, your team code as it appears in the file, your league/LSC code (combined as e.g. XYABC — whatever TeamUnify told you it expects). Writes <input>.sd3 and <input>.log next to the input by default; override with -o/--log.
This re-parses the .sd3 it just wrote and cross-checks every name, time, date, and place against the source .hy3. It exits non-zero and prints VERIFICATION FAILED with the specific mismatches if anything doesn't match — don't upload the file in that case. On success it prints VERIFICATION PASSED.
make-meet-report "Results 2026-07-08 Home vs Away.hy3" --standards examples/ompa_2026_scy_standards.jsonWrites <input>_Results.pdf by default (override with -o), covering both teams, one ranked table per event (no heat breakdown), DQ/NS/scratch results listed at the bottom. --standards is optional — omit it to skip the classification column entirely, or point it at your own league's time-standards file (see below).
--standards takes a JSON file describing your league's cutoff times per stroke/sex/age-bracket/tier. examples/ompa_2026_scy_standards.json is a real one (Orinda Moraga Pools, 2026 SCY season) to use as a template:
{
"name": "Your League 2026 SCY Time Standards",
"tiers": ["gold", "silver", "bronze"],
"standards": {
"FREESTYLE": {
"F": { "11-12": { "gold": "29.49", "silver": "33.00", "bronze": "36.00" } },
"M": { "11-12": { "gold": "29.21", "silver": "32.50", "bronze": "35.50" } }
},
"BACKSTROKE": { "...": "..." },
"BREASTSTROKE": { "...": "..." },
"BUTTERFLY": { "...": "..." },
"MEDLEY": { "...": "..." },
"FREE_RELAY": { "...": "..." },
"MEDLEY_RELAY": { "...": "..." }
}
}tiersis fastest-to-slowest, any names, any number of tiers.- Age brackets are
"min-max"strings (use"0-6"for 6-and-under). - Any tier can be omitted/null for a cell your league doesn't publish (e.g. many leagues only publish one tier for relays).
MEDLEYis individual IM; relays use their ownFREE_RELAY/MEDLEY_RELAYkeys since they're often only published at one tier.
TeamUnify's SD3 importer has a few behaviors that aren't obvious from the outside — each was found by importing a real file, watching results silently not show up, and digging until the cause was clear. hy3_to_sd3.py handles all of these automatically:
.hy3encoding: SwimTopia's export is sometimes CP1252 (accented names etc.), which hard-fails a plain UTF-8 decode. Detected and re-decoded automatically.- Missing C2 record: individual (non-relay) results need a
C2"team entry" record alongsideC1, or they silently fail to attach to the team — while relay results (which don't depend onC2) import fine. This exact asymmetry (relays work, individuals don't, no error) is what originally pointed at the cause. - Blank swimmer ID: TeamUnify matches results to athlete profiles using a 14-character ID derived from birthdate + name (present in the
.hy3asusa_swimming_id). Leaving it blank means the result silently doesn't attach. Carried through via theD3/F0records, truncated to fit the ID field onD0/F0itself as SwimTopia's own export does. - Zero-padding bug in a third-party library: the
sdifpackage's generic field encoder strips leading zeros from purely-numeric "alpha" fields, silently corrupting the youngest age brackets'event_agefield ("0006"→"6"). Patched by post-processing the encoded output. \r\r\nline endings: passingnewline="\r\n"to Python'sopen()when the string being written already contains\r\nseparators causes double-translation. Harmless to most parsers but non-compliant; fixed by disabling translation and writing the pre-formatted separators verbatim.
All of the above were confirmed by decoding a real SwimTopia-generated .sd3 file (from the same meet) and cross-checking every field — see tests/ for the regression tests that came out of that.
- Scored points aren't carried through. Real HY3 files include per-swim scoring points, but the parsing library this tool uses (
hytek_parser) doesn't expose that field. Not needed for a normal TeamUnify import (which just needs times/places), but if you need team-scoring data out of this pipeline, that field isn't there yet. hytek_parser's metadata claimsattrs<22, which conflicts with the modernattrsthatsdifneeds. Both work fine together in practice (verified extensively) — the two-step install above works around pip's resolver rejecting the combination outright. Ifpip install -e .fails with a resolver error, use the two-step install instead.
pip install -r requirements-dev.txt
pip install -e . --no-deps
pytest tests/ -vtests/fixtures/sample_meet.hy3 is a small, fully synthetic two-team meet (no real swimmer data) generated by tests/fixtures/generate_sample_meet.py — regenerate it with python3 tests/fixtures/generate_sample_meet.py if you need to extend it.
Issues and PRs welcome. If you hit a new TeamUnify import quirk, the most useful contribution is: the specific symptom, the relevant lines from the .log file hy3_to_sd3.py produces, and (if you can get one) a comparison against a results file from the host's own software — that combination is what found every bug fixed so far.
MIT — see LICENSE.