hytek-parser 2.2.0__tar.gz → 2.4.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.2.0 → hytek_parser-2.4.0}/PKG-INFO +1 -1
- hytek_parser-2.4.0/hytek_parser/hy3/_utils.py +69 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/enums.py +31 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/__init__.py +2 -1
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/b_meet_parsers.py +2 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/e_event_parsers.py +7 -2
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/f_relay_parsers.py +19 -5
- hytek_parser-2.4.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +81 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/schemas.py +21 -1
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/pyproject.toml +1 -1
- hytek_parser-2.2.0/hytek_parser/hy3/_utils.py +0 -32
- hytek_parser-2.2.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +0 -49
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/LICENSE.md +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/README.md +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/__init__.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/_utils.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/export_xls/__init__.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/export_xls/_utils.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/export_xls/schemas.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/__init__.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/a_file_parsers.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/c_team_parsers.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/g_split_parsers.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3_parser.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hyv/__init__.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hyv/enums.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hyv/schemas.py +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/py.typed +0 -0
- {hytek_parser-2.2.0 → hytek_parser-2.4.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
|
|
|
@@ -9,7 +9,7 @@ from .d_swimmer_parsers import d1_parser
|
|
|
9
9
|
from .e_event_parsers import e1_parser, e2_parser
|
|
10
10
|
from .f_relay_parsers import f1_parser, f2_parser, f3_parser
|
|
11
11
|
from .g_split_parsers import g1_parser
|
|
12
|
-
from .h_dq_parsers import h1_parser
|
|
12
|
+
from .h_dq_parsers import h1_parser, h2_parser
|
|
13
13
|
|
|
14
14
|
__all__ = ["HY3_LINE_PARSERS"]
|
|
15
15
|
|
|
@@ -38,4 +38,5 @@ HY3_LINE_PARSERS: dict[
|
|
|
38
38
|
"G1": g1_parser,
|
|
39
39
|
# DQ info
|
|
40
40
|
"H1": h1_parser,
|
|
41
|
+
"H2": h2_parser,
|
|
41
42
|
}
|
|
@@ -33,6 +33,8 @@ def b2_parser(
|
|
|
33
33
|
meet.masters = extract(line, 94, 2) == "06"
|
|
34
34
|
meet.type_ = select_from_enum(MeetType, extract(line, 97, 2))
|
|
35
35
|
meet.course = select_from_enum(Course, extract(line, 99, 1))
|
|
36
|
+
meet.notes = extract(line, 3, 45) or None
|
|
37
|
+
meet.sanction_number = extract(line, 109, 20) or None
|
|
36
38
|
|
|
37
39
|
file.meet = meet
|
|
38
40
|
return file
|
|
@@ -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,
|
|
@@ -66,8 +66,9 @@ def e1_parser(
|
|
|
66
66
|
# Meet Division: col 77-79 in most MM versions, col 92-93 in MM4/MM5-7.0Fa.
|
|
67
67
|
# The two are mutually exclusive; col 77 takes precedence.
|
|
68
68
|
meet_division = extract(line, 77, 3) or extract(line, 92, 2) or None
|
|
69
|
+
exhibition = extract(line, 84, 1) == "X"
|
|
69
70
|
|
|
70
|
-
event.get_or_create_entry(
|
|
71
|
+
entry = event.get_or_create_entry(
|
|
71
72
|
swimmers=entry_swimmers,
|
|
72
73
|
relay=False,
|
|
73
74
|
event_number=entry_event_number,
|
|
@@ -76,6 +77,7 @@ def e1_parser(
|
|
|
76
77
|
converted_seed_time=entry_converted_seed_time,
|
|
77
78
|
converted_seed_time_course=entry_converted_seed_time_course,
|
|
78
79
|
meet_division=meet_division,
|
|
80
|
+
exhibition=exhibition,
|
|
79
81
|
)
|
|
80
82
|
|
|
81
83
|
# Update event
|
|
@@ -115,6 +117,8 @@ def e2_parser(
|
|
|
115
117
|
button_3_time = parse_time_or_none(extract(line, 55, 8))
|
|
116
118
|
backup_4_time = parse_time_or_none(extract(line, 75, 8))
|
|
117
119
|
alt_time_code = extract(line, 96, 1) or None # observed: 'A' / 'K' / blank
|
|
120
|
+
# col 83-87. Signed; sentinels are blank / 0.00 in any sign spelling.
|
|
121
|
+
reaction_time = parse_reaction_time(extract(line, 83, 5))
|
|
118
122
|
|
|
119
123
|
raw_date = extract(line, 88, 8).strip()
|
|
120
124
|
date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
|
|
@@ -148,6 +152,7 @@ def e2_parser(
|
|
|
148
152
|
setattr(entry, f"{prefix}_button_2_time", button_2_time)
|
|
149
153
|
setattr(entry, f"{prefix}_button_3_time", button_3_time)
|
|
150
154
|
setattr(entry, f"{prefix}_backup_4_time", backup_4_time)
|
|
155
|
+
setattr(entry, f"{prefix}_reaction_time", reaction_time)
|
|
151
156
|
setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
|
|
152
157
|
|
|
153
158
|
event.last_entry = entry
|
|
@@ -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,
|
|
@@ -108,9 +108,11 @@ def f2_parser(
|
|
|
108
108
|
overall_place = safe_cast(int, extract(line, 30, 4))
|
|
109
109
|
|
|
110
110
|
# previously-dropped F2 timing fields. The five timing-column
|
|
111
|
-
# offsets are IDENTICAL to e2_parser
|
|
112
|
-
#
|
|
113
|
-
# F2
|
|
111
|
+
# offsets are IDENTICAL to e2_parser. F2 then carries FOUR reaction-time
|
|
112
|
+
# slots at cols 83-102 (5 chars each) where E2 carries one at 83-87, which
|
|
113
|
+
# is why F2's date sits at col 103 and its alt_time_code at col 111.
|
|
114
|
+
# Slot 1 is the leadoff block start; slots 2-4 are exchange takeovers and
|
|
115
|
+
# are legitimately negative when a swimmer leaves early.
|
|
114
116
|
pad_time = parse_time_or_none(extract(line, 63, 12))
|
|
115
117
|
button_1_time = parse_time_or_none(extract(line, 39, 8))
|
|
116
118
|
button_2_time = parse_time_or_none(extract(line, 47, 8))
|
|
@@ -118,6 +120,10 @@ def f2_parser(
|
|
|
118
120
|
backup_4_time = parse_time_or_none(extract(line, 75, 8))
|
|
119
121
|
alt_time_code = extract(line, 111, 1) or None # F2 offset; observed: 'A'/'K'/blank
|
|
120
122
|
|
|
123
|
+
reaction_times = [
|
|
124
|
+
parse_reaction_time(extract(line, 83 + 5 * i, 5)) for i in range(4)
|
|
125
|
+
]
|
|
126
|
+
|
|
121
127
|
raw_date = extract(line, 103, 8).strip()
|
|
122
128
|
date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
|
|
123
129
|
|
|
@@ -150,6 +156,7 @@ def f2_parser(
|
|
|
150
156
|
setattr(entry, f"{prefix}_button_2_time", button_2_time)
|
|
151
157
|
setattr(entry, f"{prefix}_button_3_time", button_3_time)
|
|
152
158
|
setattr(entry, f"{prefix}_backup_4_time", backup_4_time)
|
|
159
|
+
setattr(entry, f"{prefix}_reaction_times", reaction_times)
|
|
153
160
|
setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
|
|
154
161
|
|
|
155
162
|
event.last_entry = entry
|
|
@@ -172,7 +179,14 @@ def f3_parser(
|
|
|
172
179
|
# Out of swimmers
|
|
173
180
|
break
|
|
174
181
|
|
|
175
|
-
swimmer = file.meet.swimmers
|
|
182
|
+
swimmer = file.meet.swimmers.get(swimmer_meet_id)
|
|
183
|
+
if swimmer is None:
|
|
184
|
+
# The F3 leg references a swimmer meet-id with no D1 roster record in
|
|
185
|
+
# this file — seen in some incomplete meet exports (e.g. relay legs for
|
|
186
|
+
# athletes the roster section omits). Skip the leg rather than raising a
|
|
187
|
+
# KeyError; the relay keeps its other legs. Mirrors the tolerance the
|
|
188
|
+
# empty-swimmers and absent-leg-1 cases already get below.
|
|
189
|
+
continue
|
|
176
190
|
swimmer_leg = safe_cast(int, extract(line, 15 + offset, 1))
|
|
177
191
|
|
|
178
192
|
# Hy-Tek encodes legs 1..8; preserve the leg number as-is.
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
# No-op if the entry carries no DQ slot to attach to — mirrors h2_parser.
|
|
21
|
+
# An H1 can appear whose last_entry is not the DQ'd swim it describes (e.g. a
|
|
22
|
+
# relay DQ, or a non-DQ entry emitted between the DQ result and its H1); skip
|
|
23
|
+
# the detail rather than raising. The DQ result itself is unaffected, only the
|
|
24
|
+
# human-readable reason string is dropped.
|
|
25
|
+
if not (entry.prelim_dq_info or entry.swimoff_dq_info or entry.finals_dq_info):
|
|
26
|
+
return file
|
|
27
|
+
|
|
28
|
+
if entry.finals_dq_info:
|
|
29
|
+
# DQ happened in prelims
|
|
30
|
+
assert (
|
|
31
|
+
entry.finals_dq_info.code == dq_code
|
|
32
|
+
), "DQ Codes should match in the H1 line"
|
|
33
|
+
|
|
34
|
+
entry.finals_dq_info.info_str = dq_info
|
|
35
|
+
elif entry.swimoff_dq_info:
|
|
36
|
+
# DQ happened in prelims
|
|
37
|
+
assert (
|
|
38
|
+
entry.swimoff_dq_info.code == dq_code
|
|
39
|
+
), "DQ Codes should match in the H1 line"
|
|
40
|
+
|
|
41
|
+
entry.swimoff_dq_info.info_str = dq_info
|
|
42
|
+
elif entry.prelim_dq_info:
|
|
43
|
+
# DQ happened in prelims
|
|
44
|
+
assert (
|
|
45
|
+
entry.prelim_dq_info.code == dq_code
|
|
46
|
+
), "DQ Codes should match in the H1 line"
|
|
47
|
+
|
|
48
|
+
entry.prelim_dq_info.info_str = dq_info
|
|
49
|
+
|
|
50
|
+
event.last_entry = entry
|
|
51
|
+
file.meet.last_event = (event_num, event)
|
|
52
|
+
|
|
53
|
+
return file
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def h2_parser(
|
|
57
|
+
line: str, file: ParsedHytekFile, opts: dict[str, Any]
|
|
58
|
+
) -> ParsedHytekFile:
|
|
59
|
+
"""Parse an H2 line: the specific, human-readable DQ infraction detail.
|
|
60
|
+
|
|
61
|
+
Attaches the detail text to whichever DQ slot the preceding H1 populated
|
|
62
|
+
(finals -> swimoff -> prelim), mirroring h1_parser's resolution. Unlike H1,
|
|
63
|
+
the H2 2-char code is NOT asserted against the DQ code (it is a stroke/leg
|
|
64
|
+
sub-classification, not the same DisqualificationCode). No-op if no DQ slot
|
|
65
|
+
is set.
|
|
66
|
+
"""
|
|
67
|
+
event_num, event = file.meet.last_event
|
|
68
|
+
entry = event.last_entry
|
|
69
|
+
|
|
70
|
+
detail = extract(line, 5, 124) or None
|
|
71
|
+
|
|
72
|
+
if entry.finals_dq_info:
|
|
73
|
+
entry.finals_dq_info.info_str_detail = detail
|
|
74
|
+
elif entry.swimoff_dq_info:
|
|
75
|
+
entry.swimoff_dq_info.info_str_detail = detail
|
|
76
|
+
elif entry.prelim_dq_info:
|
|
77
|
+
entry.prelim_dq_info.info_str_detail = detail
|
|
78
|
+
|
|
79
|
+
event.last_entry = entry
|
|
80
|
+
file.meet.last_event = (event_num, event)
|
|
81
|
+
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
|
|
|
@@ -84,6 +84,7 @@ class DisqualificationInfo:
|
|
|
84
84
|
|
|
85
85
|
code: DisqualificationCode
|
|
86
86
|
info_str: Optional[str] = None
|
|
87
|
+
info_str_detail: Optional[str] = None
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
@define(init=False)
|
|
@@ -118,6 +119,8 @@ class EventEntry:
|
|
|
118
119
|
prelim_button_2_time: Optional[float] = None
|
|
119
120
|
prelim_button_3_time: Optional[float] = None
|
|
120
121
|
prelim_backup_4_time: Optional[float] = None
|
|
122
|
+
prelim_reaction_time: Optional[float] = None
|
|
123
|
+
prelim_reaction_times: Optional[List[Optional[float]]] = None
|
|
121
124
|
# col 96; semantics unverified — observed 'A'/'K'/blank
|
|
122
125
|
prelim_alt_time_code: Optional[str] = None
|
|
123
126
|
|
|
@@ -138,6 +141,8 @@ class EventEntry:
|
|
|
138
141
|
swimoff_button_2_time: Optional[float] = None
|
|
139
142
|
swimoff_button_3_time: Optional[float] = None
|
|
140
143
|
swimoff_backup_4_time: Optional[float] = None
|
|
144
|
+
swimoff_reaction_time: Optional[float] = None
|
|
145
|
+
swimoff_reaction_times: Optional[List[Optional[float]]] = None
|
|
141
146
|
# col 96; semantics unverified — observed 'A'/'K'/blank
|
|
142
147
|
swimoff_alt_time_code: Optional[str] = None
|
|
143
148
|
|
|
@@ -158,6 +163,8 @@ class EventEntry:
|
|
|
158
163
|
finals_button_2_time: Optional[float] = None
|
|
159
164
|
finals_button_3_time: Optional[float] = None
|
|
160
165
|
finals_backup_4_time: Optional[float] = None
|
|
166
|
+
finals_reaction_time: Optional[float] = None
|
|
167
|
+
finals_reaction_times: Optional[List[Optional[float]]] = None
|
|
161
168
|
# col 96; semantics unverified — observed 'A'/'K'/blank
|
|
162
169
|
finals_alt_time_code: Optional[str] = None
|
|
163
170
|
|
|
@@ -171,6 +178,7 @@ class EventEntry:
|
|
|
171
178
|
# or numeric ('0'/'1'/'2'/'3'). Stored at cols 77-79 in most MM versions,
|
|
172
179
|
# cols 92-93 in MM4/MM5-7.0Fa (read with col-77 precedence).
|
|
173
180
|
meet_division: Optional[str] = None
|
|
181
|
+
exhibition: bool = False
|
|
174
182
|
|
|
175
183
|
def __init__(
|
|
176
184
|
self,
|
|
@@ -184,6 +192,7 @@ class EventEntry:
|
|
|
184
192
|
relay_team_id: Optional[str] = None,
|
|
185
193
|
relay_swim_team_code: Optional[str] = None,
|
|
186
194
|
meet_division: Optional[str] = None,
|
|
195
|
+
exhibition: bool = False,
|
|
187
196
|
) -> None:
|
|
188
197
|
self.swimmers = swimmers
|
|
189
198
|
self.relay = relay
|
|
@@ -195,6 +204,7 @@ class EventEntry:
|
|
|
195
204
|
self.relay_team_id = relay_team_id
|
|
196
205
|
self.relay_swim_team_code = relay_swim_team_code
|
|
197
206
|
self.meet_division = meet_division
|
|
207
|
+
self.exhibition = exhibition
|
|
198
208
|
|
|
199
209
|
for course in ("prelim", "swimoff", "finals"):
|
|
200
210
|
setattr(self, f"{course}_time", None)
|
|
@@ -212,6 +222,8 @@ class EventEntry:
|
|
|
212
222
|
setattr(self, f"{course}_button_2_time", None)
|
|
213
223
|
setattr(self, f"{course}_button_3_time", None)
|
|
214
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)
|
|
215
227
|
setattr(self, f"{course}_alt_time_code", None)
|
|
216
228
|
|
|
217
229
|
self.prelim_dq_info = None
|
|
@@ -283,6 +295,7 @@ class Event:
|
|
|
283
295
|
relay_team_id: Optional[str] = None,
|
|
284
296
|
relay_swim_team_code: Optional[str] = None,
|
|
285
297
|
meet_division: Optional[str] = None,
|
|
298
|
+
exhibition: bool = False,
|
|
286
299
|
) -> EventEntry:
|
|
287
300
|
"""Get an event entry or create one if needed."""
|
|
288
301
|
entry = EventEntry(
|
|
@@ -296,6 +309,7 @@ class Event:
|
|
|
296
309
|
relay_team_id=relay_team_id,
|
|
297
310
|
relay_swim_team_code=relay_swim_team_code,
|
|
298
311
|
meet_division=meet_division,
|
|
312
|
+
exhibition=exhibition,
|
|
299
313
|
)
|
|
300
314
|
if self.entries and self.entries[-1].same_swimmer_entry_as(entry):
|
|
301
315
|
# P/F entries always listed together: a swimmer (individuals) or a
|
|
@@ -342,6 +356,12 @@ class Meet:
|
|
|
342
356
|
swimmers: dict[int, Swimmer]
|
|
343
357
|
events: dict[str, Event]
|
|
344
358
|
|
|
359
|
+
# Extended info (optional; populated by later parser stages). Kept at the
|
|
360
|
+
# end of the field block (not next to `facility`/`country`) because attrs
|
|
361
|
+
# forbids a defaulted field preceding the non-default fields above.
|
|
362
|
+
sanction_number: Optional[str] = None
|
|
363
|
+
notes: Optional[str] = None
|
|
364
|
+
|
|
345
365
|
# Bookeeping
|
|
346
366
|
_last_team: tuple[str, Team] = field(default=None)
|
|
347
367
|
_last_event: tuple[str, Event] = field(default=None)
|
|
@@ -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,49 +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
|
|
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.2.0 → hytek_parser-2.4.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
|