hytek-parser 2.4.0__tar.gz → 3.0.1__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 (29) hide show
  1. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/PKG-INFO +1 -1
  2. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/_utils.py +9 -3
  3. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/e_event_parsers.py +16 -5
  4. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/f_relay_parsers.py +16 -5
  5. hytek_parser-3.0.1/hytek_parser/hy3/line_parsers/h_dq_parsers.py +75 -0
  6. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/schemas.py +2 -2
  7. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/pyproject.toml +1 -1
  8. hytek_parser-2.4.0/hytek_parser/hy3/line_parsers/h_dq_parsers.py +0 -81
  9. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/LICENSE.md +0 -0
  10. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/README.md +0 -0
  11. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/__init__.py +0 -0
  12. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/export_xls/__init__.py +0 -0
  13. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/export_xls/_utils.py +0 -0
  14. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/export_xls/schemas.py +0 -0
  15. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/__init__.py +0 -0
  16. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/_utils.py +0 -0
  17. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/enums.py +0 -0
  18. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/__init__.py +0 -0
  19. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/a_file_parsers.py +0 -0
  20. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/b_meet_parsers.py +0 -0
  21. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/c_team_parsers.py +0 -0
  22. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/d_swimmer_parsers.py +0 -0
  23. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3/line_parsers/g_split_parsers.py +0 -0
  24. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hy3_parser.py +0 -0
  25. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hyv/__init__.py +0 -0
  26. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hyv/enums.py +0 -0
  27. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/hyv/schemas.py +0 -0
  28. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/py.typed +0 -0
  29. {hytek_parser-2.4.0 → hytek_parser-3.0.1}/hytek_parser/types.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hytek-parser
3
- Version: 2.4.0
3
+ Version: 3.0.1
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
@@ -143,11 +143,17 @@ def int_or_none(value:str|None) -> int|None:
143
143
  return int(value) if value and value.isdigit() else None
144
144
 
145
145
  def date_or_none(value: str|None) -> date|None:
146
- """Safely return either a date from a numeric string value, or None
146
+ """Safely return either a valid date from a numeric string value, or None
147
147
 
148
148
  Args:
149
149
  value (str|None): The value to cast to date
150
150
 
151
151
  Returns:
152
- date|None: Either the parsed date value or None"""
153
- return datetime.strptime(value, "%m%d%Y").date() if value and value.isnumeric() else None
152
+ date|None: The parsed date, or None for blank, malformed, or invalid values"""
153
+ if not value or not value.isnumeric():
154
+ return None
155
+ try:
156
+ return datetime.strptime(value, "%m%d%Y").date()
157
+ except ValueError:
158
+ # Some Meet Manager exports contain numeric but impossible dates.
159
+ return None
@@ -1,7 +1,12 @@
1
- from datetime import datetime
2
1
  from typing import Any
3
2
 
4
- from hytek_parser._utils import extract, get_age_group, safe_cast, select_from_enum
3
+ from hytek_parser._utils import (
4
+ date_or_none,
5
+ extract,
6
+ get_age_group,
7
+ safe_cast,
8
+ select_from_enum,
9
+ )
5
10
  from hytek_parser.hy3._utils import parse_reaction_time, parse_time, parse_time_or_none
