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.
Files changed (30) hide show
  1. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/PKG-INFO +1 -1
  2. hytek_parser-3.0.0/hytek_parser/hy3/_utils.py +69 -0
  3. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/enums.py +31 -0
  4. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/e_event_parsers.py +12 -2
  5. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/f_relay_parsers.py +27 -6
  6. hytek_parser-3.0.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +75 -0
  7. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/schemas.py +11 -3
  8. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/pyproject.toml +1 -1
  9. hytek_parser-2.3.0/hytek_parser/hy3/_utils.py +0 -32
  10. hytek_parser-2.3.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +0 -77
  11. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/LICENSE.md +0 -0
  12. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/README.md +0 -0
  13. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/__init__.py +0 -0
  14. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/_utils.py +0 -0
  15. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/export_xls/__init__.py +0 -0
  16. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/export_xls/_utils.py +0 -0
  17. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/export_xls/schemas.py +0 -0
  18. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/__init__.py +0 -0
  19. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/__init__.py +0 -0
  20. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/a_file_parsers.py +0 -0
  21. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/b_meet_parsers.py +0 -0
  22. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/c_team_parsers.py +0 -0
  23. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py +0 -0
  24. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3/line_parsers/g_split_parsers.py +0 -0
  25. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hy3_parser.py +0 -0
  26. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hyv/__init__.py +0 -0
  27. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hyv/enums.py +0 -0
  28. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/hyv/schemas.py +0 -0
  29. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/py.typed +0 -0
  30. {hytek_parser-2.3.0 → hytek_parser-3.0.0}/hytek_parser/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hytek-parser
3
- Version: 2.3.0
3
+ Version: 3.0.0
4
4
  Summary: Parsers for the files produced by Hytek's Meet Manager.
5
5
  Home-page: https://www.github.com/SwimComm/hytek-parser
6
6
  License: MIT
@@ -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(int, extract(line, 16, 6))
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(int, extract(line, 16, 6))
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; only alt_time_code differs because F2
112
- # has a 15-column gap before its date field (date at col 103, not 88).
113
- # F2 alt_time_code lives at col 111, not col 96.
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[swimmer_meet_id]
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: int
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: int,
435
+ distance: float,
428
436
  stroke: Stroke,
429
437
  course: Course,
430
438
  gender: Gender,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hytek-parser"
3
- version = "2.3.0"
3
+ version = "3.0.0"
4
4
  description = "Parsers for the files produced by Hytek's Meet Manager."
5
5
  license = "MIT"
6
6
  authors = ["Nino Maruszewski <nino.maruszewski@hotmail.com>"]
@@ -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