bepfe-export-parser 0.6.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.
- bepfe_export_parser-0.6.3/LICENSE +21 -0
- bepfe_export_parser-0.6.3/PKG-INFO +20 -0
- bepfe_export_parser-0.6.3/README.md +4 -0
- bepfe_export_parser-0.6.3/pyproject.toml +78 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/__init__.py +0 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/exceptions.py +6 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/util.py +67 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/__init__.py +0 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/convert.py +31 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/file.py +55 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/model.py +121 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/__init__.py +0 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/abstract.py +18 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/base.py +103 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/constants.py +17 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/heat_info.py +86 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/overview.py +127 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/result.py +99 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/parse/settings.py +59 -0
- bepfe_export_parser-0.6.3/src/bepfe_export_parser/win/reader.py +23 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Lance Watermelon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bepfe-export-parser
|
|
3
|
+
Version: 0.6.3
|
|
4
|
+
Summary: Parses CSV files exported from Bepfe race manager software.
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Author: Lars Wagner
|
|
8
|
+
Author-email: wagner1975@web.de
|
|
9
|
+
Requires-Python: >=3.12,<4.0
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# bepfe-export-parser
|
|
18
|
+
Python library for parsing CSV files exported from Bepfe racing software.
|
|
19
|
+
|
|
20
|
+
To be continued......
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "bepfe-export-parser"
|
|
3
|
+
version = "0.6.3"
|
|
4
|
+
description = "Parses CSV files exported from Bepfe race manager software."
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Lars Wagner",email = "wagner1975@web.de"}
|
|
7
|
+
]
|
|
8
|
+
license = {text = "MIT"}
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12,<4.0"
|
|
11
|
+
dependencies = [
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
16
|
+
build-backend = "poetry.core.masonry.api"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"mypy (>=1.19.1,<2.0.0)",
|
|
21
|
+
"pytest (>=9.0.2,<10.0.0)",
|
|
22
|
+
"ruff (>=0.14.13,<0.15.0)",
|
|
23
|
+
"pytest-mock (>=3.15.1,<4.0.0)",
|
|
24
|
+
"pytest-cov (>=7.1.0,<8.0.0)",
|
|
25
|
+
"coverage (>=7.14.1,<8.0.0)",
|
|
26
|
+
"python-semantic-release (>=10.5.3,<11.0.0)",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.semantic_release]
|
|
30
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
31
|
+
|
|
32
|
+
# to have a first version when no github version tags are available yet
|
|
33
|
+
initial_version = "0.1.0"
|
|
34
|
+
|
|
35
|
+
# commit message conventions
|
|
36
|
+
commit_parser = "conventional"
|
|
37
|
+
|
|
38
|
+
# how to format the git tag
|
|
39
|
+
tag_format = "v{version}"
|
|
40
|
+
|
|
41
|
+
# uses this command for building the package
|
|
42
|
+
build_command = "poetry build"
|
|
43
|
+
|
|
44
|
+
# allow versions starting like 0.x.y
|
|
45
|
+
allow_zero_version = true
|
|
46
|
+
|
|
47
|
+
upload_to_vcs_release = true
|
|
48
|
+
|
|
49
|
+
[tool.semantic_release.remote]
|
|
50
|
+
name = "origin"
|
|
51
|
+
|
|
52
|
+
[tool.semantic_release.branches.main]
|
|
53
|
+
match = "main"
|
|
54
|
+
prerelease = false
|
|
55
|
+
|
|
56
|
+
[tool.semantic_release.branches.develop]
|
|
57
|
+
match = "develop"
|
|
58
|
+
prerelease = true
|
|
59
|
+
prerelease_token = "rc"
|
|
60
|
+
|
|
61
|
+
[tool.mypy]
|
|
62
|
+
strict = true
|
|
63
|
+
files = "./src"
|
|
64
|
+
exclude = ".*__init__\\.py$"
|
|
65
|
+
|
|
66
|
+
[tool.pytest]
|
|
67
|
+
pythonpath = ["./src"]
|
|
68
|
+
testpaths = ["tests"]
|
|
69
|
+
|
|
70
|
+
[tool.coverage.run]
|
|
71
|
+
source = ["bepfe_export_parser"]
|
|
72
|
+
omit = [
|
|
73
|
+
"tests/*",
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
[tool.coverage.report]
|
|
77
|
+
show_missing = true
|
|
78
|
+
skip_covered = false
|
|
File without changes
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def convert_comma_string_to_decimal(s: str) -> Decimal:
|
|
5
|
+
return Decimal(s.replace(",", "."))
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def convert_time_string_to_decimal(s: str) -> Decimal:
|
|
9
|
+
result = Decimal("0.0")
|
|
10
|
+
|
|
11
|
+
point_parts = s.split(".")
|
|
12
|
+
if len(point_parts) > 2:
|
|
13
|
+
raise IndexError("Too many point separated parts")
|
|
14
|
+
if len(point_parts) > 1:
|
|
15
|
+
result = result + Decimal(f"0.{point_parts[1]}")
|
|
16
|
+
|
|
17
|
+
colon_parts = point_parts[0].split(":")
|
|
18
|
+
if len(colon_parts) > 3:
|
|
19
|
+
raise IndexError("Too many colon separated parts")
|
|
20
|
+
colon_parts.reverse()
|
|
21
|
+
if len(colon_parts) > 0:
|
|
22
|
+
result += int(colon_parts[0])
|
|
23
|
+
if len(colon_parts) > 1:
|
|
24
|
+
result += int(colon_parts[1]) * 60
|
|
25
|
+
if len(colon_parts) > 2:
|
|
26
|
+
result += int(colon_parts[2]) * 3600
|
|
27
|
+
|
|
28
|
+
return result
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def normalize_date_string(date_str: str, separator: str = ".") -> str:
|
|
32
|
+
if len(separator) < 1:
|
|
33
|
+
raise ValueError("separator is invalid")
|
|
34
|
+
|
|
35
|
+
day, month, year = date_str.split(separator)
|
|
36
|
+
|
|
37
|
+
len_day = len(day)
|
|
38
|
+
if len_day < 1 or len_day > 2:
|
|
39
|
+
raise ValueError("day string has invalid length")
|
|
40
|
+
if not day.isnumeric():
|
|
41
|
+
raise ValueError("day string is not numeric")
|
|
42
|
+
d = int(day)
|
|
43
|
+
if d < 1 or d > 31:
|
|
44
|
+
raise ValueError("day value is out of range")
|
|
45
|
+
|
|
46
|
+
len_month = len(month)
|
|
47
|
+
if len_month < 1 or len_month > 2:
|
|
48
|
+
raise ValueError("month string has invalid length")
|
|
49
|
+
if not month.isnumeric():
|
|
50
|
+
raise ValueError("month string is not numeric")
|
|
51
|
+
m = int(month)
|
|
52
|
+
if m < 1 or m > 12:
|
|
53
|
+
raise ValueError("month value is out of range")
|
|
54
|
+
|
|
55
|
+
len_year = len(year)
|
|
56
|
+
if len_year != 2 and len_year != 4:
|
|
57
|
+
raise ValueError("year string has invalid length")
|
|
58
|
+
if not year.isnumeric():
|
|
59
|
+
raise ValueError("year string is not numeric")
|
|
60
|
+
y = int(year)
|
|
61
|
+
if len_year == 2:
|
|
62
|
+
offset = 2000 if y < 80 else 1900
|
|
63
|
+
y += offset
|
|
64
|
+
if y < 1000:
|
|
65
|
+
raise ValueError("year value is out of range")
|
|
66
|
+
|
|
67
|
+
return separator.join([f"{int(d):02d}", f"{int(m):02d}", f"{int(y):04d}"])
|
|
File without changes
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from bepfe_export_parser.exceptions import ConvertingError
|
|
4
|
+
from bepfe_export_parser.util import (
|
|
5
|
+
convert_comma_string_to_decimal,
|
|
6
|
+
convert_time_string_to_decimal,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def convert_str_to_int(s: str, err_msg: str | None = None) -> int:
|
|
11
|
+
try:
|
|
12
|
+
return int(s)
|
|
13
|
+
except Exception as exc:
|
|
14
|
+
new_exc = ConvertingError(err_msg) if err_msg else ConvertingError()
|
|
15
|
+
raise new_exc from exc
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def convert_comma_str_to_decimal(s: str, err_msg: str | None = None) -> Decimal:
|
|
19
|
+
try:
|
|
20
|
+
return convert_comma_string_to_decimal(s)
|
|
21
|
+
except Exception as exc:
|
|
22
|
+
new_exc = ConvertingError(err_msg) if err_msg else ConvertingError()
|
|
23
|
+
raise new_exc from exc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def convert_time_str_to_decimal(s: str, err_msg: str | None = None) -> Decimal:
|
|
27
|
+
try:
|
|
28
|
+
return convert_time_string_to_decimal(s)
|
|
29
|
+
except Exception as exc:
|
|
30
|
+
new_exc = ConvertingError(err_msg) if err_msg else ConvertingError()
|
|
31
|
+
raise new_exc from exc
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from datetime import date, datetime
|
|
6
|
+
|
|
7
|
+
from bepfe_export_parser.util import normalize_date_string
|
|
8
|
+
from bepfe_export_parser.win.model import FileData
|
|
9
|
+
from bepfe_export_parser.win.parse.base import ContentParser
|
|
10
|
+
from bepfe_export_parser.win.reader import StatefulRowReader
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FileDataExtractor:
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
filepath: str,
|
|
17
|
+
newline: str = "",
|
|
18
|
+
encoding: str = "cp1252",
|
|
19
|
+
delimiter: str = ";",
|
|
20
|
+
) -> None:
|
|
21
|
+
self._filepath = filepath
|
|
22
|
+
self._newline = newline
|
|
23
|
+
self._encoding = encoding
|
|
24
|
+
self._delimiter = delimiter
|
|
25
|
+
|
|
26
|
+
def _extract_date_from_filename(self) -> date | None:
|
|
27
|
+
result = None
|
|
28
|
+
|
|
29
|
+
filename = os.path.basename(self._filepath)
|
|
30
|
+
|
|
31
|
+
match = re.search(r"\d{2}\.\d{2}\.\d{4}", filename)
|
|
32
|
+
if not match:
|
|
33
|
+
match = re.search(r"\d{2}\.\d{2}\.\d{2}", filename)
|
|
34
|
+
|
|
35
|
+
if match:
|
|
36
|
+
date_str = normalize_date_string(match.group())
|
|
37
|
+
result = datetime.strptime(date_str, "%d.%m.%Y").date()
|
|
38
|
+
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
def extract(self) -> FileData:
|
|
42
|
+
date_from_filename = self._extract_date_from_filename()
|
|
43
|
+
|
|
44
|
+
with open(
|
|
45
|
+
self._filepath, newline=self._newline, encoding=self._encoding
|
|
46
|
+
) as stream:
|
|
47
|
+
content = ContentParser(
|
|
48
|
+
StatefulRowReader(csv.reader(stream, delimiter=self._delimiter))
|
|
49
|
+
).parse()
|
|
50
|
+
|
|
51
|
+
return FileData(
|
|
52
|
+
filepath=self._filepath,
|
|
53
|
+
date_from_filename=date_from_filename,
|
|
54
|
+
content=content,
|
|
55
|
+
)
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from datetime import date
|
|
3
|
+
from decimal import Decimal
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class RaceTypeEnum(Enum):
|
|
8
|
+
MULTI_RACE = "Serienrennen"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RaceModeEnum(Enum):
|
|
12
|
+
F1 = "F1-Modus"
|
|
13
|
+
SLOT = "Slot-Modus"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HeatLengthUnitEnum(Enum):
|
|
17
|
+
SECONDS = "SECONDS"
|
|
18
|
+
LAPS = "LAPS"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class PersonalizedLapsInHeat:
|
|
23
|
+
driver_name: str
|
|
24
|
+
heat_num: int
|
|
25
|
+
lane_num: int
|
|
26
|
+
lap_count: Decimal
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class DetailedLapTimeInHeat:
|
|
31
|
+
heat_num: int
|
|
32
|
+
lane_num: int
|
|
33
|
+
lap_time: Decimal
|
|
34
|
+
lap_num: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class PersonalizedLapTimeInHeat:
|
|
39
|
+
driver_name: str
|
|
40
|
+
heat_num: int
|
|
41
|
+
lane_num: int
|
|
42
|
+
lap_time: Decimal
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class HeatLength:
|
|
47
|
+
value: int
|
|
48
|
+
unit: HeatLengthUnitEnum
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class QualifyingEntry:
|
|
53
|
+
position_num: int
|
|
54
|
+
driver_name: str
|
|
55
|
+
lap_time: Decimal
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass
|
|
59
|
+
class ShortResultEntry:
|
|
60
|
+
position_num: int
|
|
61
|
+
driver_name: str
|
|
62
|
+
total_lap_count: Decimal
|
|
63
|
+
total_time: Decimal
|
|
64
|
+
average_lap_time: Decimal
|
|
65
|
+
fastest_lap: DetailedLapTimeInHeat
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class LapsOnLane:
|
|
70
|
+
round_num: int
|
|
71
|
+
# for multiple driver appearances on the same lane: first round = 1, second round = 2, ...
|
|
72
|
+
lane_num: int
|
|
73
|
+
# lane number: 1, 2, 3, ...
|
|
74
|
+
lap_count: Decimal
|
|
75
|
+
# lap count on lane at round
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass
|
|
79
|
+
class LapsPerLaneOverviewEntry:
|
|
80
|
+
driver_name: str
|
|
81
|
+
# name of the driver
|
|
82
|
+
heats: list[LapsOnLane]
|
|
83
|
+
# values for all heats driven
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class LapTimeOnLane:
|
|
88
|
+
round_num: int
|
|
89
|
+
# for multiple driver appearances on the same lane: first round = 1, second round = 2, ...
|
|
90
|
+
lane_num: int
|
|
91
|
+
# lane number: 1, 2, 3, ...
|
|
92
|
+
lap_time: Decimal
|
|
93
|
+
# lap time on lane at round
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@dataclass
|
|
97
|
+
class FastestLapsOverviewEntry:
|
|
98
|
+
driver_name: str
|
|
99
|
+
# name of the driver
|
|
100
|
+
heats: list[LapTimeOnLane]
|
|
101
|
+
# values for all lanes driven
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
@dataclass
|
|
105
|
+
class Content:
|
|
106
|
+
race_type: RaceTypeEnum
|
|
107
|
+
race_mode: RaceModeEnum
|
|
108
|
+
heat_length: HeatLength
|
|
109
|
+
qualifying: list[QualifyingEntry]
|
|
110
|
+
best_heat: PersonalizedLapsInHeat | None
|
|
111
|
+
fastest_lap: PersonalizedLapTimeInHeat | None
|
|
112
|
+
short_result: list[ShortResultEntry]
|
|
113
|
+
laps_per_lane_overview: list[LapsPerLaneOverviewEntry]
|
|
114
|
+
fastest_laps_overview: list[FastestLapsOverviewEntry]
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@dataclass
|
|
118
|
+
class FileData:
|
|
119
|
+
filepath: str
|
|
120
|
+
date_from_filename: date | None
|
|
121
|
+
content: Content
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from bepfe_export_parser.win.reader import StatefulRowReader
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class AbstractParser(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def parse(self) -> Any:
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class DefaultParser(AbstractParser):
|
|
14
|
+
def __init__(self, reader: StatefulRowReader) -> None:
|
|
15
|
+
self._reader = reader
|
|
16
|
+
|
|
17
|
+
def _get_reader(self) -> StatefulRowReader:
|
|
18
|
+
return self._reader
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from bepfe_export_parser.win.model import Content
|
|
4
|
+
from bepfe_export_parser.win.parse.abstract import DefaultParser
|
|
5
|
+
from bepfe_export_parser.win.parse.constants import (
|
|
6
|
+
BEST_HEAT_MARKER,
|
|
7
|
+
FASTEST_LAP_MARKER,
|
|
8
|
+
FASTEST_LAPS_OVERVIEW_MARKER,
|
|
9
|
+
HEAT_LENGTH_MARKER,
|
|
10
|
+
LAPS_PER_LANE_OVERVIEW_MARKER,
|
|
11
|
+
QUALIFYING_MARKER,
|
|
12
|
+
RACE_MODE_MARKER,
|
|
13
|
+
RACE_TYPE_MARKER,
|
|
14
|
+
SHORT_RESULT_MARKER,
|
|
15
|
+
)
|
|
16
|
+
from bepfe_export_parser.win.parse.heat_info import (
|
|
17
|
+
HeatLapsInfoParser,
|
|
18
|
+
HeatTimeInfoParser,
|
|
19
|
+
)
|
|
20
|
+
from bepfe_export_parser.win.parse.result import QualifyingParser, ShortResultParser
|
|
21
|
+
from bepfe_export_parser.win.parse.settings import (
|
|
22
|
+
HeatLengthParser,
|
|
23
|
+
RaceModeParser,
|
|
24
|
+
RaceTypeParser,
|
|
25
|
+
)
|
|
26
|
+
from bepfe_export_parser.win.parse.overview import (
|
|
27
|
+
FastestLapsOverviewParser,
|
|
28
|
+
LapsPerLaneOverviewParser,
|
|
29
|
+
)
|
|
30
|
+
from bepfe_export_parser.win.reader import StatefulRowReader
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ContentCollector:
|
|
34
|
+
_results: dict[str, Any]
|
|
35
|
+
|
|
36
|
+
def __init__(self) -> None:
|
|
37
|
+
self._results = {}
|
|
38
|
+
|
|
39
|
+
def add_result(self, key: str, result: Any) -> None:
|
|
40
|
+
self._results[key] = result
|
|
41
|
+
|
|
42
|
+
def construct_model(self) -> Content:
|
|
43
|
+
race_type = self._results.get(RACE_TYPE_MARKER)
|
|
44
|
+
if not race_type:
|
|
45
|
+
raise KeyError("race type result is missing")
|
|
46
|
+
|
|
47
|
+
race_mode = self._results.get(RACE_MODE_MARKER)
|
|
48
|
+
if not race_mode:
|
|
49
|
+
raise KeyError("race mode result is missing")
|
|
50
|
+
|
|
51
|
+
heat_length = self._results.get(HEAT_LENGTH_MARKER)
|
|
52
|
+
if not heat_length:
|
|
53
|
+
raise KeyError("heat length result is missing")
|
|
54
|
+
|
|
55
|
+
qualifying = self._results.get(QUALIFYING_MARKER, [])
|
|
56
|
+
|
|
57
|
+
best_heat = self._results.get(BEST_HEAT_MARKER)
|
|
58
|
+
fastest_lap = self._results.get(FASTEST_LAP_MARKER)
|
|
59
|
+
short_result = self._results.get(SHORT_RESULT_MARKER, [])
|
|
60
|
+
|
|
61
|
+
laps_per_lane_overview = self._results.get(LAPS_PER_LANE_OVERVIEW_MARKER, [])
|
|
62
|
+
fastest_laps_overview = self._results.get(FASTEST_LAPS_OVERVIEW_MARKER, [])
|
|
63
|
+
|
|
64
|
+
return Content(
|
|
65
|
+
race_type=race_type,
|
|
66
|
+
race_mode=race_mode,
|
|
67
|
+
heat_length=heat_length,
|
|
68
|
+
qualifying=qualifying,
|
|
69
|
+
best_heat=best_heat,
|
|
70
|
+
fastest_lap=fastest_lap,
|
|
71
|
+
short_result=short_result,
|
|
72
|
+
laps_per_lane_overview=laps_per_lane_overview,
|
|
73
|
+
fastest_laps_overview=fastest_laps_overview,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ContentParser(DefaultParser):
|
|
78
|
+
def __init__(self, reader: StatefulRowReader) -> None:
|
|
79
|
+
super().__init__(reader)
|
|
80
|
+
self._mapped_parsers: dict[str, type[DefaultParser]] = {
|
|
81
|
+
RACE_TYPE_MARKER: RaceTypeParser,
|
|
82
|
+
RACE_MODE_MARKER: RaceModeParser,
|
|
83
|
+
HEAT_LENGTH_MARKER: HeatLengthParser,
|
|
84
|
+
QUALIFYING_MARKER: QualifyingParser,
|
|
85
|
+
BEST_HEAT_MARKER: HeatLapsInfoParser,
|
|
86
|
+
FASTEST_LAP_MARKER: HeatTimeInfoParser,
|
|
87
|
+
SHORT_RESULT_MARKER: ShortResultParser,
|
|
88
|
+
LAPS_PER_LANE_OVERVIEW_MARKER: LapsPerLaneOverviewParser,
|
|
89
|
+
FASTEST_LAPS_OVERVIEW_MARKER: FastestLapsOverviewParser,
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
def parse(self) -> Content:
|
|
93
|
+
collector = ContentCollector()
|
|
94
|
+
reader = self._get_reader()
|
|
95
|
+
while (current_row := reader.fetch_next_row()) is not None:
|
|
96
|
+
if len(current_row) < 1:
|
|
97
|
+
continue
|
|
98
|
+
marker = current_row[0]
|
|
99
|
+
current_parser = self._mapped_parsers.get(marker)
|
|
100
|
+
if current_parser:
|
|
101
|
+
collector.add_result(marker, current_parser(reader).parse())
|
|
102
|
+
|
|
103
|
+
return collector.construct_model()
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
HEAT_LABEL = "Lauf"
|
|
2
|
+
LANE_LABEL = "Spur"
|
|
3
|
+
|
|
4
|
+
ROUNDS_UNIT = "R"
|
|
5
|
+
SECONDS_UNIT = "s"
|
|
6
|
+
|
|
7
|
+
MINUTES_UNIT = "Minuten"
|
|
8
|
+
|
|
9
|
+
RACE_TYPE_MARKER = "Typ"
|
|
10
|
+
RACE_MODE_MARKER = "Modus"
|
|
11
|
+
HEAT_LENGTH_MARKER = "Vorgabe"
|
|
12
|
+
QUALIFYING_MARKER = "Qualifikation"
|
|
13
|
+
FASTEST_LAP_MARKER = "Bestzeit"
|
|
14
|
+
BEST_HEAT_MARKER = "Bester Lauf"
|
|
15
|
+
SHORT_RESULT_MARKER = "Kurzergebnis"
|
|
16
|
+
LAPS_PER_LANE_OVERVIEW_MARKER = "Ergebnis-Spurübersicht Runden"
|
|
17
|
+
FASTEST_LAPS_OVERVIEW_MARKER = "Bestzeiten-Übersicht"
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from bepfe_export_parser.win.convert import (
|
|
4
|
+
convert_str_to_int,
|
|
5
|
+
convert_comma_str_to_decimal,
|
|
6
|
+
)
|
|
7
|
+
from bepfe_export_parser.win.model import (
|
|
8
|
+
PersonalizedLapsInHeat,
|
|
9
|
+
PersonalizedLapTimeInHeat,
|
|
10
|
+
)
|
|
11
|
+
from bepfe_export_parser.win.parse.abstract import DefaultParser
|
|
12
|
+
from bepfe_export_parser.win.parse.constants import (
|
|
13
|
+
HEAT_LABEL,
|
|
14
|
+
LANE_LABEL,
|
|
15
|
+
ROUNDS_UNIT,
|
|
16
|
+
SECONDS_UNIT,
|
|
17
|
+
)
|
|
18
|
+
from bepfe_export_parser.exceptions import ConvertingError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HeatRelatedParser(DefaultParser):
|
|
22
|
+
@staticmethod
|
|
23
|
+
def _convert_int_with_label(text: str, label: str, col_name: str) -> int:
|
|
24
|
+
parts = text.split()
|
|
25
|
+
|
|
26
|
+
if len(parts) != 2:
|
|
27
|
+
raise ConvertingError(f"text in {col_name} column is invalid")
|
|
28
|
+
|
|
29
|
+
label_part, value_part = parts
|
|
30
|
+
|
|
31
|
+
if not label_part.startswith(label):
|
|
32
|
+
raise ConvertingError(f"label part in {col_name} column is invalid")
|
|
33
|
+
|
|
34
|
+
return convert_str_to_int(
|
|
35
|
+
value_part, f"value part in {col_name} column is invalid"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
@staticmethod
|
|
39
|
+
def _convert_decimal_with_unit(text: str, unit: str, col_name: str) -> Decimal:
|
|
40
|
+
parts = text.split()
|
|
41
|
+
|
|
42
|
+
if len(parts) != 2:
|
|
43
|
+
raise ConvertingError(f"text in {col_name} column is invalid")
|
|
44
|
+
|
|
45
|
+
value_part, unit_part = parts
|
|
46
|
+
|
|
47
|
+
result = convert_comma_str_to_decimal(
|
|
48
|
+
value_part, f"value part in {col_name} column is invalid"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if unit_part.strip() != unit:
|
|
52
|
+
raise ConvertingError(f"unit part in {col_name} column is invalid")
|
|
53
|
+
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def _extract_heat_common_info(self) -> tuple[str, str, int, int]:
|
|
57
|
+
current_row = self._get_reader().get_current_row_checked(5)
|
|
58
|
+
|
|
59
|
+
driver_name = current_row[1]
|
|
60
|
+
mixed = current_row[2]
|
|
61
|
+
heat_num = HeatRelatedParser._convert_int_with_label(
|
|
62
|
+
current_row[3], HEAT_LABEL, "heat"
|
|
63
|
+
)
|
|
64
|
+
lane_num = HeatRelatedParser._convert_int_with_label(
|
|
65
|
+
current_row[4], LANE_LABEL, "lane"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return (driver_name, mixed, heat_num, lane_num)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class HeatLapsInfoParser(HeatRelatedParser):
|
|
72
|
+
def parse(self) -> PersonalizedLapsInHeat:
|
|
73
|
+
driver, text, heat, lane = self._extract_heat_common_info()
|
|
74
|
+
value = HeatRelatedParser._convert_decimal_with_unit(text, ROUNDS_UNIT, "laps")
|
|
75
|
+
return PersonalizedLapsInHeat(
|
|
76
|
+
driver_name=driver, heat_num=heat, lane_num=lane, lap_count=value
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class HeatTimeInfoParser(HeatRelatedParser):
|
|
81
|
+
def parse(self) -> PersonalizedLapTimeInHeat:
|
|
82
|
+
driver, text, heat, lane = self._extract_heat_common_info()
|
|
83
|
+
value = HeatRelatedParser._convert_decimal_with_unit(text, SECONDS_UNIT, "time")
|
|
84
|
+
return PersonalizedLapTimeInHeat(
|
|
85
|
+
driver_name=driver, heat_num=heat, lane_num=lane, lap_time=value
|
|
86
|
+
)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
|
|
3
|
+
from bepfe_export_parser.exceptions import InvalidRowError
|
|
4
|
+
from bepfe_export_parser.win.convert import (
|
|
5
|
+
convert_comma_str_to_decimal,
|
|
6
|
+
convert_str_to_int,
|
|
7
|
+
)
|
|
8
|
+
from bepfe_export_parser.win.model import (
|
|
9
|
+
FastestLapsOverviewEntry,
|
|
10
|
+
LapTimeOnLane,
|
|
11
|
+
LapsOnLane,
|
|
12
|
+
LapsPerLaneOverviewEntry,
|
|
13
|
+
)
|
|
14
|
+
from bepfe_export_parser.win.parse.abstract import DefaultParser
|
|
15
|
+
from bepfe_export_parser.win.reader import StatefulRowReader
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LanesRelatedOverviewParser:
|
|
19
|
+
_NUM_COLS_BEFORE_LANES = 2
|
|
20
|
+
_BROKEN_COL_STR = "'-"
|
|
21
|
+
|
|
22
|
+
def __init__(self, reader: StatefulRowReader) -> None:
|
|
23
|
+
self._reader = reader
|
|
24
|
+
|
|
25
|
+
def _fetch_header_lane_numbers(self) -> list[int]:
|
|
26
|
+
header_row = self._reader.fetch_next_row()
|
|
27
|
+
if header_row is None or len(header_row) == 0:
|
|
28
|
+
raise InvalidRowError("header is not available")
|
|
29
|
+
|
|
30
|
+
# header must contain at least three columns: blank, driver, first lane
|
|
31
|
+
header_len = len(header_row)
|
|
32
|
+
if header_len <= self._NUM_COLS_BEFORE_LANES:
|
|
33
|
+
raise InvalidRowError("header has too few columns")
|
|
34
|
+
|
|
35
|
+
# get the lane numbers from the header row
|
|
36
|
+
lane_numbers = []
|
|
37
|
+
i = self._NUM_COLS_BEFORE_LANES
|
|
38
|
+
while i < header_len:
|
|
39
|
+
col_str = header_row[i].strip()
|
|
40
|
+
if not col_str:
|
|
41
|
+
break
|
|
42
|
+
lane_numbers.append(
|
|
43
|
+
convert_str_to_int(col_str, "text in header lane column is invalid")
|
|
44
|
+
)
|
|
45
|
+
i = i + 1
|
|
46
|
+
|
|
47
|
+
return lane_numbers
|
|
48
|
+
|
|
49
|
+
def parse(self) -> list[tuple[str, list[tuple[int, int, Decimal]]]]:
|
|
50
|
+
entries = []
|
|
51
|
+
|
|
52
|
+
# header row with lane numbers must exist
|
|
53
|
+
lane_numbers = self._fetch_header_lane_numbers()
|
|
54
|
+
len_lanes = len(lane_numbers)
|
|
55
|
+
|
|
56
|
+
# the body must contain at least one row
|
|
57
|
+
current_row = self._reader.fetch_next_row()
|
|
58
|
+
if current_row is None or len(current_row) == 0:
|
|
59
|
+
raise InvalidRowError("row is not available")
|
|
60
|
+
|
|
61
|
+
# evaluate all body rows
|
|
62
|
+
while current_row is not None and len(current_row) > 0:
|
|
63
|
+
if len(current_row) < self._NUM_COLS_BEFORE_LANES + len_lanes:
|
|
64
|
+
raise InvalidRowError("row has too few columns")
|
|
65
|
+
|
|
66
|
+
driver_name = current_row[1].strip()
|
|
67
|
+
if len(driver_name) > 0:
|
|
68
|
+
round_num = 1
|
|
69
|
+
heats: list[tuple[int, int, Decimal]] = []
|
|
70
|
+
entries.append((driver_name, heats))
|
|
71
|
+
else:
|
|
72
|
+
round_num += 1
|
|
73
|
+
|
|
74
|
+
i = 0
|
|
75
|
+
while i < len_lanes:
|
|
76
|
+
col_idx = self._NUM_COLS_BEFORE_LANES + i
|
|
77
|
+
col_str = current_row[col_idx].strip()
|
|
78
|
+
if col_str and col_str != self._BROKEN_COL_STR:
|
|
79
|
+
lane_num = lane_numbers[i]
|
|
80
|
+
value = convert_comma_str_to_decimal(
|
|
81
|
+
col_str, "text in lane column is invalid"
|
|
82
|
+
)
|
|
83
|
+
heats.append((round_num, lane_num, value))
|
|
84
|
+
i = i + 1
|
|
85
|
+
|
|
86
|
+
current_row = self._reader.fetch_next_row()
|
|
87
|
+
|
|
88
|
+
return entries
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class LapsPerLaneOverviewParser(DefaultParser):
|
|
92
|
+
def parse(self) -> list[LapsPerLaneOverviewEntry]:
|
|
93
|
+
return [
|
|
94
|
+
LapsPerLaneOverviewEntry(
|
|
95
|
+
driver_name=driver_name,
|
|
96
|
+
heats=[
|
|
97
|
+
LapsOnLane(
|
|
98
|
+
round_num=round_num, lane_num=lane_num, lap_count=lap_count
|
|
99
|
+
)
|
|
100
|
+
for round_num, lane_num, lap_count in heats
|
|
101
|
+
],
|
|
102
|
+
)
|
|
103
|
+
for driver_name, heats in LanesRelatedOverviewParser(
|
|
104
|
+
self._get_reader()
|
|
105
|
+
).parse()
|
|
106
|
+
]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class FastestLapsOverviewParser(DefaultParser):
|
|
110
|
+
def __init__(self, reader: StatefulRowReader) -> None:
|
|
111
|
+
self._reader = reader
|
|
112
|
+
|
|
113
|
+
def parse(self) -> list[FastestLapsOverviewEntry]:
|
|
114
|
+
return [
|
|
115
|
+
FastestLapsOverviewEntry(
|
|
116
|
+
driver_name=driver_name,
|
|
117
|
+
heats=[
|
|
118
|
+
LapTimeOnLane(
|
|
119
|
+
round_num=round_num, lane_num=lane_num, lap_time=lap_time
|
|
120
|
+
)
|
|
121
|
+
for round_num, lane_num, lap_time in heats
|
|
122
|
+
],
|
|
123
|
+
)
|
|
124
|
+
for driver_name, heats in LanesRelatedOverviewParser(
|
|
125
|
+
self._get_reader()
|
|
126
|
+
).parse()
|
|
127
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from bepfe_export_parser.win.convert import (
|
|
2
|
+
convert_comma_str_to_decimal,
|
|
3
|
+
convert_str_to_int,
|
|
4
|
+
convert_time_str_to_decimal,
|
|
5
|
+
)
|
|
6
|
+
from bepfe_export_parser.win.model import (
|
|
7
|
+
DetailedLapTimeInHeat,
|
|
8
|
+
QualifyingEntry,
|
|
9
|
+
ShortResultEntry,
|
|
10
|
+
)
|
|
11
|
+
from bepfe_export_parser.exceptions import InvalidRowError
|
|
12
|
+
from bepfe_export_parser.win.parse.abstract import DefaultParser
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class QualifyingParser(DefaultParser):
|
|
16
|
+
def parse(self) -> list[QualifyingEntry]:
|
|
17
|
+
entries = []
|
|
18
|
+
|
|
19
|
+
reader = self._get_reader()
|
|
20
|
+
|
|
21
|
+
current_row = reader.fetch_next_row()
|
|
22
|
+
if current_row is None or len(current_row) == 0:
|
|
23
|
+
raise InvalidRowError("row is not available")
|
|
24
|
+
|
|
25
|
+
while current_row is not None and len(current_row) > 0:
|
|
26
|
+
if len(current_row) < 3:
|
|
27
|
+
raise InvalidRowError("row has too few columns")
|
|
28
|
+
|
|
29
|
+
entries.append(
|
|
30
|
+
QualifyingEntry(
|
|
31
|
+
position_num=convert_str_to_int(
|
|
32
|
+
current_row[0], "text in position column is invalid"
|
|
33
|
+
),
|
|
34
|
+
driver_name=current_row[1],
|
|
35
|
+
lap_time=convert_comma_str_to_decimal(
|
|
36
|
+
current_row[2], "text in lap time column is invalid"
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
)
|
|
40
|
+
current_row = reader.fetch_next_row()
|
|
41
|
+
|
|
42
|
+
return entries
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ShortResultParser(DefaultParser):
|
|
46
|
+
def parse(self) -> list[ShortResultEntry]:
|
|
47
|
+
entries = []
|
|
48
|
+
|
|
49
|
+
reader = self._get_reader()
|
|
50
|
+
|
|
51
|
+
header_row = reader.fetch_next_row() # skip header
|
|
52
|
+
if header_row is None or len(header_row) == 0:
|
|
53
|
+
raise InvalidRowError("header is not available")
|
|
54
|
+
|
|
55
|
+
header_len = len(header_row)
|
|
56
|
+
|
|
57
|
+
current_row = reader.fetch_next_row()
|
|
58
|
+
if current_row is None or len(current_row) == 0:
|
|
59
|
+
raise InvalidRowError("row is not available")
|
|
60
|
+
|
|
61
|
+
while current_row is not None and len(current_row) > 0:
|
|
62
|
+
if len(current_row) < header_len:
|
|
63
|
+
raise InvalidRowError("row has too few columns")
|
|
64
|
+
|
|
65
|
+
entries.append(
|
|
66
|
+
ShortResultEntry(
|
|
67
|
+
position_num=convert_str_to_int(
|
|
68
|
+
current_row[0], "text in position column is invalid"
|
|
69
|
+
),
|
|
70
|
+
driver_name=current_row[1],
|
|
71
|
+
total_lap_count=convert_comma_str_to_decimal(
|
|
72
|
+
current_row[2], "text in laps column is invalid"
|
|
73
|
+
),
|
|
74
|
+
total_time=convert_time_str_to_decimal(
|
|
75
|
+
current_row[3], "text in time column is invalid"
|
|
76
|
+
),
|
|
77
|
+
average_lap_time=convert_comma_str_to_decimal(
|
|
78
|
+
current_row[4], "text in average lap time column is invalid"
|
|
79
|
+
),
|
|
80
|
+
fastest_lap=DetailedLapTimeInHeat(
|
|
81
|
+
lap_time=convert_comma_str_to_decimal(
|
|
82
|
+
current_row[5], "text in fastest lap time column is invalid"
|
|
83
|
+
),
|
|
84
|
+
heat_num=convert_str_to_int(
|
|
85
|
+
current_row[6], "text in fastest lap heat column is invalid"
|
|
86
|
+
),
|
|
87
|
+
lane_num=convert_str_to_int(
|
|
88
|
+
current_row[7], "text in fastest lap lane column is invalid"
|
|
89
|
+
),
|
|
90
|
+
lap_num=convert_str_to_int(
|
|
91
|
+
current_row[8], "text in fastest lap lap column is invalid"
|
|
92
|
+
),
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
current_row = reader.fetch_next_row()
|
|
98
|
+
|
|
99
|
+
return entries
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from bepfe_export_parser.exceptions import ConvertingError
|
|
2
|
+
from bepfe_export_parser.win.convert import convert_str_to_int
|
|
3
|
+
from bepfe_export_parser.win.model import (
|
|
4
|
+
HeatLength,
|
|
5
|
+
HeatLengthUnitEnum,
|
|
6
|
+
RaceModeEnum,
|
|
7
|
+
RaceTypeEnum,
|
|
8
|
+
)
|
|
9
|
+
from bepfe_export_parser.win.parse.abstract import DefaultParser
|
|
10
|
+
from bepfe_export_parser.win.parse.constants import MINUTES_UNIT
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class HeatLengthParser(DefaultParser):
|
|
14
|
+
def parse(self) -> HeatLength:
|
|
15
|
+
parts = self._get_reader().get_current_row_checked(2)[1].split()
|
|
16
|
+
|
|
17
|
+
if len(parts) != 2:
|
|
18
|
+
raise ConvertingError("text in heat length column is invalid")
|
|
19
|
+
|
|
20
|
+
value_part, unit_part = parts
|
|
21
|
+
|
|
22
|
+
value = convert_str_to_int(
|
|
23
|
+
value_part, "value part in heat length column is invalid"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
if unit_part.strip() != MINUTES_UNIT:
|
|
27
|
+
raise ConvertingError("unit part in heat length column is invalid")
|
|
28
|
+
|
|
29
|
+
seconds = value * 60
|
|
30
|
+
|
|
31
|
+
return HeatLength(value=seconds, unit=HeatLengthUnitEnum.SECONDS)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class RaceModeParser(DefaultParser):
|
|
35
|
+
def parse(self) -> RaceModeEnum:
|
|
36
|
+
text = self._get_reader().get_current_row_checked(2)[1]
|
|
37
|
+
|
|
38
|
+
race_mode = next(
|
|
39
|
+
(entry for entry in RaceModeEnum if entry.value == text),
|
|
40
|
+
None,
|
|
41
|
+
)
|
|
42
|
+
if race_mode is None:
|
|
43
|
+
raise ConvertingError("value in race mode column is invalid")
|
|
44
|
+
|
|
45
|
+
return race_mode
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RaceTypeParser(DefaultParser):
|
|
49
|
+
def parse(self) -> RaceTypeEnum:
|
|
50
|
+
text = self._get_reader().get_current_row_checked(2)[1]
|
|
51
|
+
|
|
52
|
+
race_type = next(
|
|
53
|
+
(entry for entry in RaceTypeEnum if entry.value == text),
|
|
54
|
+
None,
|
|
55
|
+
)
|
|
56
|
+
if race_type is None:
|
|
57
|
+
raise ConvertingError("value in race type column is invalid")
|
|
58
|
+
|
|
59
|
+
return race_type
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from _csv import Reader
|
|
2
|
+
from bepfe_export_parser.exceptions import InvalidRowError
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class StatefulRowReader:
|
|
6
|
+
def __init__(self, reader: Reader) -> None:
|
|
7
|
+
self._reader = reader
|
|
8
|
+
self._current_row: list[str] | None = None
|
|
9
|
+
|
|
10
|
+
def fetch_next_row(self) -> list[str] | None:
|
|
11
|
+
self._current_row = next(self._reader, None)
|
|
12
|
+
return self._current_row
|
|
13
|
+
|
|
14
|
+
def get_current_row(self) -> list[str] | None:
|
|
15
|
+
return self._current_row
|
|
16
|
+
|
|
17
|
+
def get_current_row_checked(self, min_length: int) -> list[str]:
|
|
18
|
+
row = self.get_current_row()
|
|
19
|
+
if row is None or len(row) == 0:
|
|
20
|
+
raise InvalidRowError("row is not available")
|
|
21
|
+
if len(row) < min_length:
|
|
22
|
+
raise InvalidRowError("row has too few columns")
|
|
23
|
+
return row
|