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.
@@ -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 rfc3339(dt):
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 = bool(strtobool(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 rfc3339(tree)
71
+ return to_rfc3339(tree)
60
72
 
61
73
  return dtype(tree)
62
74