hytek-parser 2.0.0__tar.gz → 2.2.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 (28) hide show
  1. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/PKG-INFO +1 -1
  2. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/__init__.py +3 -1
  3. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/_utils.py +13 -1
  4. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/c_team_parsers.py +22 -5
  5. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py +9 -0
  6. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/e_event_parsers.py +23 -2
  7. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/f_relay_parsers.py +18 -2
  8. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/schemas.py +67 -3
  9. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/pyproject.toml +1 -1
  10. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/LICENSE.md +0 -0
  11. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/README.md +0 -0
  12. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/_utils.py +0 -0
  13. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/export_xls/__init__.py +0 -0
  14. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/export_xls/_utils.py +0 -0
  15. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/export_xls/schemas.py +0 -0
  16. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/__init__.py +0 -0
  17. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/enums.py +0 -0
  18. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/__init__.py +0 -0
  19. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/a_file_parsers.py +0 -0
  20. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/b_meet_parsers.py +0 -0
  21. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/g_split_parsers.py +0 -0
  22. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3/line_parsers/h_dq_parsers.py +0 -0
  23. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hy3_parser.py +0 -0
  24. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hyv/__init__.py +0 -0
  25. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hyv/enums.py +0 -0
  26. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/hyv/schemas.py +0 -0
  27. {hytek_parser-2.0.0 → hytek_parser-2.2.0}/hytek_parser/py.typed +0 -0
  28. {hytek_parser-2.0.0 → hytek_parser-2.2.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.0.0
3
+ Version: 2.2.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
@@ -1,4 +1,6 @@
1
1
  """A Hytek hy3 file parser."""
2
+ from importlib.metadata import version
3
+
2
4
  from .export_xls import ExportXlsParseError, parse_event_export_xls
3
5
  from .hy3 import HY3_LINE_PARSERS
4
6
  from .hy3_parser import parse_hy3
@@ -15,4 +17,4 @@ __all__ = [
15
17
  ]
16
18
  __author__ = "Nino Maruszewski"
17
19
  __license__ = "MIT"
18
- __version__ = "2.0.0"
20
+ __version__ = version(__package__)
@@ -1,4 +1,4 @@
1
- from typing import Union
1
+ from typing import Optional, Union
2
2
 
3
3
  from hytek_parser._utils import safe_cast, select_from_enum
4
4
  from hytek_parser.hy3.enums import ReplacedTimeTimeCode
@@ -18,3 +18,15 @@ def parse_time(raw_time: str) -> Union[float, ReplacedTimeTimeCode]:
18
18
  return casted
19
19
  else:
20
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
@@ -26,8 +26,24 @@ def c1_parser(
26
26
  # Then truncate to 5 chars
27
27
  team_code = "".join(TEAM_CODE_REGEX.findall(team_name.upper()))[:5]
28
28
 
29
+ # LSC code at C1 cols 54-55 (2 chars, e.g. 'NE', 'PC', 'CA');
30
+ # populates the previously-unset Team.region field.
31
+ # Bug fix: was extract(54, 3), which grabbed the first letter of the contact
32
+ # name at col 56, corrupting ~19% of teams' LSC codes.
33
+ region = extract(line, 54, 2) or None
34
+
35
+ # C1 contact name slots: cols 56-85 (contact_name_1) and 86-115 (contact_name_2).
36
+ # Often identical; blank slot → None.
37
+ contact_name_1 = extract(line, 56, 30) or None
38
+ contact_name_2 = extract(line, 86, 30) or None
39
+
29
40
  file.meet.get_or_create_team(
30
- name=team_name, short_name=team_short_name, code=team_code
41
+ name=team_name,
42
+ short_name=team_short_name,
43
+ code=team_code,
44
+ region=region,
45
+ contact_name_1=contact_name_1,
46
+ contact_name_2=contact_name_2,
31
47
  )
32
48
  return file
33
49
 
@@ -39,7 +55,11 @@ def c2_parser(
39
55
  # Get the last team
40
56
  team_code, team = file.meet.last_team
41
57
 
42
- team.address_1 = extract(line, 3, 60)
58
+ # C2 has two 30-char address lines: line 1 (cols 3-32) is a free-text
59
+ # "c/o"/attention line (often a contact person, sometimes the org), line 2
60
+ # (cols 33-62) is the street. They were previously read as one 60-char blob.
61
+ team.address_1 = extract(line, 3, 30)
62
+ team.address_2 = extract(line, 33, 30)
43
63
  team.city = extract(line, 63, 30)
44
64
  team.state = extract(line, 93, 2)
45
65
  team.zip_code = extract(line, 95, 10)
@@ -49,9 +69,6 @@ def c2_parser(
49
69
  else:
50
70
  team.country = opts["default_country"]
51
71
 
52
- # TODO: Team region
53
- team.region = "Unknown"
54
-
55
72
  file.meet.last_team = (team_code, team)
56
73
  return file
57
74
 
@@ -24,6 +24,15 @@ def d1_parser(
24
24
  swimmer.date_of_birth = date_or_none(extract(line, 89, 8))
25
25
  swimmer.age = int(extract(line, 97, 3))
26
26
 
27
+ # previously dropped D1 fields. Capture both per spec.
28
+ # citizenship: cols 113-115 (3 chars). Observed: 'USA' or blank.
29
+ # status: col 125 (1 char). Athlete status; observed 'N' (Normal) or blank.
30
+ swimmer.citizenship = extract(line, 113, 3) or None
31
+ swimmer.status = extract(line, 125, 1) or None
32
+ # class_year: cols 100-101 (2 chars). "Fr"/"So"/"Jr"/"Sr" school
33
+ # class in HS-meet exports; other data / blank in club exports.
34
+ swimmer.class_year = extract(line, 100, 2) or None
35
+
27
36
  swimmer.team_code = team_code
28
37
 
29
38
  file.meet.add_swimmer(swimmer)
@@ -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
5
+ from hytek_parser.hy3._utils import parse_time, parse_time_or_none
6
6
  from hytek_parser.hy3.enums import (
7
7
  Course,
8
8
  DisqualificationCode,
@@ -63,6 +63,10 @@ def e1_parser(
63
63
  entry_converted_seed_time = parse_time(extract(line, 43, 8))
64
64
  entry_converted_seed_time_course = event.course
65
65
 
66
+ # Meet Division: col 77-79 in most MM versions, col 92-93 in MM4/MM5-7.0Fa.
67
+ # The two are mutually exclusive; col 77 takes precedence.
68
+ meet_division = extract(line, 77, 3) or extract(line, 92, 2) or None
69
+
66
70
  event.get_or_create_entry(
67
71
  swimmers=entry_swimmers,
68
72
  relay=False,
@@ -71,6 +75,7 @@ def e1_parser(
71
75
  seed_course=entry_seed_course,
72
76
  converted_seed_time=entry_converted_seed_time,
73
77
  converted_seed_time_course=entry_converted_seed_time_course,
78
+ meet_division=meet_division,
74
79
  )
75
80
 
76
81
  # Update event
@@ -100,7 +105,17 @@ def e2_parser(
100
105
  heat_place = safe_cast(int, extract(line, 27, 3))
101
106
  overall_place = safe_cast(int, extract(line, 30, 4))
102
107
 
103
- # Skipping over pad/plunger times since they are not that useful
108
+ # previously-dropped E2 timing fields.
109
+ # See spec for column-offset derivation and failure-mode evidence
110
+ # (touchpad turn-touch misattribution in short SCY sprints).
111
+ # parse_time_or_none returns None for "0.00" and blank/non-numeric fields.
112
+ pad_time = parse_time_or_none(extract(line, 63, 12))
113
+ button_1_time = parse_time_or_none(extract(line, 39, 8))
114
+ button_2_time = parse_time_or_none(extract(line, 47, 8))
115
+ button_3_time = parse_time_or_none(extract(line, 55, 8))
116
+ backup_4_time = parse_time_or_none(extract(line, 75, 8))
117
+ alt_time_code = extract(line, 96, 1) or None # observed: 'A' / 'K' / blank
118
+
104
119
  raw_date = extract(line, 88, 8).strip()
105
120
  date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
106
121
 
@@ -128,6 +143,12 @@ def e2_parser(
128
143
  setattr(entry, f"{prefix}_heat_place", heat_place)
129
144
  setattr(entry, f"{prefix}_overall_place", overall_place)
130
145
  setattr(entry, f"{prefix}_date", date_)
146
+ setattr(entry, f"{prefix}_pad_time", pad_time)
147
+ setattr(entry, f"{prefix}_button_1_time", button_1_time)
148
+ setattr(entry, f"{prefix}_button_2_time", button_2_time)
149
+ setattr(entry, f"{prefix}_button_3_time", button_3_time)
150
+ setattr(entry, f"{prefix}_backup_4_time", backup_4_time)
151
+ setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
131
152
 
132
153
  event.last_entry = entry
133
154
  file.meet.last_event = (event_num, event)
@@ -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
5
+ from hytek_parser.hy3._utils import parse_time, parse_time_or_none
6
6
  from hytek_parser.hy3.enums import (
7
7
  Course,
8
8
  DisqualificationCode,
@@ -107,7 +107,17 @@ def f2_parser(
107
107
  heat_place = safe_cast(int, extract(line, 27, 3))
108
108
  overall_place = safe_cast(int, extract(line, 30, 4))
109
109
 
110
- # Skipping over pad/plunger times since they are not that useful
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.
114
+ pad_time = parse_time_or_none(extract(line, 63, 12))
115
+ button_1_time = parse_time_or_none(extract(line, 39, 8))
116
+ button_2_time = parse_time_or_none(extract(line, 47, 8))
117
+ button_3_time = parse_time_or_none(extract(line, 55, 8))
118
+ backup_4_time = parse_time_or_none(extract(line, 75, 8))
119
+ alt_time_code = extract(line, 111, 1) or None # F2 offset; observed: 'A'/'K'/blank
120
+
111
121
  raw_date = extract(line, 103, 8).strip()
112
122
  date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
113
123
 
@@ -135,6 +145,12 @@ def f2_parser(
135
145
  setattr(entry, f"{prefix}_heat_place", heat_place)
136
146
  setattr(entry, f"{prefix}_overall_place", overall_place)
137
147
  setattr(entry, f"{prefix}_date", date_)
148
+ setattr(entry, f"{prefix}_pad_time", pad_time)
149
+ setattr(entry, f"{prefix}_button_1_time", button_1_time)
150
+ setattr(entry, f"{prefix}_button_2_time", button_2_time)
151
+ setattr(entry, f"{prefix}_button_3_time", button_3_time)
152
+ setattr(entry, f"{prefix}_backup_4_time", backup_4_time)
153
+ setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
138
154
 
139
155
  event.last_entry = entry
140
156
  file.meet.last_event = (event_num, event)
@@ -36,6 +36,17 @@ class Swimmer:
36
36
  usa_swimming_id: str
37
37
  team_code: str # Team 5-letter code
38
38
 
39
+ # D1 fields previously dropped by the parser
40
+ citizenship: Optional[str] = None
41
+ # col 125; athlete status (Hytek MM "Status" field). Raw 1-char code — only
42
+ # "N" (Normal) or blank observed across ~9.25M records; other GUI statuses
43
+ # (Impaired/Exhibition/Foreigner/Rookie) never seen in data, so left as a
44
+ # raw code rather than an enum.
45
+ status: Optional[str] = None
46
+ # cols 100-101; "Fr"/"So"/"Jr"/"Sr" school class in HS-meet exports,
47
+ # other data / blank in club exports.
48
+ class_year: Optional[str] = None
49
+
39
50
 
40
51
  @define
41
52
  class Team:
@@ -53,13 +64,15 @@ class Team:
53
64
  state: str
54
65
  zip_code: str
55
66
  country: str
56
- region: str
67
+ region: Optional[str]
57
68
 
58
69
  # Contact info
59
70
  daytime_phone: str
60
71
  evening_phone: str
61
72
  fax: str
62
73
  email: str
74
+ contact_name_1: Optional[str] = None
75
+ contact_name_2: Optional[str] = None
63
76
 
64
77
  # Swimmers
65
78
  swimmers: dict[int, Swimmer] = Factory(dict)
@@ -99,6 +112,14 @@ class EventEntry:
99
112
  prelim_heat_place: Optional[int] = None
100
113
  prelim_overall_place: Optional[int] = None
101
114
  prelim_date: Optional[date] = None
115
+ # E2 fields previously dropped (pad + 3 buttons + backup_4 + alt code)
116
+ prelim_pad_time: Optional[float] = None
117
+ prelim_button_1_time: Optional[float] = None
118
+ prelim_button_2_time: Optional[float] = None
119
+ prelim_button_3_time: Optional[float] = None
120
+ prelim_backup_4_time: Optional[float] = None
121
+ # col 96; semantics unverified — observed 'A'/'K'/blank
122
+ prelim_alt_time_code: Optional[str] = None
102
123
 
103
124
  # Swimoff Time info
104
125
  swimoff_time: Optional[Union[float, ReplacedTimeTimeCode]] = None
@@ -111,6 +132,14 @@ class EventEntry:
111
132
  swimoff_heat_place: Optional[int] = None
112
133
  swimoff_overall_place: Optional[int] = None
113
134
  swimoff_date: Optional[date] = None
135
+ # E2 fields previously dropped (pad + 3 buttons + backup_4 + alt code)
136
+ swimoff_pad_time: Optional[float] = None
137
+ swimoff_button_1_time: Optional[float] = None
138
+ swimoff_button_2_time: Optional[float] = None
139
+ swimoff_button_3_time: Optional[float] = None
140
+ swimoff_backup_4_time: Optional[float] = None
141
+ # col 96; semantics unverified — observed 'A'/'K'/blank
142
+ swimoff_alt_time_code: Optional[str] = None
114
143
 
115
144
  # Finals time info
116
145
  finals_time: Optional[Union[float, ReplacedTimeTimeCode]] = None
@@ -123,11 +152,26 @@ class EventEntry:
123
152
  finals_heat_place: Optional[int] = None
124
153
  finals_overall_place: Optional[int] = None
125
154
  finals_date: Optional[date] = None
155
+ # E2 fields previously dropped (pad + 3 buttons + backup_4 + alt code)
156
+ finals_pad_time: Optional[float] = None
157
+ finals_button_1_time: Optional[float] = None
158
+ finals_button_2_time: Optional[float] = None
159
+ finals_button_3_time: Optional[float] = None
160
+ finals_backup_4_time: Optional[float] = None
161
+ # col 96; semantics unverified — observed 'A'/'K'/blank
162
+ finals_alt_time_code: Optional[str] = None
126
163
 
127
164
  # Relay attribution (moved from Event — set by f1_parser)
128
165
  relay_team_id: Optional[str] = None
129
166
  relay_swim_team_code: Optional[str] = None
130
167
 
168
+ # Meet Division. The host-configured division label for the
169
+ # entry: 'JV'/'VR' (varsity-style), classification codes ('A'/'AA'/'AAA'/'BB'),
170
+ # HS enrollment classes ('5A'/'4A'/'3A'), age-group/level codes ('AG'/'SR'),
171
+ # or numeric ('0'/'1'/'2'/'3'). Stored at cols 77-79 in most MM versions,
172
+ # cols 92-93 in MM4/MM5-7.0Fa (read with col-77 precedence).
173
+ meet_division: Optional[str] = None
174
+
131
175
  def __init__(
132
176
  self,
133
177
  swimmers: dict[int, Swimmer],
@@ -139,6 +183,7 @@ class EventEntry:
139
183
  converted_seed_time_course: Course,
140
184
  relay_team_id: Optional[str] = None,
141
185
  relay_swim_team_code: Optional[str] = None,
186
+ meet_division: Optional[str] = None,
142
187
  ) -> None:
143
188
  self.swimmers = swimmers
144
189
  self.relay = relay
@@ -149,6 +194,7 @@ class EventEntry:
149
194
  self.converted_seed_time_course = converted_seed_time_course
150
195
  self.relay_team_id = relay_team_id
151
196
  self.relay_swim_team_code = relay_swim_team_code
197
+ self.meet_division = meet_division
152
198
 
153
199
  for course in ("prelim", "swimoff", "finals"):
154
200
  setattr(self, f"{course}_time", None)
@@ -161,6 +207,12 @@ class EventEntry:
161
207
  setattr(self, f"{course}_heat_place", None)
162
208
  setattr(self, f"{course}_overall_place", None)
163
209
  setattr(self, f"{course}_date", None)
210
+ setattr(self, f"{course}_pad_time", None)
211
+ setattr(self, f"{course}_button_1_time", None)
212
+ setattr(self, f"{course}_button_2_time", None)
213
+ setattr(self, f"{course}_button_3_time", None)
214
+ setattr(self, f"{course}_backup_4_time", None)
215
+ setattr(self, f"{course}_alt_time_code", None)
164
216
 
165
217
  self.prelim_dq_info = None
166
218
  self.swimoff_dq_info = None
@@ -230,6 +282,7 @@ class Event:
230
282
  converted_seed_time_course: Course,
231
283
  relay_team_id: Optional[str] = None,
232
284
  relay_swim_team_code: Optional[str] = None,
285
+ meet_division: Optional[str] = None,
233
286
  ) -> EventEntry:
234
287
  """Get an event entry or create one if needed."""
235
288
  entry = EventEntry(
@@ -242,6 +295,7 @@ class Event:
242
295
  converted_seed_time_course=converted_seed_time_course,
243
296
  relay_team_id=relay_team_id,
244
297
  relay_swim_team_code=relay_swim_team_code,
298
+ meet_division=meet_division,
245
299
  )
246
300
  if self.entries and self.entries[-1].same_swimmer_entry_as(entry):
247
301
  # P/F entries always listed together: a swimmer (individuals) or a
@@ -306,7 +360,15 @@ class Meet:
306
360
  self.swimmers[swimmer.meet_id] = swimmer
307
361
  self.teams[swimmer.team_code].swimmers[swimmer.meet_id] = swimmer
308
362
 
309
- def get_or_create_team(self, name: str, short_name: str, code: str) -> Team:
363
+ def get_or_create_team(
364
+ self,
365
+ name: str,
366
+ short_name: str,
367
+ code: str,
368
+ region: Optional[str] = None,
369
+ contact_name_1: Optional[str] = None,
370
+ contact_name_2: Optional[str] = None,
371
+ ) -> Team:
310
372
  """Get a team or create if needed."""
311
373
  if team := self.teams.get(code):
312
374
  return team
@@ -319,13 +381,15 @@ class Meet:
319
381
  address_2="N/A",
320
382
  city="N/A",
321
383
  country="N/A",
322
- region="N/A",
384
+ region=region,
323
385
  state="N/A",
324
386
  zip_code="N/A",
325
387
  daytime_phone="N/A",
326
388
  evening_phone="N/A",
327
389
  fax="N/A",
328
390
  email="N/A",
391
+ contact_name_1=contact_name_1,
392
+ contact_name_2=contact_name_2,
329
393
  )
330
394
 
331
395
  # Add team, this also updates the teams dict
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hytek-parser"
3
- version = "2.0.0"
3
+ version = "2.2.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>"]
File without changes
File without changes