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.
- {lenexpy-3.0.2 → lenexpy-3.0.3}/PKG-INFO +2 -1
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/__init__.py +10 -4
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/__init__.py +4 -4
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/decoder.py +3 -3
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/encoder.py +2 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lxf_encoder.py +2 -2
- lenexpy-3.0.3/lenexpy/models/_bootstrap.py +104 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/agedate.py +10 -10
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/agegroup.py +3 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/athelete.py +16 -8
- lenexpy-3.0.3/lenexpy/models/base.py +68 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/club.py +6 -5
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/contact.py +1 -1
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/course.py +15 -15
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/currency.py +32 -32
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/entry.py +7 -3
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/event.py +45 -13
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/fee.py +13 -13
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/gender.py +7 -7
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/handicap.py +26 -26
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/heat.py +17 -17
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/judge.py +39 -39
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/lenex.py +3 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/meet.py +28 -19
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/meetinforecord.py +7 -7
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/nation.py +214 -214
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/official.py +1 -1
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/pool.py +9 -9
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/qualify.py +9 -9
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/ranking.py +2 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/record.py +6 -5
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/recordlist.py +2 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/relayposition.py +15 -6
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/relayrecord.py +6 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/result.py +14 -3
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/session.py +29 -7
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/split.py +4 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/stroke.py +20 -20
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/swimstyle.py +11 -11
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/swimtime.py +33 -33
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timestandardlist.py +13 -13
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timing.py +3 -3
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/entry.py +11 -11
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/heat.py +15 -15
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/result.py +1 -2
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/strenum.py +14 -14
- {lenexpy-3.0.2 → lenexpy-3.0.3}/pyproject.toml +23 -22
- lenexpy-3.0.2/lenexpy/models/base.py +0 -24
- lenexpy-3.0.2/lenexpy/models/reactiontime.py +0 -5
- {lenexpy-3.0.2 → lenexpy-3.0.3}/README.md +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lef_decoder.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lef_encoder.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/decoder/lxf_decoder.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/__init__.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/bank.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/common.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/constructor.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/facility.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/meetinfoentry.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/pointtable.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/relaymeet.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timestandard.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models/timestandardref.py +0 -0
- {lenexpy-3.0.2 → lenexpy-3.0.3}/lenexpy/models_st/__init__.py +0 -0
- {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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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(
|
|
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()
|