hytek-parser 2.3.0__tar.gz → 3.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/PKG-INFO +1 -1
- hytek_parser-3.0.0/hytek_parser/hy3/_utils.py +69 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/enums.py +31 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/e_event_parsers.py +12 -2
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/f_relay_parsers.py +27 -6
- hytek_parser-3.0.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +75 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/schemas.py +11 -3
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/pyproject.toml +1 -1
- hytek_parser-2.3.0/hytek_parser/hy3/_utils.py +0 -32
- hytek_parser-2.3.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +0 -77
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/LICENSE.md +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/README.md +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/__init__.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/_utils.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/export_xls/__init__.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/export_xls/_utils.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/export_xls/schemas.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/__init__.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/__init__.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/a_file_parsers.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/b_meet_parsers.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/c_team_parsers.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/g_split_parsers.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3_parser.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hyv/__init__.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hyv/enums.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hyv/schemas.py +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/py.typed +0 -0
- {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/types.py +0 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import math
|
|
2
|
+
from typing import Optional, Union
|
|
3
|
+
|
|
4
|
+
from hytek_parser._utils import safe_cast, select_from_enum
|
|
5
|
+
from hytek_parser.hy3.enums import ReplacedTimeTimeCode
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_time(raw_time: str) -> Union[float, ReplacedTimeTimeCode]:
|
|
9
|
+
"""Parse a time into either a number or time code.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
raw_time (str): The time string extracted from the Hytek file.
|
|
13
|
+
|
|
14
|
+
Returns:
|
|
15
|
+
Union[float, ReplacedTimeTimeCode]: Either a numerical time or a time code.
|
|
16
|
+
"""
|
|
17
|
+
if (casted := safe_cast(float, raw_time, default=-1)) != -1:
|
|
18
|
+
# Number
|
|
19
|
+
return casted
|
|
20
|
+
else:
|
|
21
|
+
return select_from_enum(ReplacedTimeTimeCode, raw_time)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def parse_time_or_none(raw_time: str) -> Optional[float]:
|
|
25
|
+
"""Parse a timing field where 0.00/blank means 'not recorded'.
|
|
26
|
+
|
|
27
|
+
Unlike parse_time (which returns 0.0 for "0.00" and a ReplacedTimeTimeCode
|
|
28
|
+
for blank/non-numeric input), this returns None unless the value is a
|
|
29
|
+
positive float. Used for pad and backup-button times, where an unused
|
|
30
|
+
slot is written as 0.00 and should surface as None rather than 0.0.
|
|
31
|
+
"""
|
|
32
|
+
val = parse_time(raw_time)
|
|
33
|
+
return val if isinstance(val, float) and val > 0.0 else None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def parse_reaction_time(raw: str) -> Optional[float]:
|
|
37
|
+
"""Parse a reaction/takeoff-time column (E2 col 83-87, F2 col 83-102).
|
|
38
|
+
|
|
39
|
+
NEGATIVE VALUES ARE MEANINGFUL and load-bearing here: a relay takeover slot
|
|
40
|
+
records an early exchange as a negative number (7,868 values corpus-wide).
|
|
41
|
+
``parse_time_or_none`` requires > 0.0 and would silently destroy every one
|
|
42
|
+
of them -- do not substitute it.
|
|
43
|
+
|
|
44
|
+
Sentinels, all meaning "not recorded": blank, 0.00 in any sign spelling,
|
|
45
|
+
and the literal NRT ("No Reaction Time") that Meet Manager writes into
|
|
46
|
+
takeover slots when the exchange was not measured.
|
|
47
|
+
|
|
48
|
+
Values above the plausible reaction range are returned unchanged. A
|
|
49
|
+
minority of files put something else in these columns; its meaning is
|
|
50
|
+
unresolved, and filtering it here would make it permanently invisible.
|
|
51
|
+
"""
|
|
52
|
+
val = raw.strip()
|
|
53
|
+
if not val or val.upper() == "NRT":
|
|
54
|
+
return None
|
|
55
|
+
try:
|
|
56
|
+
num = float(val)
|
|
57
|
+
except ValueError:
|
|
58
|
+
# Observed malformed forms: a bare "+" sign, stray high bytes.
|
|
59
|
+
return None
|
|
60
|
+
if not math.isfinite(num):
|
|
61
|
+
# float() accepts "nan"/"inf"/"-inf", all five characters or fewer,
|
|
62
|
+
# so they fit this column like any other token. Neither is a
|
|
63
|
+
# reaction time -- and a bare int() downstream would raise on them
|
|
64
|
+
# (ValueError on nan, OverflowError on inf) instead of yielding None
|
|
65
|
+
# like every other malformed token, dropping the whole file.
|
|
66
|
+
return None
|
|
67
|
+
# float() maps "0.00", "+0.00" and "-0.00" all to zero; all three are the
|
|
68
|
+
# "not recorded" sentinel.
|
|
69
|
+
return None if num == 0.0 else num
|
|
@@ -12,6 +12,25 @@ class Gender(Enum):
|
|
|
12
12
|
FEMALE = "F"
|
|
13
13
|
UNKNOWN = "U"
|
|
14
14
|
|
|
15
|
+
@classmethod
|
|
16
|
+
def _missing_(cls, value):
|
|
17
|
+
# Hy-Tek exporters occasionally emit a lowercase sex byte. Case is
|
|
18
|
+
# not meaningful in this column, and UNKNOWN specifically means
|
|
19
|
+
# "the file did not say" -- so a lowercase 'm' has to resolve to
|
|
20
|
+
# MALE rather than become indistinguishable from a blank column.
|
|
21
|
+
#
|
|
22
|
+
# Retry ONLY when upcasing changes the value. Without that guard an
|
|
23
|
+
# unrecognized uppercase byte re-enters this hook with an identical
|
|
24
|
+
# value and recurses forever.
|
|
25
|
+
#
|
|
26
|
+
# _missing_ rather than aenum's native _missing_value_: both work
|
|
27
|
+
# here, but _missing_ is also the stdlib hook, so it keeps working
|
|
28
|
+
# if the enum base ever changes. _missing_value_ would silently
|
|
29
|
+
# become a no-op and reintroduce the bug with no signal.
|
|
30
|
+
if isinstance(value, str) and value.upper() != value:
|
|
31
|
+
return cls(value.upper())
|
|
32
|
+
return None
|
|
33
|
+
|
|
15
34
|
|
|
16
35
|
class Stroke(Enum):
|
|
17
36
|
"""Types of swimming strokes."""
|
|
@@ -23,6 +42,18 @@ class Stroke(Enum):
|
|
|
23
42
|
BREASTSTROKE = "C", "3", 3
|
|
24
43
|
BUTTERFLY = "D", "4", 4
|
|
25
44
|
MEDLEY = "E", "5", 5
|
|
45
|
+
# Diving. Hy-Tek encodes the BOARD in the stroke column:
|
|
46
|
+
# F = 1-metre springboard, G = 3-metre springboard, H = platform.
|
|
47
|
+
# Verified on a multi-conference corpus: the chars nest strictly
|
|
48
|
+
# (F, then F+G, then F+G+H -- never G without F, never H without G),
|
|
49
|
+
# carry the same dive count within a meet, and their median scores
|
|
50
|
+
# ascend with degree of difficulty.
|
|
51
|
+
# Without these members all three fall through select_from_enum() to
|
|
52
|
+
# UNKNOWN, which is the catch-all for any unrecognized byte -- making
|
|
53
|
+
# diving indistinguishable from file corruption.
|
|
54
|
+
DIVING_1M = "F"
|
|
55
|
+
DIVING_3M = "G"
|
|
56
|
+
DIVING_PLATFORM = "H"
|
|
26
57
|
|
|
27
58
|
UNKNOWN = "U", "0", 0
|
|
28
59
|
|
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from hytek_parser._utils import extract, get_age_group, safe_cast, select_from_enum
|
|
5
|
-
from hytek_parser.hy3._utils import parse_time, parse_time_or_none
|
|
5
|
+
from hytek_parser.hy3._utils import parse_reaction_time, parse_time, parse_time_or_none
|
|
6
6
|
from hytek_parser.hy3.enums import (
|
|
7
7
|
Course,
|
|
8
8
|
DisqualificationCode,
|
|
@@ -12,6 +12,7 @@ from hytek_parser.hy3.enums import (
|
|
|
12
12
|
Stroke,
|
|
13
13
|
WithTimeTimeCode,
|
|
14
14
|
)
|
|
15
|
+
from hytek_parser.hy3.line_parsers.h_dq_parsers import LAST_DQ_SLOT_KEY
|
|
15
16
|
from hytek_parser.hy3.schemas import DisqualificationInfo, ParsedHytekFile
|
|
16
17
|
|
|
17
18
|
|
|
@@ -26,7 +27,7 @@ def e1_parser(
|
|
|
26
27
|
# Event info
|
|
27
28
|
event_gender = select_from_enum(Gender, extract(line, 14, 1))
|
|
28
29
|
event_gender_age = select_from_enum(GenderAge, extract(line, 15, 1))
|
|
29
|
-
distance = safe_cast(
|
|
30
|
+
distance = safe_cast(float, extract(line, 16, 6))
|
|
30
31
|
stroke = select_from_enum(Stroke, extract(line, 22, 1))
|
|
31
32
|
age_min, age_max = get_age_group(
|
|
32
33
|
age_min=safe_cast(int, extract(line, 23, 3)),
|
|
@@ -117,6 +118,8 @@ def e2_parser(
|
|
|
117
118
|
button_3_time = parse_time_or_none(extract(line, 55, 8))
|
|
118
119
|
backup_4_time = parse_time_or_none(extract(line, 75, 8))
|
|
119
120
|
alt_time_code = extract(line, 96, 1) or None # observed: 'A' / 'K' / blank
|
|
121
|
+
# col 83-87. Signed; sentinels are blank / 0.00 in any sign spelling.
|
|
122
|
+
reaction_time = parse_reaction_time(extract(line, 83, 5))
|
|
120
123
|
|
|
121
124
|
raw_date = extract(line, 88, 8).strip()
|
|
122
125
|
date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
|
|
@@ -150,8 +153,15 @@ def e2_parser(
|
|
|
150
153
|
setattr(entry, f"{prefix}_button_2_time", button_2_time)
|
|
151
154
|
setattr(entry, f"{prefix}_button_3_time", button_3_time)
|
|
152
155
|
setattr(entry, f"{prefix}_backup_4_time", backup_4_time)
|
|
156
|
+
setattr(entry, f"{prefix}_reaction_time", reaction_time)
|
|
153
157
|
setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
|
|
154
158
|
|
|
159
|
+
# Anchor the H1 reason / H2 detail lines that follow to this DQ slot (see
|
|
160
|
+
# h_dq_parsers). File position is the only signal that survives a same-code
|
|
161
|
+
# prelim+finals double-DQ; clear it on a non-DQ result so a later orphaned
|
|
162
|
+
# H1/H2 does not attach to a stale slot.
|
|
163
|
+
opts[LAST_DQ_SLOT_KEY] = f"{prefix}_dq_info" if dq_info is not None else None
|
|
164
|
+
|
|
155
165
|
event.last_entry = entry
|
|
156
166
|
file.meet.last_event = (event_num, event)
|
|
157
167
|
|
|
@@ -2,7 +2,7 @@ from datetime import datetime
|
|
|
2
2
|
from typing import Any
|
|
3
3
|
|
|
4
4
|
from hytek_parser._utils import extract, get_age_group, safe_cast, select_from_enum
|
|
5
|
-
from hytek_parser.hy3._utils import parse_time, parse_time_or_none
|
|
5
|
+
from hytek_parser.hy3._utils import parse_reaction_time, parse_time, parse_time_or_none
|
|
6
6
|
from hytek_parser.hy3.enums import (
|
|
7
7
|
Course,
|
|
8
8
|
DisqualificationCode,
|
|
@@ -11,6 +11,7 @@ from hytek_parser.hy3.enums import (
|
|
|
11
11
|
Stroke,
|
|
12
12
|
WithTimeTimeCode,
|
|
13
13
|
)
|
|
14
|
+
from hytek_parser.hy3.line_parsers.h_dq_parsers import LAST_DQ_SLOT_KEY
|
|
14
15
|
from hytek_parser.hy3.schemas import (
|
|
15
16
|
DisqualificationInfo,
|
|
16
17
|
Gender,
|
|
@@ -30,7 +31,7 @@ def f1_parser(
|
|
|
30
31
|
# Get event info
|
|
31
32
|
event_gender = select_from_enum(Gender, extract(line, 14, 1))
|
|
32
33
|
event_gender_age = select_from_enum(GenderAge, extract(line, 15, 1))
|
|
33
|
-
distance = safe_cast(
|
|
34
|
+
distance = safe_cast(float, extract(line, 16, 6))
|
|
34
35
|
stroke = select_from_enum(Stroke, extract(line, 22, 1))
|
|
35
36
|
|
|
36
37
|
# Have to set these now since swimmer ages are not available yet
|
|
@@ -108,9 +109,11 @@ def f2_parser(
|
|
|
108
109
|
overall_place = safe_cast(int, extract(line, 30, 4))
|
|
109
110
|
|
|
110
111
|
# previously-dropped F2 timing fields. The five timing-column
|
|
111
|
-
# offsets are IDENTICAL to e2_parser
|
|
112
|
-
#
|
|
113
|
-
# F2
|
|
112
|
+
# offsets are IDENTICAL to e2_parser. F2 then carries FOUR reaction-time
|
|
113
|
+
# slots at cols 83-102 (5 chars each) where E2 carries one at 83-87, which
|
|
114
|
+
# is why F2's date sits at col 103 and its alt_time_code at col 111.
|
|
115
|
+
# Slot 1 is the leadoff block start; slots 2-4 are exchange takeovers and
|
|
116
|
+
# are legitimately negative when a swimmer leaves early.
|
|
114
117
|
pad_time = parse_time_or_none(extract(line, 63, 12))
|
|
115
118
|
button_1_time = parse_time_or_none(extract(line, 39, 8))
|
|
116
119
|
button_2_time = parse_time_or_none(extract(line, 47, 8))
|
|
@@ -118,6 +121,10 @@ def f2_parser(
|
|
|
118
121
|
backup_4_time = parse_time_or_none(extract(line, 75, 8))
|
|
119
122
|
alt_time_code = extract(line, 111, 1) or None # F2 offset; observed: 'A'/'K'/blank
|
|
120
123
|
|
|
124
|
+
reaction_times = [
|
|
125
|
+
parse_reaction_time(extract(line, 83 + 5 * i, 5)) for i in range(4)
|
|
126
|
+
]
|
|
127
|
+
|
|
121
128
|
raw_date = extract(line, 103, 8).strip()
|
|
122
129
|
date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
|
|
123
130
|
|
|
@@ -150,8 +157,15 @@ def f2_parser(
|
|
|
150
157
|
setattr(entry, f"{prefix}_button_2_time", button_2_time)
|
|
151
158
|
setattr(entry, f"{prefix}_button_3_time", button_3_time)
|
|
152
159
|
setattr(entry, f"{prefix}_backup_4_time", backup_4_time)
|
|
160
|
+
setattr(entry, f"{prefix}_reaction_times", reaction_times)
|
|
153
161
|
setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
|
|
154
162
|
|
|
163
|
+
# Anchor the H1 reason / H2 detail lines that follow to this DQ slot (see
|
|
164
|
+
# h_dq_parsers). File position is the only signal that survives a same-code
|
|
165
|
+
# prelim+finals double-DQ; clear it on a non-DQ result so a later orphaned
|
|
166
|
+
# H1/H2 does not attach to a stale slot.
|
|
167
|
+
opts[LAST_DQ_SLOT_KEY] = f"{prefix}_dq_info" if dq_info is not None else None
|
|
168
|
+
|
|
155
169
|
event.last_entry = entry
|
|
156
170
|
file.meet.last_event = (event_num, event)
|
|
157
171
|
|
|
@@ -172,7 +186,14 @@ def f3_parser(
|
|
|
172
186
|
# Out of swimmers
|
|
173
187
|
break
|
|
174
188
|
|
|
175
|
-
swimmer = file.meet.swimmers
|
|
189
|
+
swimmer = file.meet.swimmers.get(swimmer_meet_id)
|
|
190
|
+
if swimmer is None:
|
|
191
|
+
# The F3 leg references a swimmer meet-id with no D1 roster record in
|
|
192
|
+
# this file — seen in some incomplete meet exports (e.g. relay legs for
|
|
193
|
+
# athletes the roster section omits). Skip the leg rather than raising a
|
|
194
|
+
# KeyError; the relay keeps its other legs. Mirrors the tolerance the
|
|
195
|
+
# empty-swimmers and absent-leg-1 cases already get below.
|
|
196
|
+
continue
|
|
176
197
|
swimmer_leg = safe_cast(int, extract(line, 15 + offset, 1))
|
|
177
198
|
|
|
178
199
|
# Hy-Tek encodes legs 1..8; preserve the leg number as-is.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from hytek_parser._utils import extract
|
|
4
|
+
from hytek_parser.hy3.schemas import ParsedHytekFile
|
|
5
|
+
|
|
6
|
+
# Key under which the E2/F2 result parser records the DQ slot it just populated,
|
|
7
|
+
# so the H1 reason line and H2 detail line that immediately follow attach to that
|
|
8
|
+
# same slot. A swim DQ'd in both prelims and finals resolves to ONE shared entry
|
|
9
|
+
# with two populated slots; the result line preceding each round's H1/H2 in file
|
|
10
|
+
# order is the authoritative anchor for which slot those detail lines belong to.
|
|
11
|
+
# This holds even when both rounds carry the SAME DQ code (e.g. a false start in
|
|
12
|
+
# both prelims and finals): matching on the code alone cannot tell the two slots
|
|
13
|
+
# apart, but file position always can.
|
|
14
|
+
LAST_DQ_SLOT_KEY = "_last_dq_slot"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def h1_parser(
|
|
18
|
+
line: str, file: ParsedHytekFile, opts: dict[str, Any]
|
|
19
|
+
) -> ParsedHytekFile:
|
|
20
|
+
"""Parse an H1 line: the DQ reason string, routed to its own DQ slot.
|
|
21
|
+
|
|
22
|
+
Attaches the reason to the slot the immediately-preceding E2/F2 result line
|
|
23
|
+
populated (recorded in ``opts`` under ``LAST_DQ_SLOT_KEY``). That result line
|
|
24
|
+
is the authoritative anchor: a swim DQ'd in both prelims and finals carries
|
|
25
|
+
two populated slots on one shared entry, and file position — not the DQ code —
|
|
26
|
+
tells the two H1s apart, including when both rounds share the same code.
|
|
27
|
+
|
|
28
|
+
No-op when no slot is anchored or the anchored slot is unpopulated — the DQ
|
|
29
|
+
status and code are recorded by the E2/F2 result line regardless, so only the
|
|
30
|
+
human-readable reason string is ever at stake. Leaves the anchor in place so
|
|
31
|
+
the H2 detail line that follows attaches to the same slot.
|
|
32
|
+
"""
|
|
33
|
+
event_num, event = file.meet.last_event
|
|
34
|
+
entry = event.last_entry
|
|
35
|
+
|
|
36
|
+
info_str = extract(line, 5, 124) # Whitespace is stripped
|
|
37
|
+
|
|
38
|
+
slot = opts.get(LAST_DQ_SLOT_KEY)
|
|
39
|
+
if slot is not None:
|
|
40
|
+
info = getattr(entry, slot)
|
|
41
|
+
if info is not None:
|
|
42
|
+
info.info_str = info_str
|
|
43
|
+
|
|
44
|
+
event.last_entry = entry
|
|
45
|
+
file.meet.last_event = (event_num, event)
|
|
46
|
+
|
|
47
|
+
return file
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def h2_parser(
|
|
51
|
+
line: str, file: ParsedHytekFile, opts: dict[str, Any]
|
|
52
|
+
) -> ParsedHytekFile:
|
|
53
|
+
"""Parse an H2 line: the specific, human-readable DQ infraction detail.
|
|
54
|
+
|
|
55
|
+
Attaches the detail to the same slot the preceding E2/F2 result line anchored
|
|
56
|
+
(``LAST_DQ_SLOT_KEY``) — the slot h1_parser also wrote to. H2 cannot
|
|
57
|
+
self-identify its slot: its own 2-char code is the stroke/leg infraction (e.g.
|
|
58
|
+
"2L"), not the slot's ``DisqualificationCode`` (e.g. the relay-leg code "6A"),
|
|
59
|
+
so the anchor is the only reliable signal. No-op when no slot is anchored or
|
|
60
|
+
the anchored slot is unpopulated.
|
|
61
|
+
"""
|
|
62
|
+
event_num, event = file.meet.last_event
|
|
63
|
+
entry = event.last_entry
|
|
64
|
+
|
|
65
|
+
detail = extract(line, 5, 124) or None
|
|
66
|
+
|
|
67
|
+
slot = opts.get(LAST_DQ_SLOT_KEY)
|
|
68
|
+
if slot is not None:
|
|
69
|
+
info = getattr(entry, slot)
|
|
70
|
+
if info is not None:
|
|
71
|
+
info.info_str_detail = detail
|
|
72
|
+
|
|
73
|
+
event.last_entry = entry
|
|
74
|
+
file.meet.last_event = (event_num, event)
|
|
75
|
+
return file
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from datetime import date, datetime
|
|
2
|
-
from typing import Optional, Union
|
|
2
|
+
from typing import List, Optional, Union
|
|
3
3
|
|
|
4
4
|
from attrs import Factory, define, field
|
|
5
5
|
|
|
@@ -119,6 +119,8 @@ class EventEntry:
|
|
|
119
119
|
prelim_button_2_time: Optional[float] = None
|
|
120
120
|
prelim_button_3_time: Optional[float] = None
|
|
121
121
|
prelim_backup_4_time: Optional[float] = None
|
|
122
|
+
prelim_reaction_time: Optional[float] = None
|
|
123
|
+
prelim_reaction_times: Optional[List[Optional[float]]] = None
|
|
122
124
|
# col 96; semantics unverified — observed 'A'/'K'/blank
|
|
123
125
|
prelim_alt_time_code: Optional[str] = None
|
|
124
126
|
|
|
@@ -139,6 +141,8 @@ class EventEntry:
|
|
|
139
141
|
swimoff_button_2_time: Optional[float] = None
|
|
140
142
|
swimoff_button_3_time: Optional[float] = None
|
|
141
143
|
swimoff_backup_4_time: Optional[float] = None
|
|
144
|
+
swimoff_reaction_time: Optional[float] = None
|
|
145
|
+
swimoff_reaction_times: Optional[List[Optional[float]]] = None
|
|
142
146
|
# col 96; semantics unverified — observed 'A'/'K'/blank
|
|
143
147
|
swimoff_alt_time_code: Optional[str] = None
|
|
144
148
|
|
|
@@ -159,6 +163,8 @@ class EventEntry:
|
|
|
159
163
|
finals_button_2_time: Optional[float] = None
|
|
160
164
|
finals_button_3_time: Optional[float] = None
|
|
161
165
|
finals_backup_4_time: Optional[float] = None
|
|
166
|
+
finals_reaction_time: Optional[float] = None
|
|
167
|
+
finals_reaction_times: Optional[List[Optional[float]]] = None
|
|
162
168
|
# col 96; semantics unverified — observed 'A'/'K'/blank
|
|
163
169
|
finals_alt_time_code: Optional[str] = None
|
|
164
170
|
|
|
@@ -216,6 +222,8 @@ class EventEntry:
|
|
|
216
222
|
setattr(self, f"{course}_button_2_time", None)
|
|
217
223
|
setattr(self, f"{course}_button_3_time", None)
|
|
218
224
|
setattr(self, f"{course}_backup_4_time", None)
|
|
225
|
+
setattr(self, f"{course}_reaction_time", None)
|
|
226
|
+
setattr(self, f"{course}_reaction_times", None)
|
|
219
227
|
setattr(self, f"{course}_alt_time_code", None)
|
|
220
228
|
|
|
221
229
|
self.prelim_dq_info = None
|
|
@@ -256,7 +264,7 @@ class Event:
|
|
|
256
264
|
|
|
257
265
|
# ID info
|
|
258
266
|
number: str
|
|
259
|
-
distance:
|
|
267
|
+
distance: float
|
|
260
268
|
stroke: Stroke
|
|
261
269
|
course: Course
|
|
262
270
|
date_: Optional[date]
|
|
@@ -424,7 +432,7 @@ class Meet:
|
|
|
424
432
|
def get_or_create_event(
|
|
425
433
|
self,
|
|
426
434
|
number: str,
|
|
427
|
-
distance:
|
|
435
|
+
distance: float,
|
|
428
436
|
stroke: Stroke,
|
|
429
437
|
course: Course,
|
|
430
438
|
gender: Gender,
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
from typing import Optional, Union
|
|
2
|
-
|
|
3
|
-
from hytek_parser._utils import safe_cast, select_from_enum
|
|
4
|
-
from hytek_parser.hy3.enums import ReplacedTimeTimeCode
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def parse_time(raw_time: str) -> Union[float, ReplacedTimeTimeCode]:
|
|
8
|
-
"""Parse a time into either a number or time code.
|
|
9
|
-
|
|
10
|
-
Args:
|
|
11
|
-
raw_time (str): The time string extracted from the Hytek file.
|
|
12
|
-
|
|
13
|
-
Returns:
|
|
14
|
-
Union[float, ReplacedTimeTimeCode]: Either a numerical time or a time code.
|
|
15
|
-
"""
|
|
16
|
-
if (casted := safe_cast(float, raw_time, default=-1)) != -1:
|
|
17
|
-
# Number
|
|
18
|
-
return casted
|
|
19
|
-
else:
|
|
20
|
-
return select_from_enum(ReplacedTimeTimeCode, raw_time)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def parse_time_or_none(raw_time: str) -> Optional[float]:
|
|
24
|
-
"""Parse a timing field where 0.00/blank means 'not recorded'.
|
|
25
|
-
|
|
26
|
-
Unlike parse_time (which returns 0.0 for "0.00" and a ReplacedTimeTimeCode
|
|
27
|
-
for blank/non-numeric input), this returns None unless the value is a
|
|
28
|
-
positive float. Used for pad and backup-button times, where an unused
|
|
29
|
-
slot is written as 0.00 and should surface as None rather than 0.0.
|
|
30
|
-
"""
|
|
31
|
-
val = parse_time(raw_time)
|
|
32
|
-
return val if isinstance(val, float) and val > 0.0 else None
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from hytek_parser._utils import extract, select_from_enum
|
|
4
|
-
from hytek_parser.hy3.enums import DisqualificationCode
|
|
5
|
-
from hytek_parser.hy3.schemas import ParsedHytekFile
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def h1_parser(
|
|
9
|
-
line: str, file: ParsedHytekFile, opts: dict[str, Any]
|
|
10
|
-
) -> ParsedHytekFile:
|
|
11
|
-
"""Parse a G1 split time entry."""
|
|
12
|
-
# Get event
|
|
13
|
-
event_num, event = file.meet.last_event
|
|
14
|
-
entry = event.last_entry
|
|
15
|
-
|
|
16
|
-
# Get dq code and message
|
|
17
|
-
dq_code = select_from_enum(DisqualificationCode, extract(line, 3, 2))
|
|
18
|
-
dq_info = extract(line, 5, 124) # Whitespace is stripped
|
|
19
|
-
|
|
20
|
-
assert (
|
|
21
|
-
entry.prelim_dq_info or entry.swimoff_dq_info or entry.finals_dq_info
|
|
22
|
-
), "There must be a DQ for there to be an H1 line"
|
|
23
|
-
|
|
24
|
-
if entry.finals_dq_info:
|
|
25
|
-
# DQ happened in prelims
|
|
26
|
-
assert (
|
|
27
|
-
entry.finals_dq_info.code == dq_code
|
|
28
|
-
), "DQ Codes should match in the H1 line"
|
|
29
|
-
|
|
30
|
-
entry.finals_dq_info.info_str = dq_info
|
|
31
|
-
elif entry.swimoff_dq_info:
|
|
32
|
-
# DQ happened in prelims
|
|
33
|
-
assert (
|
|
34
|
-
entry.swimoff_dq_info.code == dq_code
|
|
35
|
-
), "DQ Codes should match in the H1 line"
|
|
36
|
-
|
|
37
|
-
entry.swimoff_dq_info.info_str = dq_info
|
|
38
|
-
elif entry.prelim_dq_info:
|
|
39
|
-
# DQ happened in prelims
|
|
40
|
-
assert (
|
|
41
|
-
entry.prelim_dq_info.code == dq_code
|
|
42
|
-
), "DQ Codes should match in the H1 line"
|
|
43
|
-
|
|
44
|
-
entry.prelim_dq_info.info_str = dq_info
|
|
45
|
-
|
|
46
|
-
event.last_entry = entry
|
|
47
|
-
file.meet.last_event = (event_num, event)
|
|
48
|
-
|
|
49
|
-
return file
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def h2_parser(
|
|
53
|
-
line: str, file: ParsedHytekFile, opts: dict[str, Any]
|
|
54
|
-
) -> ParsedHytekFile:
|
|
55
|
-
"""Parse an H2 line: the specific, human-readable DQ infraction detail.
|
|
56
|
-
|
|
57
|
-
Attaches the detail text to whichever DQ slot the preceding H1 populated
|
|
58
|
-
(finals -> swimoff -> prelim), mirroring h1_parser's resolution. Unlike H1,
|
|
59
|
-
the H2 2-char code is NOT asserted against the DQ code (it is a stroke/leg
|
|
60
|
-
sub-classification, not the same DisqualificationCode). No-op if no DQ slot
|
|
61
|
-
is set.
|
|
62
|
-
"""
|
|
63
|
-
event_num, event = file.meet.last_event
|
|
64
|
-
entry = event.last_entry
|
|
65
|
-
|
|
66
|
-
detail = extract(line, 5, 124) or None
|
|
67
|
-
|
|
68
|
-
if entry.finals_dq_info:
|
|
69
|
-
entry.finals_dq_info.info_str_detail = detail
|
|
70
|
-
elif entry.swimoff_dq_info:
|
|
71
|
-
entry.swimoff_dq_info.info_str_detail = detail
|
|
72
|
-
elif entry.prelim_dq_info:
|
|
73
|
-
entry.prelim_dq_info.info_str_detail = detail
|
|
74
|
-
|
|
75
|
-
event.last_entry = entry
|
|
76
|
-
file.meet.last_event = (event_num, event)
|
|
77
|
-
return file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|