6
11
  from hytek_parser.hy3.enums import (
7
12
  Course,
@@ -12,6 +17,7 @@ from hytek_parser.hy3.enums import (
12
17
  Stroke,
13
18
  WithTimeTimeCode,
14
19
  )
20
+ from hytek_parser.hy3.line_parsers.h_dq_parsers import LAST_DQ_SLOT_KEY
15
21
  from hytek_parser.hy3.schemas import DisqualificationInfo, ParsedHytekFile
16
22
 
17
23
 
@@ -26,7 +32,7 @@ def e1_parser(
26
32
  # Event info
27
33
  event_gender = select_from_enum(Gender, extract(line, 14, 1))
28
34
  event_gender_age = select_from_enum(GenderAge, extract(line, 15, 1))
29
- distance = safe_cast(int, extract(line, 16, 6))
35
+ distance = safe_cast(float, extract(line, 16, 6))
30
36
  stroke = select_from_enum(Stroke, extract(line, 22, 1))
31
37
  age_min, age_max = get_age_group(
32
38
  age_min=safe_cast(int, extract(line, 23, 3)),
@@ -120,8 +126,7 @@ def e2_parser(
120
126
  # col 83-87. Signed; sentinels are blank / 0.00 in any sign spelling.
121
127
  reaction_time = parse_reaction_time(extract(line, 83, 5))
122
128
 
123
- raw_date = extract(line, 88, 8).strip()
124
- date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
129
+ date_ = date_or_none(extract(line, 88, 8).strip())
125
130
 
126
131
  # Get entry
127
132
  event_num, event = file.meet.last_event
@@ -155,6 +160,12 @@ def e2_parser(
155
160
  setattr(entry, f"{prefix}_reaction_time", reaction_time)
156
161
  setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
157
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
+
158
169
  event.last_entry = entry
159
170
  file.meet.last_event = (event_num, event)
160
171
 
@@ -1,7 +1,12 @@
1
- from datetime import datetime
2
1
  from typing import Any
3
2
 
4
- from hytek_parser._utils import extract, get_age_group, safe_cast, select_from_enum
3
+ from hytek_parser._utils import (
4
+ date_or_none,
5
+ extract,
6
+ get_age_group,
7
+ safe_cast,
8
+ select_from_enum,
9
+ )
5
10
  from hytek_parser.hy3._utils import parse_reaction_time, parse_time, parse_time_or_none
6
11
  from hytek_parser.hy3.enums import (
7
12
  Course,
@@ -11,6 +16,7 @@ from hytek_parser.hy3.enums import (
11
16
  Stroke,
12
17
  WithTimeTimeCode,
13
18
  )
19
+ from hytek_parser.hy3.line_parsers.h_dq_parsers import LAST_DQ_SLOT_KEY
14
20
  from hytek_parser.hy3.schemas import (
15
21
  DisqualificationInfo,
16
22
  Gender,
@@ -30,7 +36,7 @@ def f1_parser(
30
36
  # Get event info
31
37
  event_gender = select_from_enum(Gender, extract(line, 14, 1))
32
38
  event_gender_age = select_from_enum(GenderAge, extract(line, 15, 1))
33
- distance = safe_cast(int, extract(line, 16, 6))
39
+ distance = safe_cast(float, extract(line, 16, 6))
34
40
  stroke = select_from_enum(Stroke, extract(line, 22, 1))
35
41
 
36
42
  # Have to set these now since swimmer ages are not available yet
@@ -124,8 +130,7 @@ def f2_parser(
124
130
  parse_reaction_time(extract(line, 83 + 5 * i, 5)) for i in range(4)
125
131
  ]
126
132
 
127
- raw_date = extract(line, 103, 8).strip()
128
- date_ = datetime.strptime(raw_date, "%m%d%Y").date() if raw_date else None
133
+ date_ = date_or_none(extract(line, 103, 8).strip())
129
134
 
130
135
  # Get entry
131
136
  event_num, event = file.meet.last_event
@@ -159,6 +164,12 @@ def f2_parser(
159
164
  setattr(entry, f"{prefix}_reaction_times", reaction_times)
160
165
  setattr(entry, f"{prefix}_alt_time_code", alt_time_code)
161
166
 
167
+ # Anchor the H1 reason / H2 detail lines that follow to this DQ slot (see
168
+ # h_dq_parsers). File position is the only signal that survives a same-code
169
+ # prelim+finals double-DQ; clear it on a non-DQ result so a later orphaned
170
+ # H1/H2 does not attach to a stale slot.
171
+ opts[LAST_DQ_SLOT_KEY] = f"{prefix}_dq_info" if dq_info is not None else None
172
+
162
173
  event.last_entry = entry
163
174
  file.meet.last_event = (event_num, event)
164
175
 
@@ -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
@@ -264,7 +264,7 @@ class Event:
264
264
 
265
265
  # ID info
266
266
  number: str
267
- distance: int
267
+ distance: float
268
268
  stroke: Stroke
269
269
  course: Course
270
270
  date_: Optional[date]
@@ -432,7 +432,7 @@ class Meet:
432
432
  def get_or_create_event(
433
433
  self,
434
434
  number: str,
435
- distance: int,
435
+ distance: float,
436
436
  stroke: Stroke,
437
437
  course: Course,
438
438
  gender: Gender,
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "hytek-parser"
3
- version = "2.4.0"
3
+ version = "3.0.1"
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,81 +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
- # 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
File without changes
File without changes