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.
Files changed (30) hide show
  1. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/PKG-INFO +1 -1
  2. hytek_parser-2.4.0/hytek_parser/hy3/_utils.py +69 -0
  3. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/enums.py +31 -0
  4. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/__init__.py +2 -1
  5. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/b_meet_parsers.py +2 -0
  6. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/e_event_parsers.py +7 -2
  7. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/f_relay_parsers.py +19 -5
  8. hytek_parser-2.4.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +81 -0
  9. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/schemas.py +21 -1
  10. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/pyproject.toml +1 -1
  11. hytek_parser-2.2.0/hytek_parser/hy3/_utils.py +0 -32
  12. hytek_parser-2.2.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +0 -49
  13. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/LICENSE.md +0 -0
  14. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/README.md +0 -0
  15. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/__init__.py +0 -0
  16. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/_utils.py +0 -0
  17. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/export_xls/__init__.py +0 -0
  18. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/export_xls/_utils.py +0 -0
  19. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/export_xls/schemas.py +0 -0
  20. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/__init__.py +0 -0
  21. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/a_file_parsers.py +0 -0
  22. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/c_team_parsers.py +0 -0
  23. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py +0 -0
  24. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3/line_parsers/g_split_parsers.py +0 -0
  25. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hy3_parser.py +0 -0
  26. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hyv/__init__.py +0 -0
  27. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hyv/enums.py +0 -0
  28. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/hyv/schemas.py +0 -0
  29. {hytek_parser-2.2.0 → hytek_parser-2.4.0}/hytek_parser/py.typed +0 -0
  30. {hytek_parser-2.2.0 → hytek_parser-2.4.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.2.0
3
+ Version: 2.4.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
 
@@ -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; 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.
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[swimmer_meet_id]
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,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hytek-parser"
3
- version = "2.2.0"
3
+ version = "2.4.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,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