lenexpy 3.0.2__tar.gz → 3.0.3__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 (65) hide show
  1. {lenexpy-3.0.2 → lenexpy-3.0.3}/PKG-INFO +2 -1
  2. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/__init__.py +10 -4
  3. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/__init__.py +4 -4
  4. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/decoder.py +3 -3
  5. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/encoder.py +2 -2
  6. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lxf_encoder.py +2 -2
  7. lenexpy-3.0.3/lenexpy/models/_bootstrap.py +104 -0
  8. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/agedate.py +10 -10
  9. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/agegroup.py +3 -2
  10. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/athelete.py +16 -8
  11. lenexpy-3.0.3/lenexpy/models/base.py +68 -0
  12. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/club.py +6 -5
  13. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/contact.py +1 -1
  14. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/course.py +15 -15
  15. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/currency.py +32 -32
  16. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/entry.py +7 -3
  17. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/event.py +45 -13
  18. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/fee.py +13 -13
  19. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/gender.py +7 -7
  20. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/handicap.py +26 -26
  21. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/heat.py +17 -17
  22. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/judge.py +39 -39
  23. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/lenex.py +3 -0
  24. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/meet.py +28 -19
  25. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/meetinforecord.py +7 -7
  26. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/nation.py +214 -214
  27. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/official.py +1 -1
  28. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/pool.py +9 -9
  29. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/qualify.py +9 -9
  30. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/ranking.py +2 -2
  31. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/record.py +6 -5
  32. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/recordlist.py +2 -2
  33. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/relayposition.py +15 -6
  34. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/relayrecord.py +6 -2
  35. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/result.py +14 -3
  36. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/session.py +29 -7
  37. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/split.py +4 -2
  38. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/stroke.py +20 -20
  39. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/swimstyle.py +11 -11
  40. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/swimtime.py +33 -33
  41. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timestandardlist.py +13 -13
  42. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timing.py +3 -3
  43. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/entry.py +11 -11
  44. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/heat.py +15 -15
  45. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/result.py +1 -2
  46. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/strenum.py +14 -14
  47. {lenexpy-3.0.2 → lenexpy-3.0.3}/pyproject.toml +23 -22
  48. lenexpy-3.0.2/lenexpy/models/base.py +0 -24
  49. lenexpy-3.0.2/lenexpy/models/reactiontime.py +0 -5
  50. {lenexpy-3.0.2 → lenexpy-3.0.3}/README.md +0 -0
  51. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lef_decoder.py +0 -0
  52. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lef_encoder.py +0 -0
  53. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lxf_decoder.py +0 -0
  54. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/__init__.py +0 -0
  55. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/bank.py +0 -0
  56. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/common.py +0 -0
  57. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/constructor.py +0 -0
  58. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/facility.py +0 -0
  59. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/meetinfoentry.py +0 -0
  60. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/pointtable.py +0 -0
  61. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/relaymeet.py +0 -0
  62. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timestandard.py +0 -0
  63. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timestandardref.py +0 -0
  64. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/__init__.py +0 -0
  65. {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/athelete.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lenexpy
3
- Version: 3.0.2
3
+ Version: 3.0.3
4
4
  Summary:
5
5
  Author: LordCode
6
6
  Author-email: 9999269010dddd@gmail.com
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.11
10
10
  Classifier: Programming Language :: Python :: 3.12
11
11
  Classifier: Programming Language :: Python :: 3.13
12
12
  Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Dist: lxml (>=6.0.2,<7.0.0)
13
14
  Requires-Dist: pydantic-xml (>=2.18.0,<3.0.0)
14
15
  Description-Content-Type: text/markdown
15
16
 
@@ -1,3 +1,7 @@
1
+ from lenexpy.models._bootstrap import bootstrap_models
2
+
3
+
4
+ from typing import Tuple
1
5
  from .decoder import load as fromfile, save as tofile
2
6
  from .models.agedate import TypeAgeDate, AgeDate
3
7
  from .models.agegroup import Calculate, AgeGroup
@@ -25,7 +29,6 @@ from .models.pointtable import PointTable
25
29
  from .models.pool import TypePool, Pool
26
30
  from .models.qualify import Conversion, Qualify
27
31
  from .models.ranking import Ranking
28
- from .models.reactiontime import ReactionTime
29
32
  from .models.record import Record
30
33
  from .models.recordlist import RecordList
31
34
  from .models.relaymeet import RelayMeet
@@ -52,7 +55,11 @@ from .models_st.entry import Status as StatusST, Entry as EntryST
52
55
  from .models_st.heat import Final as FinalST, StatusHeat as StatusHeatST, Heat as HeatST
53
56
  from .models_st.result import StatusResult as StatusResultST, Result as ResultST
54
57
 
55
- __all__ = [
58
+ Club.model_rebuild(force=True, _types_namespace={"Athlete": Athlete})
59
+ Athlete.model_rebuild(force=True, _types_namespace={"Club": Club})
60
+ RelayRecord.model_rebuild(force=True, _types_namespace={"Club": Club})
61
+
62
+ __all__: Tuple[str, ...] = (
56
63
  "fromfile",
57
64
  "tofile",
58
65
  "TypeAgeDate",
@@ -96,7 +103,6 @@ __all__ = [
96
103
  "Conversion",
97
104
  "Qualify",
98
105
  "Ranking",
99
- "ReactionTime",
100
106
  "Record",
101
107
  "RecordList",
102
108
  "RelayMeet",
@@ -126,4 +132,4 @@ __all__ = [
126
132
  "HeatST",
127
133
  "StatusResultST",
128
134
  "ResultST",
129
- ]
135
+ )
@@ -1,4 +1,4 @@
1
- from .decoder import decode as load
2
- from .encoder import encode as save
3
- from .lef_decoder import decode_lef_bytes
4
- from .lxf_decoder import decode_lxf_bytes
1
+ from .decoder import decode as load
2
+ from .encoder import encode as save
3
+ from .lef_decoder import decode_lef_bytes
4
+ from .lxf_decoder import decode_lxf_bytes
@@ -1,11 +1,11 @@
1
-
1
+
2
2
  from pathlib import Path
3
3
 
4
4
  from lenexpy.decoder.lef_decoder import decode_lef
5
5
  from lenexpy.decoder.lxf_decoder import decode_lxf
6
6
  from lenexpy.models.lenex import Lenex
7
-
8
-
7
+
8
+
9
9
  def decode(filename: str) -> Lenex:
10
10
  suffix = Path(filename).suffix.lower()
11
11
  if suffix in (".xml", ".lef"):
@@ -3,8 +3,8 @@ from pathlib import Path
3
3
  from lenexpy.models.lenex import Lenex
4
4
  from .lef_encoder import encode_lef
5
5
  from .lxf_encoder import encode_lxf
6
-
7
-
6
+
7
+
8
8
  def encode(lenex: Lenex, filename: str):
9
9
  suffix = Path(filename).suffix.lower()
10
10
  if suffix in (".xml", ".lef"):
@@ -4,8 +4,8 @@ from zipfile import ZipFile, ZIP_DEFLATED
4
4
 
5
5
  from lenexpy.models.lenex import Lenex
6
6
  from .lef_encoder import encode_lef_bytes
7
-
8
-
7
+
8
+
9
9
  def encode_lxf(lenex: Lenex, filename: str):
10
10
  if not filename.endswith('.lxf'):
11
11
  raise TypeError('The file type must be .lxf')
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+ _BOOTSTRAPPED = False
3
+
4
+
5
+ def bootstrap_models() -> None:
6
+ """
7
+ Ensures pydantic-xml serializers are built after all model modules are loaded.
8
+ Must be called before Lenex.from_xml / to_xml.
9
+ """
10
+ global _BOOTSTRAPPED
11
+ if _BOOTSTRAPPED:
12
+ return
13
+
14
+ # Импортируем корневую модель и "узлы" графа.
15
+ # Важно: импорт внутри функции, чтобы не создавать круги при импорте пакета.
16
+ from .lenex import Lenex
17
+
18
+ # Эти импорты нужны, чтобы классы реально были в памяти и попали в namespace
19
+ from .club import Club
20
+ from .athelete import Athlete
21
+ from .relayrecord import RelayRecord
22
+
23
+ # Дополнительно (часто нужно) — базовые контейнеры цепочки:
24
+ from .meet import Meet
25
+ from .session import Session
26
+ from .event import Event
27
+ from .heat import Heat
28
+ from .result import Result
29
+ from .entry import Entry
30
+ from .recordlist import RecordList
31
+ from .record import Record
32
+ from .relaymeet import RelayMeet
33
+ from .relayposition import RelayPosition
34
+ from .split import Split
35
+ from .contact import Contact
36
+ from .official import Official
37
+ from .nation import Nation
38
+ from .pool import Pool
39
+ from .swimstyle import SwimStyle
40
+ from .stroke import Stroke
41
+ from .swimtime import SwimTime
42
+ from .timestandardlist import TimeStandardList
43
+ from .timestandard import TimeStandard
44
+ from .timestandardref import TimeStandardRef
45
+ from .timing import Timing
46
+ from .agedate import AgeDate
47
+ from .agegroup import AgeGroup
48
+ from .fee import Fee
49
+ from .qualify import Qualify
50
+ from .ranking import Ranking
51
+ from .pointtable import PointTable
52
+ from .handicap import Handicap
53
+ from .meetinfoentry import MeetInfoEntry
54
+ from .meetinforecord import MeetInfoRecord
55
+
56
+ ns = {
57
+ "Lenex": Lenex,
58
+ "Club": Club,
59
+ "Athlete": Athlete,
60
+ "RelayRecord": RelayRecord,
61
+ "Meet": Meet,
62
+ "Session": Session,
63
+ "Event": Event,
64
+ "Heat": Heat,
65
+ "Result": Result,
66
+ "Entry": Entry,
67
+ "RecordList": RecordList,
68
+ "Record": Record,
69
+ "RelayMeet": RelayMeet,
70
+ "RelayPosition": RelayPosition,
71
+ "Split": Split,
72
+ "Contact": Contact,
73
+ "Official": Official,
74
+ "Nation": Nation,
75
+ "Pool": Pool,
76
+ "SwimStyle": SwimStyle,
77
+ "Stroke": Stroke,
78
+ "SwimTime": SwimTime,
79
+ "TimeStandardList": TimeStandardList,
80
+ "TimeStandard": TimeStandard,
81
+ "TimeStandardRef": TimeStandardRef,
82
+ "Timing": Timing,
83
+ "AgeDate": AgeDate,
84
+ "AgeGroup": AgeGroup,
85
+ "Fee": Fee,
86
+ "Qualify": Qualify,
87
+ "Ranking": Ranking,
88
+ "PointTable": PointTable,
89
+ "Handicap": Handicap,
90
+ "MeetInfoEntry": MeetInfoEntry,
91
+ "MeetInfoRecord": MeetInfoRecord,
92
+ }
93
+
94
+ # Главная часть: rebuild корневой модели (она должна получить __xml_serializer__)
95
+ Lenex.model_rebuild(force=True, _types_namespace=ns)
96
+
97
+ # На всякий случай "дожмём" ключевые узлы графа (это дешево, но стабилизирует)
98
+ for m in (Club, Athlete, RelayRecord, RelayMeet, RelayPosition, Meet, Session, Event, Heat, Result, Entry, RecordList, Record):
99
+ m.model_rebuild(force=True, _types_namespace=ns)
100
+
101
+ _BOOTSTRAPPED = True
102
+
103
+
104
+ bootstrap_models()
@@ -4,16 +4,16 @@ from lenexpy.strenum import StrEnum
4
4
  from pydantic_xml import attr
5
5
 
6
6
  from .base import LenexBaseXmlModel
7
-
8
-
9
- class TypeAgeDate(StrEnum):
10
- YEAR = "YEAR"
11
- DATE = "DATE"
12
- POR = "POR"
13
- CAN_FNQ = "CAN.FNQ"
14
- LUX = "LUX"
15
-
16
-
7
+
8
+
9
+ class TypeAgeDate(StrEnum):
10
+ YEAR = "YEAR"
11
+ DATE = "DATE"
12
+ POR = "POR"
13
+ CAN_FNQ = "CAN.FNQ"
14
+ LUX = "LUX"
15
+
16
+
17
17
  # TODO: confirm root tag for AgeDate.
18
18
  class AgeDate(LenexBaseXmlModel, tag="AGEDATE"):
19
19
  type: TypeAgeDate = attr(name="type")
@@ -1,5 +1,5 @@
1
1
  from lenexpy.strenum import StrEnum
2
- from typing import List, Optional
2
+ from typing import List, Optional, Union
3
3
  from pydantic_xml import attr, wrapped, element
4
4
 
5
5
  from .base import LenexBaseXmlModel
@@ -20,7 +20,8 @@ class AgeGroup(LenexBaseXmlModel, tag="AGEGROUP"):
20
20
  agemin: int = attr(name="agemin")
21
21
  gender: Optional[Gender] = attr(name="gender", default=None)
22
22
  calculate: Optional[Calculate] = attr(name="calculate", default=None)
23
- handicap: Optional[int] = attr(name="handicap", default=None)
23
+ # list handicap, example "1,2,3", parsing for ,
24
+ handicap: Optional[Union[int, str]] = attr(name="handicap", default=None)
24
25
  levelmax: Optional[int] = attr(name="levelmax", default=None)
25
26
  levelmin: Optional[int] = attr(name="levelmin", default=None)
26
27
  levels: Optional[str] = attr(name="levels", default=None)
@@ -1,5 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from datetime import date
2
- from typing import List, Optional
4
+ from typing import TYPE_CHECKING, List, Optional
3
5
 
4
6
  from pydantic import model_validator
5
7
  from pydantic_xml import attr, element, wrapped
@@ -13,6 +15,9 @@ from .handicap import Handicap
13
15
  from .nation import Nation
14
16
  from .result import Result
15
17
 
18
+ if TYPE_CHECKING:
19
+ from .club import Club
20
+
16
21
 
17
22
  class AthleteStatus(StrEnum):
18
23
  EXHIBITION = "EXHIBITION"
@@ -26,18 +31,20 @@ class AthleteStatus(StrEnum):
26
31
 
27
32
  # TODO: confirm root tag for Athlete.
28
33
  class Athlete(LenexBaseXmlModel, tag="ATHLETE"):
29
- athleteid: int = attr(name="athleteid")
30
- birthdate: date = attr(name="birthdate")
34
+ athleteid: Optional[int] = attr(name="athleteid", default=None)
35
+ sdmsid: Optional[int] = attr(name="sdmsid", default=None)
36
+ birthdate: Optional[date] = attr(name="birthdate", default=None)
37
+ firstname: Optional[str] = attr(name="firstname", default=None)
38
+ firstname_en: Optional[str] = attr(name="firstname.en", default=None)
39
+ gender: Gender = attr(name="gender", default="")
40
+ club: Optional[Club] = element(name="CLUB", default=None)
41
+ handicap: Optional[Handicap] = element(tag="HANDICAP", default=None)
31
42
  entries: List[Entry] = wrapped(
32
43
  "ENTRIES",
33
44
  element(tag="ENTRY"),
34
45
  default_factory=list,
35
46
  )
36
- firstname: str = attr(name="firstname")
37
- firstname_en: Optional[str] = attr(name="firstname.en", default=None)
38
- gender: Gender = attr(name="gender")
39
- handicap: Optional[Handicap] = element(tag="HANDICAP", default=None)
40
- lastname: str = attr(name="lastname")
47
+ lastname: Optional[str] = attr(name="lastname", default=None)
41
48
  lastname_en: Optional[str] = attr(name="lastname.en", default=None)
42
49
  level: Optional[str] = attr(name="level", default=None)
43
50
  license: Optional[str] = attr(name="license", default=None)
@@ -54,6 +61,7 @@ class Athlete(LenexBaseXmlModel, tag="ATHLETE"):
54
61
  )
55
62
  status: Optional[AthleteStatus] = attr(name="status", default=None)
56
63
  swrid: Optional[int] = attr(name="swrid", default=None)
64
+ domicile: Optional[str] = attr(name="domicile", default=None)
57
65
 
58
66
  @model_validator(mode="before")
59
67
  @classmethod
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import time as dtime
4
+ from typing import Any
5
+
6
+ from pydantic import ConfigDict, model_validator, model_serializer
7
+ from pydantic_xml import BaseXmlModel
8
+ from pydantic_xml.model import SearchMode
9
+
10
+
11
+ def _lenex_time_str(t: dtime) -> str:
12
+ if t.microsecond == 0:
13
+ if t.second == 0:
14
+ return t.strftime("%H:%M")
15
+ return t.strftime("%H:%M:%S")
16
+ return t.isoformat()
17
+
18
+
19
+ def _convert_times(obj: Any) -> Any:
20
+ if isinstance(obj, dtime):
21
+ return _lenex_time_str(obj)
22
+ if isinstance(obj, dict):
23
+ return {k: _convert_times(v) for k, v in obj.items()}
24
+ if isinstance(obj, (list, tuple)):
25
+ t = [_convert_times(v) for v in obj]
26
+ return t if isinstance(obj, list) else tuple(t)
27
+ return obj
28
+
29
+
30
+ class LenexBaseXmlModel(BaseXmlModel):
31
+ # ! DEV VERSION ! #
32
+ # model_config = ConfigDict(
33
+ # arbitrary_types_allowed=True,
34
+ # extra="allow",
35
+ # protected_namespaces=(),
36
+ # search_mode="unordered",
37
+ # )
38
+
39
+ model_config = ConfigDict(
40
+ arbitrary_types_allowed=False,
41
+ extra="forbid",
42
+ )
43
+ __xml_search_mode__ = SearchMode.UNORDERED
44
+
45
+ @model_validator(mode="before")
46
+ @classmethod
47
+ def _normalize_empty_strings(cls, data: Any):
48
+ def normalize(value: Any):
49
+ if isinstance(value, str) and value == "":
50
+ return None
51
+ if isinstance(value, list):
52
+ return [normalize(item) for item in value]
53
+ if isinstance(value, dict):
54
+ return {key: normalize(val) for key, val in value.items()}
55
+ return value
56
+
57
+ if isinstance(data, (dict, list)):
58
+ return normalize(data)
59
+ return data
60
+
61
+ @model_serializer(mode="wrap")
62
+ def _lenex_serialize(self, handler, info):
63
+ """
64
+ Глобально приводит все datetime.time к нужному формату на выходе.
65
+ Работает для всех наследников LenexBaseXmlModel.
66
+ """
67
+ data = handler(self)
68
+ return _convert_times(data)
@@ -7,14 +7,14 @@ from .base import LenexBaseXmlModel
7
7
  from .nation import Nation
8
8
  from .official import Official
9
9
  from .relaymeet import RelayMeet
10
- from .athelete import Athlete
11
10
  from .contact import Contact
11
+ from .athelete import Athlete
12
12
 
13
13
 
14
14
  class TypeClub(StrEnum):
15
- CLUB = "CLUB",
16
- NATIONALTEAM = "NATIONALTEAM",
17
- REGIONALTEAM = "REGIONALTEAM",
15
+ CLUB = "CLUB"
16
+ NATIONALTEAM = "NATIONALTEAM"
17
+ REGIONALTEAM = "REGIONALTEAM"
18
18
  UNATTACHED = "UNATTACHED"
19
19
 
20
20
 
@@ -22,12 +22,13 @@ class TypeClub(StrEnum):
22
22
  class Club(LenexBaseXmlModel, tag="CLUB"):
23
23
  contact: Optional[Contact] = element(tag="CONTACT", default=None)
24
24
  code: Optional[str] = attr(name="code", default=None)
25
+ clubid: Optional[int] = attr(name="clubid", default=None)
25
26
  athletes: List[Athlete] = wrapped(
26
27
  "ATHLETES",
27
28
  element(tag="ATHLETE"),
28
29
  default_factory=list,
29
30
  )
30
- name: str = attr(name="name")
31
+ name: Optional[str] = attr(name="name", default=None)
31
32
  name_en: Optional[str] = attr(name="name.en", default=None)
32
33
  nation: Optional[Nation] = attr(name="nation", default=None)
33
34
  number: Optional[int] = attr(name="number", default=None)
@@ -9,7 +9,7 @@ from .base import LenexBaseXmlModel
9
9
  class Contact(LenexBaseXmlModel, tag="CONTACT"):
10
10
  city: Optional[str] = attr(name="city", default=None)
11
11
  country: Optional[str] = attr(name="country", default=None)
12
- email: str = attr(name="email")
12
+ email: Optional[str] = attr(name="email", default=None)
13
13
  fax: Optional[str] = attr(name="fax", default=None)
14
14
  internet: Optional[str] = attr(name="internet", default=None)
15
15
  name: Optional[str] = attr(name="name", default=None)
@@ -1,15 +1,15 @@
1
- from lenexpy.strenum import StrEnum
2
-
3
-
4
- class Course(StrEnum):
5
- LCM = "LCM",
6
- SCM = "SCM",
7
- SCY = "SCY",
8
- SCM16 = "SCM16",
9
- SCM20 = "SCM20",
10
- SCM33 = "SCM33",
11
- SCY20 = "SCY20",
12
- SCY27 = "SCY27",
13
- SCY33 = "SCY33",
14
- SCY36 = "SCY36",
15
- OPEN = "OPEN"
1
+ from lenexpy.strenum import StrEnum
2
+
3
+
4
+ class Course(StrEnum):
5
+ LCM = "LCM",
6
+ SCM = "SCM",
7
+ SCY = "SCY",
8
+ SCM16 = "SCM16",
9
+ SCM20 = "SCM20",
10
+ SCM33 = "SCM33",
11
+ SCY20 = "SCY20",
12
+ SCY27 = "SCY27",
13
+ SCY33 = "SCY33",
14
+ SCY36 = "SCY36",
15
+ OPEN = "OPEN"
@@ -1,32 +1,32 @@
1
- from lenexpy.strenum import StrEnum
2
-
3
-
4
- class Currency(StrEnum):
5
- AUD = "Australian dollar"
6
- BRL = "Brazilian real"
7
- CAD = "Canadian dollar"
8
- CHF = "Swiss franc"
9
- DKK = "Danish krone"
10
- DZD = "Algerian dinar"
11
- GBP = "British pound"
12
- DR = "Indonesian rupiah"
13
- EUR = "Euro"
14
- HRK = "Croatian Kuna"
15
- INR = "Indian rupee"
16
- IQD = "Iraqi dinar"
17
- IRR = "Iranian rial"
18
- JPY = "Japanese yen"
19
- KRW = "Korea won"
20
- KWD = "Kuwaiti dinar"
21
- MXP = "Mexican peso"
22
- NGN = "Nigerian naira"
23
- NOK = "Norwegian krone"
24
- NZD = "New Zealand dollar"
25
- PHP = "Philippine peso"
26
- PKR = "Pakistan rupee"
27
- PYG = "Paraguay guarani"
28
- RUR = "Russian rouble"
29
- SAR = "Saudi Arabian riyal"
30
- SEK = "Swedish krona"
31
- TND = "Tunisian dinar"
32
- USD = "US dollar"
1
+ from lenexpy.strenum import StrEnum
2
+
3
+
4
+ class Currency(StrEnum):
5
+ AUD = "Australian dollar"
6
+ BRL = "Brazilian real"
7
+ CAD = "Canadian dollar"
8
+ CHF = "Swiss franc"
9
+ DKK = "Danish krone"
10
+ DZD = "Algerian dinar"
11
+ GBP = "British pound"
12
+ DR = "Indonesian rupiah"
13
+ EUR = "Euro"
14
+ HRK = "Croatian Kuna"
15
+ INR = "Indian rupee"
16
+ IQD = "Iraqi dinar"
17
+ IRR = "Iranian rial"
18
+ JPY = "Japanese yen"
19
+ KRW = "Korea won"
20
+ KWD = "Kuwaiti dinar"
21
+ MXP = "Mexican peso"
22
+ NGN = "Nigerian naira"
23
+ NOK = "Norwegian krone"
24
+ NZD = "New Zealand dollar"
25
+ PHP = "Philippine peso"
26
+ PKR = "Pakistan rupee"
27
+ PYG = "Paraguay guarani"
28
+ RUR = "Russian rouble"
29
+ SAR = "Saudi Arabian riyal"
30
+ SEK = "Swedish krona"
31
+ TND = "Tunisian dinar"
32
+ USD = "US dollar"
@@ -19,8 +19,9 @@ class Status(StrEnum):
19
19
  RJC = "RJC"
20
20
  SICK = "SICK"
21
21
  WDR = "WDR"
22
-
23
-
22
+ DNS = "DNS"
23
+
24
+
24
25
  # TODO: confirm root tag for Entry.
25
26
  class Entry(LenexBaseXmlModel, tag="ENTRY"):
26
27
  agegroupid: Optional[int] = attr(name="agegroupid", default=None)
@@ -37,7 +38,10 @@ class Entry(LenexBaseXmlModel, tag="ENTRY"):
37
38
  element(tag="RELAYPOSITION"),
38
39
  default_factory=list,
39
40
  )
40
- status: Optional[Status] = attr(name="status", default=None)
41
+ # Accept any status string to avoid losing data from fixtures that use
42
+ # additional values like DNS.
43
+ status: Optional[str] = attr(name="status", default=None)
44
+ late: Optional[str] = attr(name="late", default=None)
41
45
 
42
46
  @field_validator("entrytime", mode="before")
43
47
  @classmethod
@@ -2,7 +2,7 @@ from datetime import time as dtime
2
2
  from typing import List, Optional
3
3
 
4
4
  from lenexpy.strenum import StrEnum
5
- from pydantic import model_validator
5
+ from pydantic import field_serializer, field_validator, model_validator
6
6
  from pydantic_xml import attr, element, wrapped
7
7
 
8
8
  from .base import LenexBaseXmlModel
@@ -27,6 +27,10 @@ class Round(StrEnum):
27
27
  SOP = "SOP"
28
28
  SOS = "SOS"
29
29
  SOQ = "SOQ"
30
+ # Additional values observed in fixtures; keep here for reference while
31
+ # allowing arbitrary strings through the model.
32
+ EXTRAHEATS = "EXTRAHEATS"
33
+ TIMETRIAL = "TIMETRIAL"
30
34
 
31
35
 
32
36
  class TypeEvent(StrEnum):
@@ -36,38 +40,66 @@ class TypeEvent(StrEnum):
36
40
 
37
41
  # TODO: confirm root tag for Event.
38
42
  class Event(LenexBaseXmlModel, tag="EVENT"):
43
+ # Preserve child order observed in fixtures: SWIMSTYLE, AGEGROUPS,
44
+ # TIMESTANDARDREFS, HEATS.
45
+ swimstyle: SwimStyle = element(tag="SWIMSTYLE")
39
46
  agegroups: List[AgeGroup] = wrapped(
40
47
  "AGEGROUPS",
41
48
  element(tag="AGEGROUP"),
42
49
  default_factory=list,
43
50
  )
44
- daytime: Optional[dtime] = attr(name="daytime", default=None)
45
- eventid: int = attr(name="eventid")
46
- fee: Optional[Fee] = element(tag="FEE", default=None)
47
- gender: Optional[Gender] = attr(name="gender", default=None)
51
+ time_standard_refs: List[TimeStandardRef] = wrapped(
52
+ "TIMESTANDARDREFS",
53
+ element(tag="TIMESTANDARDREF"),
54
+ default_factory=list,
55
+ )
48
56
  heats: List[Heat] = wrapped(
49
57
  "HEATS",
50
58
  element(tag="HEAT"),
51
59
  default_factory=list,
52
60
  )
61
+ fee: Optional[Fee] = element(tag="FEE", default=None)
62
+
63
+ daytime: Optional[dtime] = attr(name="daytime", default=None)
64
+ eventid: int = attr(name="eventid")
65
+ gender: Optional[Gender] = attr(name="gender", default=None)
53
66
  maxentries: Optional[int] = attr(name="maxentries", default=None)
54
67
  number: int = attr(name="number")
55
68
  order: Optional[int] = attr(name="order", default=None)
56
69
  preveventid: Optional[int] = attr(name="preveventid", default=None)
57
- round: Optional[Round] = attr(name="round", default=None)
70
+ # Use str to avoid dropping non-standard round values such as EXTRAHEATS
71
+ # or TIMETRIAL found in fixtures.
72
+ round: Optional[str] = attr(name="round", default=None)
58
73
  run: Optional[int] = attr(name="run", default=None)
59
74
  status: Optional[StatusEvent] = attr(name="status", default=None)
60
- swimstyle: SwimStyle = element(tag="SWIMSTYLE")
61
- time_standard_refs: List[TimeStandardRef] = wrapped(
62
- "TIMESTANDARDREFS",
63
- element(tag="TIMESTANDARDREF"),
64
- default_factory=list,
65
- )
66
75
  timing: Optional[Timing] = attr(name="timing", default=None)
67
76
  type: Optional[TypeEvent] = attr(name="type", default=None)
68
77
 
69
78
  @model_validator(mode="after")
70
79
  def _validate_agegroups(self):
71
80
  if any(agegroup.id is None for agegroup in self.agegroups):
72
- raise ValueError("AGEGROUP elements inside EVENT must define agegroupid")
81
+ raise ValueError(
82
+ "AGEGROUP elements inside EVENT must define agegroupid")
73
83
  return self
84
+
85
+ @field_validator("daytime", mode="before")
86
+ @classmethod
87
+ def _parse_daytime(cls, v):
88
+ if v is None or v == "":
89
+ return None
90
+ if isinstance(v, dtime):
91
+ return v
92
+ # fromisoformat понимает "HH:MM", "HH:MM:SS", "HH:MM:SS.ffffff"
93
+ return dtime.fromisoformat(v)
94
+
95
+ @field_serializer("daytime")
96
+ def _serialize_daytime(self, v: Optional[dtime], _info):
97
+ if v is None:
98
+ return None
99
+ if v.second == 0 and v.microsecond == 0:
100
+ return v.strftime("%H:%M")
101
+ if v.microsecond == 0:
102
+ return v.strftime("%H:%M:%S")
103
+
104
+ # иначе ISO с микросекундами (стандартное поведение)
105
+ return v.isoformat()