python-ballchasing 0.2.0__py3-none-any.whl → 0.4.0__py3-none-any.whl
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.
- ballchasing/__init__.py +7 -0
- ballchasing/api.py +207 -119
- ballchasing/constants.py +68 -47
- ballchasing/typed/__init__.py +5 -0
- ballchasing/typed/deep_group.py +440 -0
- ballchasing/typed/deep_replay.py +286 -0
- ballchasing/typed/shallow_group.py +18 -0
- ballchasing/typed/shallow_replay.py +67 -0
- ballchasing/typed/shared.py +218 -0
- ballchasing/util.py +16 -4
- python_ballchasing-0.4.0.dist-info/METADATA +108 -0
- python_ballchasing-0.4.0.dist-info/RECORD +19 -0
- {python_ballchasing-0.2.0.dist-info → python_ballchasing-0.4.0.dist-info}/top_level.txt +1 -0
- scripts/make_types.py +194 -0
- scripts/test.py +52 -0
- scripts/test_typed.py +35 -0
- python_ballchasing-0.2.0.dist-info/METADATA +0 -36
- python_ballchasing-0.2.0.dist-info/RECORD +0 -10
- {python_ballchasing-0.2.0.dist-info → python_ballchasing-0.4.0.dist-info}/WHEEL +0 -0
- {python_ballchasing-0.2.0.dist-info → python_ballchasing-0.4.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from ballchasing.typed.shared import _DictToTypeMixin, BasePlayer, BaseReplay, BaseTeam
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class CameraSettings:
|
|
9
|
+
fov: int = 0
|
|
10
|
+
height: int = 0
|
|
11
|
+
pitch: int = 0
|
|
12
|
+
distance: int = 0
|
|
13
|
+
stiffness: float = 0.0
|
|
14
|
+
swivel_speed: float = 0.0
|
|
15
|
+
transition_speed: int = 0
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class TeamBallStatsDR:
|
|
20
|
+
possession_time: float = 0.0
|
|
21
|
+
time_in_side: float = 0.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class TeamCoreStatsDR:
|
|
26
|
+
shots: int = 0
|
|
27
|
+
shots_against: int = 0
|
|
28
|
+
goals: int = 0
|
|
29
|
+
goals_against: int = 0
|
|
30
|
+
saves: int = 0
|
|
31
|
+
assists: int = 0
|
|
32
|
+
score: int = 0
|
|
33
|
+
shooting_percentage: int = 0
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class PlayerCoreStatsDR:
|
|
38
|
+
shots: int = 0
|
|
39
|
+
shots_against: int = 0
|
|
40
|
+
goals: int = 0
|
|
41
|
+
goals_against: int = 0
|
|
42
|
+
saves: int = 0
|
|
43
|
+
assists: int = 0
|
|
44
|
+
score: int = 0
|
|
45
|
+
mvp: bool = False
|
|
46
|
+
shooting_percentage: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class TeamBoostStatsDR:
|
|
51
|
+
bpm: int = 0
|
|
52
|
+
bcpm: float = 0.0
|
|
53
|
+
avg_amount: float = 0.0
|
|
54
|
+
amount_collected: int = 0
|
|
55
|
+
amount_stolen: int = 0
|
|
56
|
+
amount_collected_big: int = 0
|
|
57
|
+
amount_stolen_big: int = 0
|
|
58
|
+
amount_collected_small: int = 0
|
|
59
|
+
amount_stolen_small: int = 0
|
|
60
|
+
count_collected_big: int = 0
|
|
61
|
+
count_stolen_big: int = 0
|
|
62
|
+
count_collected_small: int = 0
|
|
63
|
+
count_stolen_small: int = 0
|
|
64
|
+
amount_overfill: int = 0
|
|
65
|
+
amount_overfill_stolen: int = 0
|
|
66
|
+
amount_used_while_supersonic: int = 0
|
|
67
|
+
time_zero_boost: float = 0.0
|
|
68
|
+
time_full_boost: float = 0.0
|
|
69
|
+
time_boost_0_25: float = 0.0
|
|
70
|
+
time_boost_25_50: float = 0.0
|
|
71
|
+
time_boost_50_75: float = 0.0
|
|
72
|
+
time_boost_75_100: float = 0.0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass
|
|
76
|
+
class PlayerBoostStatsDR:
|
|
77
|
+
bpm: int = 0
|
|
78
|
+
bcpm: float = 0.0
|
|
79
|
+
avg_amount: float = 0.0
|
|
80
|
+
amount_collected: int = 0
|
|
81
|
+
amount_stolen: int = 0
|
|
82
|
+
amount_collected_big: int = 0
|
|
83
|
+
amount_stolen_big: int = 0
|
|
84
|
+
amount_collected_small: int = 0
|
|
85
|
+
amount_stolen_small: int = 0
|
|
86
|
+
count_collected_big: int = 0
|
|
87
|
+
count_stolen_big: int = 0
|
|
88
|
+
count_collected_small: int = 0
|
|
89
|
+
count_stolen_small: int = 0
|
|
90
|
+
amount_overfill: int = 0
|
|
91
|
+
amount_overfill_stolen: int = 0
|
|
92
|
+
amount_used_while_supersonic: int = 0
|
|
93
|
+
time_zero_boost: float = 0.0
|
|
94
|
+
percent_zero_boost: float = 0.0
|
|
95
|
+
time_full_boost: float = 0.0
|
|
96
|
+
percent_full_boost: float = 0.0
|
|
97
|
+
time_boost_0_25: float = 0.0
|
|
98
|
+
time_boost_25_50: float = 0.0
|
|
99
|
+
time_boost_50_75: float = 0.0
|
|
100
|
+
time_boost_75_100: float = 0.0
|
|
101
|
+
percent_boost_0_25: float = 0.0
|
|
102
|
+
percent_boost_25_50: float = 0.0
|
|
103
|
+
percent_boost_50_75: float = 0.0
|
|
104
|
+
percent_boost_75_100: float = 0.0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class TeamMovementStatsDR:
|
|
109
|
+
total_distance: int = 0
|
|
110
|
+
time_supersonic_speed: float = 0.0
|
|
111
|
+
time_boost_speed: float = 0.0
|
|
112
|
+
time_slow_speed: float = 0.0
|
|
113
|
+
time_ground: float = 0.0
|
|
114
|
+
time_low_air: float = 0.0
|
|
115
|
+
time_high_air: float = 0.0
|
|
116
|
+
time_powerslide: float = 0.0
|
|
117
|
+
count_powerslide: int = 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@dataclass
|
|
121
|
+
class PlayerMovementStatsDR:
|
|
122
|
+
avg_speed: int = 0
|
|
123
|
+
total_distance: int = 0
|
|
124
|
+
time_supersonic_speed: float = 0.0
|
|
125
|
+
time_boost_speed: float = 0.0
|
|
126
|
+
time_slow_speed: float = 0.0
|
|
127
|
+
time_ground: float = 0.0
|
|
128
|
+
time_low_air: float = 0.0
|
|
129
|
+
time_high_air: float = 0.0
|
|
130
|
+
time_powerslide: float = 0.0
|
|
131
|
+
count_powerslide: int = 0
|
|
132
|
+
avg_powerslide_duration: float = 0.0
|
|
133
|
+
avg_speed_percentage: float = 0.0
|
|
134
|
+
percent_slow_speed: float = 0.0
|
|
135
|
+
percent_boost_speed: float = 0.0
|
|
136
|
+
percent_supersonic_speed: float = 0.0
|
|
137
|
+
percent_ground: float = 0.0
|
|
138
|
+
percent_low_air: float = 0.0
|
|
139
|
+
percent_high_air: float = 0.0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@dataclass
|
|
143
|
+
class TeamPositioningStatsDR:
|
|
144
|
+
time_defensive_third: float = 0.0
|
|
145
|
+
time_neutral_third: float = 0.0
|
|
146
|
+
time_offensive_third: float = 0.0
|
|
147
|
+
time_defensive_half: float = 0.0
|
|
148
|
+
time_offensive_half: float = 0.0
|
|
149
|
+
time_behind_ball: float = 0.0
|
|
150
|
+
time_infront_ball: float = 0.0
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class PlayerPositioningStatsDR:
|
|
155
|
+
avg_distance_to_ball: int = 0
|
|
156
|
+
avg_distance_to_ball_possession: int = 0
|
|
157
|
+
avg_distance_to_ball_no_possession: int = 0
|
|
158
|
+
avg_distance_to_mates: int = 0
|
|
159
|
+
time_defensive_third: float = 0.0
|
|
160
|
+
time_neutral_third: float = 0.0
|
|
161
|
+
time_offensive_third: float = 0.0
|
|
162
|
+
time_defensive_half: float = 0.0
|
|
163
|
+
time_offensive_half: float = 0.0
|
|
164
|
+
time_behind_ball: float = 0.0
|
|
165
|
+
time_infront_ball: float = 0.0
|
|
166
|
+
time_most_back: float = 0.0
|
|
167
|
+
time_most_forward: float = 0.0
|
|
168
|
+
time_closest_to_ball: float = 0.0
|
|
169
|
+
time_farthest_from_ball: float = 0.0
|
|
170
|
+
percent_defensive_third: float = 0.0
|
|
171
|
+
percent_offensive_third: float = 0.0
|
|
172
|
+
percent_neutral_third: float = 0.0
|
|
173
|
+
percent_defensive_half: float = 0.0
|
|
174
|
+
percent_offensive_half: float = 0.0
|
|
175
|
+
percent_behind_ball: float = 0.0
|
|
176
|
+
percent_infront_ball: float = 0.0
|
|
177
|
+
percent_most_back: float = 0.0
|
|
178
|
+
percent_most_forward: float = 0.0
|
|
179
|
+
percent_closest_to_ball: float = 0.0
|
|
180
|
+
percent_farthest_from_ball: float = 0.0
|
|
181
|
+
goals_against_while_last_defender: int = 0
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@dataclass
|
|
185
|
+
class TeamDemoStatsDR:
|
|
186
|
+
inflicted: int = 0
|
|
187
|
+
taken: int = 0
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@dataclass
|
|
191
|
+
class PlayerDemoStatsDR:
|
|
192
|
+
inflicted: int = 0
|
|
193
|
+
taken: int = 0
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
@dataclass
|
|
197
|
+
class TeamStatsDR(_DictToTypeMixin):
|
|
198
|
+
ball: Optional[TeamBallStatsDR] = None
|
|
199
|
+
core: Optional[TeamCoreStatsDR] = None
|
|
200
|
+
boost: Optional[TeamBoostStatsDR] = None
|
|
201
|
+
movement: Optional[TeamMovementStatsDR] = None
|
|
202
|
+
positioning: Optional[TeamPositioningStatsDR] = None
|
|
203
|
+
demo: Optional[TeamDemoStatsDR] = None
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@dataclass
|
|
207
|
+
class PlayerStatsDR(_DictToTypeMixin):
|
|
208
|
+
core: Optional[PlayerCoreStatsDR] = None
|
|
209
|
+
boost: Optional[PlayerBoostStatsDR] = None
|
|
210
|
+
movement: Optional[PlayerMovementStatsDR] = None
|
|
211
|
+
positioning: Optional[PlayerPositioningStatsDR] = None
|
|
212
|
+
demo: Optional[PlayerDemoStatsDR] = None
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@dataclass
|
|
216
|
+
class PlayerDR(BasePlayer):
|
|
217
|
+
# start_time: float = 0.0
|
|
218
|
+
# end_time: float = 0.0
|
|
219
|
+
# name: str = ""
|
|
220
|
+
# id: Optional[PlayerID] = None
|
|
221
|
+
# pro: bool = False
|
|
222
|
+
# mvp: bool = False
|
|
223
|
+
# rank: Optional[Rank] = None
|
|
224
|
+
car_id: int = 0
|
|
225
|
+
car_name: str = ""
|
|
226
|
+
camera: Optional[CameraSettings] = None
|
|
227
|
+
steering_sensitivity: float = 0
|
|
228
|
+
stats: Optional[PlayerStatsDR] = None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@dataclass
|
|
232
|
+
class TeamDR(BaseTeam):
|
|
233
|
+
# name: str = ""
|
|
234
|
+
color: str = ""
|
|
235
|
+
players: List[PlayerDR] = field(default_factory=list)
|
|
236
|
+
stats: Optional[TeamStatsDR] = None
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class Server:
|
|
241
|
+
name: str = ""
|
|
242
|
+
region: str = ""
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@dataclass
|
|
246
|
+
class DeepReplay(BaseReplay):
|
|
247
|
+
# id: str = ""
|
|
248
|
+
# link: str = ""
|
|
249
|
+
# rocket_league_id: str = ""
|
|
250
|
+
# recorder: str = ""
|
|
251
|
+
# map_code: str = ""
|
|
252
|
+
# map_name: str = ""
|
|
253
|
+
# playlist_id: str = ""
|
|
254
|
+
# playlist_name: str = ""
|
|
255
|
+
# duration: int = 0
|
|
256
|
+
# overtime: bool = False
|
|
257
|
+
# overtime_seconds: int = 0
|
|
258
|
+
# season: int = 0
|
|
259
|
+
# season_type: str = ""
|
|
260
|
+
# date: Optional[datetime] = None
|
|
261
|
+
# visibility: str = ""
|
|
262
|
+
# created: Optional[datetime] = None
|
|
263
|
+
# uploader: Optional[User] = None
|
|
264
|
+
# min_rank: Optional[Rank] = None
|
|
265
|
+
# max_rank: Optional[Rank] = None
|
|
266
|
+
# groups: List[BaseGroup] = field(default_factory=list)
|
|
267
|
+
title: str = "" # replay_title in shallow replay
|
|
268
|
+
date_has_timezone: bool = False # date_has_tz in shallow replay
|
|
269
|
+
status: str = ""
|
|
270
|
+
match_guid: str = ""
|
|
271
|
+
match_type: str = ""
|
|
272
|
+
team_size: int = 0
|
|
273
|
+
blue: Optional[TeamDR] = None
|
|
274
|
+
orange: Optional[TeamDR] = None
|
|
275
|
+
server: Optional[Server] = None
|
|
276
|
+
|
|
277
|
+
def scoreline(self):
|
|
278
|
+
try:
|
|
279
|
+
bg = self.blue.stats.core.goals
|
|
280
|
+
except AttributeError:
|
|
281
|
+
bg = 0
|
|
282
|
+
try:
|
|
283
|
+
og = self.orange.stats.core.goals
|
|
284
|
+
except AttributeError:
|
|
285
|
+
og = 0
|
|
286
|
+
return bg, og
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
from ballchasing.typed.shared import User, BaseGroup
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class ShallowGroup(BaseGroup):
|
|
9
|
+
# id: str = ""
|
|
10
|
+
# link: str = ""
|
|
11
|
+
# name: str = ""
|
|
12
|
+
# created: Optional[datetime] = None
|
|
13
|
+
# player_identification: str = ""
|
|
14
|
+
# team_identification: str = ""
|
|
15
|
+
# shared: bool = False
|
|
16
|
+
direct_replays: int = 0
|
|
17
|
+
indirect_replays: int = 0
|
|
18
|
+
user: Optional[User] = None
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from ballchasing.typed.shared import _DictToTypeMixin, BasePlayer, BaseTeam, BaseReplay
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class PlayerSR(BasePlayer):
|
|
9
|
+
# start_time: float = 0.0
|
|
10
|
+
# end_time: float = 0.0
|
|
11
|
+
# name: str = ""
|
|
12
|
+
# id: Optional[PlayerID] = None
|
|
13
|
+
# pro: bool = False
|
|
14
|
+
# mvp: bool = False
|
|
15
|
+
# rank: Optional[Rank] = None
|
|
16
|
+
score: int = 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class TeamSR(BaseTeam):
|
|
21
|
+
# name: str = ""
|
|
22
|
+
goals: int = 0
|
|
23
|
+
players: List[PlayerSR] = field(default_factory=list)
|
|
24
|
+
|
|
25
|
+
def __eq__(self, other):
|
|
26
|
+
if isinstance(other, TeamSR):
|
|
27
|
+
for p1 in self.players:
|
|
28
|
+
for p2 in other.players:
|
|
29
|
+
if p1 == p2:
|
|
30
|
+
break
|
|
31
|
+
else:
|
|
32
|
+
return False
|
|
33
|
+
return True
|
|
34
|
+
return NotImplemented
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class ShallowReplay(BaseReplay):
|
|
39
|
+
# id: str = ""
|
|
40
|
+
# link: str = ""
|
|
41
|
+
# rocket_league_id: str = ""
|
|
42
|
+
# recorder: str = ""
|
|
43
|
+
# map_code: str = ""
|
|
44
|
+
# map_name: str = ""
|
|
45
|
+
# playlist_id: str = ""
|
|
46
|
+
# playlist_name: str = ""
|
|
47
|
+
# duration: int = 0
|
|
48
|
+
# overtime: bool = False
|
|
49
|
+
# overtime_seconds: int = 0
|
|
50
|
+
# season: int = 0
|
|
51
|
+
# season_type: str = ""
|
|
52
|
+
# date: Optional[datetime] = None
|
|
53
|
+
# visibility: str = ""
|
|
54
|
+
# created: Optional[datetime] = None
|
|
55
|
+
# uploader: Optional[User] = None
|
|
56
|
+
# min_rank: Optional[Rank] = None
|
|
57
|
+
# max_rank: Optional[Rank] = None
|
|
58
|
+
# groups: List[BaseGroup] = field(default_factory=list)
|
|
59
|
+
replay_title: str = "" # title in deep replay
|
|
60
|
+
date_has_tz: bool = False # date_has_timezone in deep replay
|
|
61
|
+
blue: Optional[TeamSR] = None
|
|
62
|
+
orange: Optional[TeamSR] = None
|
|
63
|
+
|
|
64
|
+
def scoreline(self):
|
|
65
|
+
bg = self.blue.goals if self.blue else 0
|
|
66
|
+
og = self.orange.goals if self.orange else 0
|
|
67
|
+
return bg, og
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
from dataclasses import dataclass, fields, field
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from functools import total_ordering
|
|
4
|
+
from typing import get_args, Optional, List
|
|
5
|
+
|
|
6
|
+
from ballchasing.util import from_rfc3339
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class _DictToTypeMixin:
|
|
10
|
+
"""
|
|
11
|
+
Mixin class to convert dicts to their respective types in dataclasses.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def __post_init__(self):
|
|
15
|
+
for field in fields(self):
|
|
16
|
+
value = getattr(self, field.name)
|
|
17
|
+
actual_type = field.type
|
|
18
|
+
if args := get_args(field.type):
|
|
19
|
+
actual_type = args[0]
|
|
20
|
+
if isinstance(value, dict):
|
|
21
|
+
new_value = actual_type(**value)
|
|
22
|
+
setattr(self, field.name, new_value)
|
|
23
|
+
elif isinstance(value, list):
|
|
24
|
+
new_value = [actual_type(**item) if isinstance(item, dict) else item for item in value]
|
|
25
|
+
setattr(self, field.name, new_value)
|
|
26
|
+
elif isinstance(value, str) and actual_type is datetime:
|
|
27
|
+
# Load rfc3339 formatted strings as datetime objects
|
|
28
|
+
new_value = from_rfc3339(value)
|
|
29
|
+
setattr(self, field.name, new_value)
|
|
30
|
+
# else: # For testing
|
|
31
|
+
# # Check that the value has the type specified in the dataclass field
|
|
32
|
+
# if value is not None and not isinstance(value, actual_type):
|
|
33
|
+
# if isinstance(value, int) and actual_type is float:
|
|
34
|
+
# pass # ints can be converted losslessly to floats
|
|
35
|
+
# else:
|
|
36
|
+
# raise TypeError(f"Expected {field.name} to be of type {actual_type}, got {type(value)}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class User:
|
|
41
|
+
steam_id: str = ""
|
|
42
|
+
name: str = ""
|
|
43
|
+
profile_url: str = ""
|
|
44
|
+
avatar: str = ""
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other):
|
|
47
|
+
if isinstance(other, User):
|
|
48
|
+
return self.steam_id == other.steam_id
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class PlayerID:
|
|
54
|
+
platform: str = ""
|
|
55
|
+
id: str = ""
|
|
56
|
+
player_number: int = 0
|
|
57
|
+
|
|
58
|
+
def __repr__(self):
|
|
59
|
+
if self.player_number == 0:
|
|
60
|
+
return f"{self.platform}:{self.id}"
|
|
61
|
+
return f"{self.platform}:{self.id}#{self.player_number}"
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@total_ordering
|
|
65
|
+
@dataclass
|
|
66
|
+
class Rank:
|
|
67
|
+
tier: int = 0
|
|
68
|
+
division: int = 0
|
|
69
|
+
name: str = ""
|
|
70
|
+
id: str = ""
|
|
71
|
+
|
|
72
|
+
def __eq__(self, other):
|
|
73
|
+
return self.tier == other.tier and self.division == other.division
|
|
74
|
+
|
|
75
|
+
def __lt__(self, other):
|
|
76
|
+
return (self.tier, self.division) < (other.tier, other.division)
|
|
77
|
+
|
|
78
|
+
@classmethod
|
|
79
|
+
def from_id(cls, rank_id: str):
|
|
80
|
+
"""
|
|
81
|
+
Create a Rank object from a rank ID string, e.g. "grand-champion-1"
|
|
82
|
+
"""
|
|
83
|
+
from ballchasing.constants import Rank as RankConstants
|
|
84
|
+
ranks = [r for r in RankConstants.ALL if r != "grand-champion"]
|
|
85
|
+
if rank_id == RankConstants.GRAND_CHAMPION_LEGACY:
|
|
86
|
+
rank_id = RankConstants.GRAND_CHAMPION_1
|
|
87
|
+
try:
|
|
88
|
+
tier = ranks.index(rank_id)
|
|
89
|
+
division = 0 # Division 0 is not really a thing but ID doesn't include it
|
|
90
|
+
name = rank_id.replace("-", " ").title().strip()
|
|
91
|
+
name = name.replace("1", "I").replace("2", "II").replace("3", "III")
|
|
92
|
+
name = name + " Division 0"
|
|
93
|
+
return cls(tier=tier, division=division, name=name, id=rank_id)
|
|
94
|
+
except ValueError:
|
|
95
|
+
raise ValueError(f"Invalid rank ID: {rank_id}. Must be one of {ranks}.")
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_name(cls, rank_name: str):
|
|
99
|
+
rank_id = rank_name.lower().replace(" ", "-")
|
|
100
|
+
rank_id = rank_id.replace("i", "1").replace("ii", "2").replace("iii", "3")
|
|
101
|
+
if "-division-" in rank_id:
|
|
102
|
+
rank_id, division = rank_id.split("-division-")
|
|
103
|
+
division = int(division)
|
|
104
|
+
else:
|
|
105
|
+
division = 0
|
|
106
|
+
|
|
107
|
+
res = cls.from_id(rank_id)
|
|
108
|
+
res.division = division
|
|
109
|
+
res.name = rank_name
|
|
110
|
+
return res
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
@dataclass
|
|
114
|
+
class BasePlayer(_DictToTypeMixin):
|
|
115
|
+
start_time: float = 0.0
|
|
116
|
+
end_time: float = 0.0
|
|
117
|
+
name: str = ""
|
|
118
|
+
id: Optional[PlayerID] = None
|
|
119
|
+
pro: bool = False
|
|
120
|
+
mvp: bool = False
|
|
121
|
+
rank: Optional[Rank] = None
|
|
122
|
+
|
|
123
|
+
def __eq__(self, other):
|
|
124
|
+
if isinstance(other, BasePlayer):
|
|
125
|
+
return self.id == other.id
|
|
126
|
+
return NotImplemented
|
|
127
|
+
|
|
128
|
+
def is_bot(self):
|
|
129
|
+
return self.id.id == "" and self.id.platform == ""
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class BaseTeam(_DictToTypeMixin):
|
|
134
|
+
name: str = ""
|
|
135
|
+
# Further specified in subclasses, needed for methods:
|
|
136
|
+
players: List[BasePlayer] = field(default_factory=list)
|
|
137
|
+
|
|
138
|
+
def __eq__(self, other):
|
|
139
|
+
# Teams are equal if all players are the same
|
|
140
|
+
if isinstance(other, BaseTeam):
|
|
141
|
+
for p1 in self.players:
|
|
142
|
+
for p2 in other.players:
|
|
143
|
+
if p1 == p2:
|
|
144
|
+
break
|
|
145
|
+
else:
|
|
146
|
+
return False
|
|
147
|
+
return True
|
|
148
|
+
return NotImplemented
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@dataclass
|
|
152
|
+
class BasicGroup: # Shared between replays and shallow/deep groups
|
|
153
|
+
id: str = ""
|
|
154
|
+
name: str = ""
|
|
155
|
+
link: str = ""
|
|
156
|
+
|
|
157
|
+
def __eq__(self, other):
|
|
158
|
+
if isinstance(other, BasicGroup):
|
|
159
|
+
return self.id == other.id
|
|
160
|
+
return NotImplemented
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@dataclass
|
|
164
|
+
class BaseGroup(BasicGroup, _DictToTypeMixin): # Shared between shallow and deep groups
|
|
165
|
+
created: Optional[datetime] = ""
|
|
166
|
+
player_identification: str = ""
|
|
167
|
+
team_identification: str = ""
|
|
168
|
+
shared: bool = False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@dataclass
|
|
172
|
+
class BaseReplay(_DictToTypeMixin): # Shared between shallow and deep replays
|
|
173
|
+
id: str = ""
|
|
174
|
+
link: str = ""
|
|
175
|
+
rocket_league_id: str = ""
|
|
176
|
+
recorder: str = ""
|
|
177
|
+
map_code: str = ""
|
|
178
|
+
map_name: str = ""
|
|
179
|
+
playlist_id: str = ""
|
|
180
|
+
playlist_name: str = ""
|
|
181
|
+
duration: int = 0
|
|
182
|
+
overtime: bool = False
|
|
183
|
+
overtime_seconds: int = 0
|
|
184
|
+
season: int = 0
|
|
185
|
+
season_type: str = ""
|
|
186
|
+
date: Optional[datetime] = None
|
|
187
|
+
visibility: str = ""
|
|
188
|
+
created: Optional[datetime] = None
|
|
189
|
+
uploader: Optional[User] = None
|
|
190
|
+
min_rank: Optional[Rank] = None
|
|
191
|
+
max_rank: Optional[Rank] = None
|
|
192
|
+
groups: List[BasicGroup] = field(default_factory=list)
|
|
193
|
+
# Further specified in subclasses, needed for methods:
|
|
194
|
+
blue: Optional[BaseTeam] = None
|
|
195
|
+
orange: Optional[BaseTeam] = None
|
|
196
|
+
|
|
197
|
+
def players(self):
|
|
198
|
+
blue = self.blue.players if self.blue else []
|
|
199
|
+
orange = self.orange.players if self.orange else []
|
|
200
|
+
return blue + orange
|
|
201
|
+
|
|
202
|
+
def teams(self):
|
|
203
|
+
teams = []
|
|
204
|
+
if self.blue:
|
|
205
|
+
teams.append(self.blue)
|
|
206
|
+
if self.orange:
|
|
207
|
+
teams.append(self.orange)
|
|
208
|
+
return teams
|
|
209
|
+
|
|
210
|
+
def team_sizes(self):
|
|
211
|
+
bs = len(self.blue.players) if self.blue else 0
|
|
212
|
+
os = len(self.orange.players) if self.orange else 0
|
|
213
|
+
return bs, os
|
|
214
|
+
|
|
215
|
+
def __eq__(self, other):
|
|
216
|
+
if isinstance(other, BaseReplay):
|
|
217
|
+
return self.id == other.id or self.rocket_league_id == other.rocket_league_id
|
|
218
|
+
return NotImplemented
|
ballchasing/util.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
|
-
from distutils.util import strtobool
|
|
3
2
|
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
def
|
|
6
|
+
def to_rfc3339(dt: Optional[datetime]):
|
|
7
7
|
if dt is None:
|
|
8
8
|
return dt
|
|
9
9
|
elif isinstance(dt, str):
|
|
@@ -14,6 +14,18 @@ def rfc3339(dt):
|
|
|
14
14
|
raise ValueError("Date must be either string or datetime")
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def from_rfc3339(s: str):
|
|
18
|
+
"""
|
|
19
|
+
Convert an RFC3339 formatted string to a datetime object.
|
|
20
|
+
"""
|
|
21
|
+
s = s.replace("Z", "+00:00")
|
|
22
|
+
try:
|
|
23
|
+
dt = datetime.fromisoformat(s)
|
|
24
|
+
except ValueError:
|
|
25
|
+
dt = datetime.strptime(s, "%Y-%m-%dT%H:%M:%S.%f%z")
|
|
26
|
+
return dt
|
|
27
|
+
|
|
28
|
+
|
|
17
29
|
def _get_stats_info():
|
|
18
30
|
cur_path = Path(__file__).parent
|
|
19
31
|
with open(cur_path / "stats_info.tsv") as stats_info:
|
|
@@ -25,7 +37,7 @@ def _get_stats_info():
|
|
|
25
37
|
stat_info = {}
|
|
26
38
|
for k, v in zip(header, row):
|
|
27
39
|
if k.startswith("is_"):
|
|
28
|
-
v =
|
|
40
|
+
v = v.lower() in ("true", "1")
|
|
29
41
|
elif k == "dtype":
|
|
30
42
|
v = {
|
|
31
43
|
"str": str,
|
|
@@ -56,7 +68,7 @@ def get_value(replay, path, dtype, *path_args):
|
|
|
56
68
|
return "MISSING"
|
|
57
69
|
|
|
58
70
|
if dtype == datetime:
|
|
59
|
-
return
|
|
71
|
+
return to_rfc3339(tree)
|
|
60
72
|
|
|
61
73
|
return dtype(tree)
|
|
62
74
|
|