ddapi 0.8.1__tar.gz → 0.9.0__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.
- {ddapi-0.8.1 → ddapi-0.9.0}/PKG-INFO +1 -1
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi/__init__.py +1 -1
- ddapi-0.9.0/ddapi/ddapi/main.py +232 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi/scheme/__init__.py +5 -1
- ddapi-0.9.0/ddapi/ddapi/scheme/ddnet.py +299 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi/scheme/ddstats_tw.py +1 -127
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi/scheme/status_tw.py +47 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi.egg-info/PKG-INFO +1 -1
- {ddapi-0.8.1 → ddapi-0.9.0}/setup.cfg +1 -1
- {ddapi-0.8.1 → ddapi-0.9.0}/setup.py +1 -1
- ddapi-0.8.1/ddapi/ddapi/main.py +0 -173
- ddapi-0.8.1/ddapi/ddapi/scheme/ddnet.py +0 -132
- {ddapi-0.8.1 → ddapi-0.9.0}/LICENSE +0 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/README.md +0 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi/api.py +0 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi.egg-info/SOURCES.txt +0 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi.egg-info/dependency_links.txt +0 -0
- {ddapi-0.8.1 → ddapi-0.9.0}/ddapi/ddapi.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
from urllib.parse import quote
|
|
2
|
+
|
|
3
|
+
from .api import API
|
|
4
|
+
from .scheme import *
|
|
5
|
+
|
|
6
|
+
__all__ = (
|
|
7
|
+
"DDnetApi",
|
|
8
|
+
"DDstats",
|
|
9
|
+
"Status"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MasterEnum(StrEnum):
|
|
14
|
+
master1 = "master1"
|
|
15
|
+
master2 = "master2"
|
|
16
|
+
master3 = "master3"
|
|
17
|
+
master4 = "master4"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DDnetApi(API):
|
|
21
|
+
def __init__(self):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.domain = "ddnet.org"
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def powered() -> str:
|
|
27
|
+
return DDnetApi().domain
|
|
28
|
+
|
|
29
|
+
async def player(self, player: str) -> DDPlayer:
|
|
30
|
+
"""Fetch player data from the API and return it as a dictionary."""
|
|
31
|
+
return await self._generate_model_instance(
|
|
32
|
+
f"https://{self.domain}/players/?json2={quote(player)}",
|
|
33
|
+
DDPlayer
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
async def status(self) -> DDStatus:
|
|
37
|
+
return await self._generate_model_instance(
|
|
38
|
+
f"https://{self.domain}/status/json/stats.json",
|
|
39
|
+
DDStatus
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
async def releases_map(self) -> ReleasesMaps:
|
|
43
|
+
return await self._generate_model_instance(
|
|
44
|
+
f"https://{self.domain}/releases/maps.json",
|
|
45
|
+
ReleasesMaps,
|
|
46
|
+
"maps"
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
async def query(self, player: str) -> Query:
|
|
50
|
+
return await self._generate_model_instance(
|
|
51
|
+
f"https://{self.domain}/players/?query={quote(player)}",
|
|
52
|
+
Query,
|
|
53
|
+
"players"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
async def query_map(self, map_name: str) -> QueryMap:
|
|
57
|
+
return await self._generate_model_instance(
|
|
58
|
+
f"https://{self.domain}/maps/?query={quote(map_name)}",
|
|
59
|
+
QueryMap,
|
|
60
|
+
'maps',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
async def query_mapper(self, player: str) -> QueryMapper:
|
|
64
|
+
return await self._generate_model_instance(
|
|
65
|
+
f"https://{self.domain}/maps/?qmapper={quote(player)}",
|
|
66
|
+
QueryMapper,
|
|
67
|
+
'players',
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def master(self, master: MasterEnum = MasterEnum.master1) -> Master:
|
|
71
|
+
return await self._generate_model_instance(
|
|
72
|
+
f"https://{master}.{self.domain}/ddnet/15/servers.json",
|
|
73
|
+
Master
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
async def map(self, map_name: str) -> DMap:
|
|
77
|
+
return await self._generate_model_instance(
|
|
78
|
+
f"https://{self.domain}/maps/?json={quote(map_name)}",
|
|
79
|
+
DMap
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DDstats(API):
|
|
84
|
+
def __init__(self):
|
|
85
|
+
super().__init__()
|
|
86
|
+
self.domain = 'ddstats.tw'
|
|
87
|
+
|
|
88
|
+
@staticmethod
|
|
89
|
+
def powered() -> str:
|
|
90
|
+
return DDstats().domain
|
|
91
|
+
|
|
92
|
+
async def player(self, player_name: str) -> Player:
|
|
93
|
+
"""Fetch player data from the API and return it as a dictionary."""
|
|
94
|
+
return await self._generate_model_instance(
|
|
95
|
+
f"https://{self.domain}/player/json?player={quote(player_name)}",
|
|
96
|
+
Player
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class Status(API):
|
|
101
|
+
def __init__(self):
|
|
102
|
+
super().__init__()
|
|
103
|
+
self.domain = 'status.tw'
|
|
104
|
+
|
|
105
|
+
@staticmethod
|
|
106
|
+
def powered() -> str:
|
|
107
|
+
return Status().domain
|
|
108
|
+
|
|
109
|
+
async def info(self) -> Info:
|
|
110
|
+
return await self._generate_model_instance(
|
|
111
|
+
f"https://api.{self.domain}/info",
|
|
112
|
+
Info
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
async def master_bans(self) -> BannedMaster:
|
|
116
|
+
return await self._generate_model_instance(
|
|
117
|
+
f"https://api.{self.domain}/master/bans/list",
|
|
118
|
+
BannedMaster,
|
|
119
|
+
"servers"
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def map_list(self) -> List:
|
|
123
|
+
return await self._generate_model_instance(
|
|
124
|
+
f"https://api.{self.domain}/map/list",
|
|
125
|
+
List,
|
|
126
|
+
"data"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
async def map(self, name: str) -> ListData:
|
|
130
|
+
return await self._generate_model_instance(
|
|
131
|
+
f"https://api.{self.domain}/map/name/{quote(name)}",
|
|
132
|
+
ListData
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
async def gametype_list(self) -> List:
|
|
136
|
+
return await self._generate_model_instance(
|
|
137
|
+
f"https://api.{self.domain}/gametype/list",
|
|
138
|
+
List,
|
|
139
|
+
"data"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
async def gametype(self, name: str) -> ListData:
|
|
143
|
+
return await self._generate_model_instance(
|
|
144
|
+
f"https://api.{self.domain}/gametype/name/{quote(name)}",
|
|
145
|
+
ListData
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
async def version_list(self) -> List:
|
|
149
|
+
return await self._generate_model_instance(
|
|
150
|
+
f"https://api.{self.domain}/version/list",
|
|
151
|
+
List,
|
|
152
|
+
"data"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
async def version(self, name: str) -> ListData:
|
|
156
|
+
return await self._generate_model_instance(
|
|
157
|
+
f"https://api.{self.domain}/version/name/{quote(name)}",
|
|
158
|
+
ListData
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
async def server_list(self) -> MasterTw:
|
|
162
|
+
"""Fetch player data from the API and return it as a dictionary."""
|
|
163
|
+
return await self._generate_model_instance(
|
|
164
|
+
f"https://api.{self.domain}/server/list",
|
|
165
|
+
MasterTw,
|
|
166
|
+
"servers"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
async def server(self, ip: str, port: int | str) -> ServerTwOne:
|
|
170
|
+
return await self._generate_model_instance(
|
|
171
|
+
f"https://api.{self.domain}/server/{quote(ip)}/{port}",
|
|
172
|
+
ServerTwOne,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
async def server_charts(self, ip: str, port: int | str, chart: ChartEnum = ChartEnum.day) -> Charts:
|
|
176
|
+
return await self._generate_model_instance(
|
|
177
|
+
f"https://api.{self.domain}/server/{quote(ip)}/{port}/chart/{chart}",
|
|
178
|
+
Charts,
|
|
179
|
+
"charts"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
async def server_history(self, ip: str, port: int | str) -> None:
|
|
183
|
+
# https://api.status.tw/server/{ip}/{port}/history
|
|
184
|
+
raise NotImplementedError()
|
|
185
|
+
|
|
186
|
+
async def server_stats(self, ip: str, port: int | str) -> None:
|
|
187
|
+
# https://api.status.tw/server/{ip}/{port}/stats
|
|
188
|
+
raise NotImplementedError()
|
|
189
|
+
|
|
190
|
+
async def clan_list(self) -> ListPl:
|
|
191
|
+
return await self._generate_model_instance(
|
|
192
|
+
f"https://api.{self.domain}/clan/list",
|
|
193
|
+
ListPl,
|
|
194
|
+
"data"
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
async def clan(self, name: str) -> ListPlData:
|
|
198
|
+
return await self._generate_model_instance(
|
|
199
|
+
f"https://api.{self.domain}/clan/name/{quote(name)}",
|
|
200
|
+
ListPlData
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def player_list(self) -> ListPl:
|
|
204
|
+
return await self._generate_model_instance(
|
|
205
|
+
f"https://api.{self.domain}/player/list",
|
|
206
|
+
ListPl,
|
|
207
|
+
"data"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
async def player(self, name: str) -> ListPlData:
|
|
211
|
+
return await self._generate_model_instance(
|
|
212
|
+
f"https://api.{self.domain}/player/name/{quote(name)}",
|
|
213
|
+
ListPlData
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
async def player_history(self, name: str) -> None:
|
|
217
|
+
# https://api.status.tw/player/name/{name}/history
|
|
218
|
+
raise NotImplementedError()
|
|
219
|
+
|
|
220
|
+
async def stats(self) -> Stats:
|
|
221
|
+
return await self._generate_model_instance(
|
|
222
|
+
f"https://api.{self.domain}/stats",
|
|
223
|
+
Stats
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
async def stats_players(self, t: str) -> None:
|
|
227
|
+
# https://api.status.tw/stats/players/day | week | month
|
|
228
|
+
raise NotImplementedError()
|
|
229
|
+
|
|
230
|
+
async def stats_servers(self, t: str) -> None:
|
|
231
|
+
# https://api.status.tw/stats/servers/week | month
|
|
232
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# ddnet
|
|
2
|
+
from collections import Counter
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Optional, Any
|
|
5
|
+
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
rm_list = ["DD-Persian", "/vDQMHSss8W", '']
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DPoints(BaseModel):
|
|
12
|
+
total: int
|
|
13
|
+
points: Optional[int] = Field(default=None)
|
|
14
|
+
rank: Optional[int] = Field(default=None)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MaxFinishes(BaseModel):
|
|
18
|
+
rank: int
|
|
19
|
+
player: str
|
|
20
|
+
num: int
|
|
21
|
+
time: float
|
|
22
|
+
min_timestamp: int
|
|
23
|
+
max_timestamp: int
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DDRanks(BaseModel):
|
|
27
|
+
rank: int
|
|
28
|
+
player: str
|
|
29
|
+
time: float
|
|
30
|
+
timestamp: int
|
|
31
|
+
country: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class DDTeamRanks(BaseModel):
|
|
35
|
+
rank: int
|
|
36
|
+
players: list[str]
|
|
37
|
+
time: float
|
|
38
|
+
timestamp: int
|
|
39
|
+
country: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class DMap(BaseModel):
|
|
43
|
+
name: str
|
|
44
|
+
website: str
|
|
45
|
+
thumbnail: str
|
|
46
|
+
web_preview: str
|
|
47
|
+
type: str
|
|
48
|
+
points: int
|
|
49
|
+
difficulty: int
|
|
50
|
+
mapper: str
|
|
51
|
+
release: Optional[int] = None
|
|
52
|
+
median_time: float
|
|
53
|
+
first_finish: int
|
|
54
|
+
last_finish: int
|
|
55
|
+
finishes: int
|
|
56
|
+
finishers: int
|
|
57
|
+
biggest_team: int
|
|
58
|
+
width: int
|
|
59
|
+
height: int
|
|
60
|
+
tiles: list[str]
|
|
61
|
+
team_ranks: list[DDTeamRanks]
|
|
62
|
+
ranks: list[DDRanks]
|
|
63
|
+
max_finishes: list[MaxFinishes]
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class DRank(BaseModel):
|
|
67
|
+
points: Optional[int] = None
|
|
68
|
+
rank: Optional[int] = None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class FirstFinish(BaseModel):
|
|
72
|
+
timestamp: float
|
|
73
|
+
map: str
|
|
74
|
+
time: float
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class LastFinish(BaseModel):
|
|
78
|
+
timestamp: float
|
|
79
|
+
map: str
|
|
80
|
+
time: float
|
|
81
|
+
country: str
|
|
82
|
+
type: Optional[str] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class FavoritePartner(BaseModel):
|
|
86
|
+
name: str
|
|
87
|
+
finishes: int
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class DData(BaseModel):
|
|
91
|
+
points: Optional[DPoints] = Field(default=None)
|
|
92
|
+
team_rank: Optional[DRank] = Field(default=None)
|
|
93
|
+
rank: Optional[DRank] = Field(default=None)
|
|
94
|
+
maps: Optional[dict] = Field(default=None)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class DDType(BaseModel):
|
|
98
|
+
Novice: Optional[DData] = None
|
|
99
|
+
Moderate: Optional[DData] = None
|
|
100
|
+
Brutal: Optional[DData] = None
|
|
101
|
+
Insane: Optional[DData] = None
|
|
102
|
+
Dummy: Optional[DData] = None
|
|
103
|
+
DDmaX_Easy: Optional[DData] = Field(default=None, validation_alias='DDmaX.Easy')
|
|
104
|
+
DDmaX_Next: Optional[DData] = Field(default=None, validation_alias='DDmaX.Next')
|
|
105
|
+
DDmaX_Nut: Optional[DData] = Field(default=None, validation_alias='DDmaX.Nut')
|
|
106
|
+
DDmaX_Pro: Optional[DData] = Field(default=None, validation_alias='DDmaX.Pro')
|
|
107
|
+
Oldschool: Optional[DData] = Field(default=None)
|
|
108
|
+
Race: Optional[DData] = None
|
|
109
|
+
Solo: Optional[DData] = None
|
|
110
|
+
total: Optional[DData] = None
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Activity(BaseModel):
|
|
114
|
+
date: str
|
|
115
|
+
hours_played: int
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class FavoriteServer(BaseModel):
|
|
119
|
+
server: str
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class DDPlayer(BaseModel):
|
|
123
|
+
player: str
|
|
124
|
+
points: DPoints
|
|
125
|
+
team_rank: Optional[DRank] = None
|
|
126
|
+
rank: Optional[DRank] = None
|
|
127
|
+
points_last_year: Optional[DRank] = None
|
|
128
|
+
points_last_month: Optional[DRank] = None
|
|
129
|
+
points_last_week: Optional[DRank] = None
|
|
130
|
+
favorite_server: FavoriteServer
|
|
131
|
+
first_finish: FirstFinish
|
|
132
|
+
last_finishes: list[LastFinish]
|
|
133
|
+
favorite_partners: Optional[list[FavoritePartner]] = None
|
|
134
|
+
types: DDType
|
|
135
|
+
activity: list[Activity]
|
|
136
|
+
hours_played_past_365_days: int
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Skin(BaseModel):
|
|
140
|
+
name: Optional[str] = None
|
|
141
|
+
color_body: Optional[int] = None
|
|
142
|
+
color_feet: Optional[int] = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class Client(BaseModel):
|
|
146
|
+
name: str
|
|
147
|
+
clan: str
|
|
148
|
+
country: int
|
|
149
|
+
score: int
|
|
150
|
+
is_player: Optional[bool] = None
|
|
151
|
+
skin: Optional[Skin] = None
|
|
152
|
+
afk: Optional[bool] = None
|
|
153
|
+
team: Optional[int] = None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class Map(BaseModel):
|
|
157
|
+
name: str
|
|
158
|
+
sha256: Optional[str] = None
|
|
159
|
+
size: Optional[int] = None
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class Community(BaseModel):
|
|
163
|
+
id: str
|
|
164
|
+
icon: str
|
|
165
|
+
admin: list[str]
|
|
166
|
+
public_key: Optional[str] = None
|
|
167
|
+
signature: Optional[str] = None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class Info(BaseModel):
|
|
171
|
+
max_clients: int
|
|
172
|
+
max_players: int
|
|
173
|
+
passworded: bool
|
|
174
|
+
game_type: str
|
|
175
|
+
name: str
|
|
176
|
+
map: Map
|
|
177
|
+
version: str
|
|
178
|
+
clients: Optional[list[Client]] = None
|
|
179
|
+
requires_login: Optional[bool] = None
|
|
180
|
+
community: Optional[Community] = None
|
|
181
|
+
|
|
182
|
+
def __len__(self) -> int:
|
|
183
|
+
return len(self.clients)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
class Server(BaseModel):
|
|
187
|
+
addresses: list | str
|
|
188
|
+
location: str = Field(default=None)
|
|
189
|
+
info: Info
|
|
190
|
+
|
|
191
|
+
def __len__(self) -> int:
|
|
192
|
+
return self.count_client
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def count_client(self) -> int:
|
|
196
|
+
if self.info.clients is not None:
|
|
197
|
+
return len(self.info.clients)
|
|
198
|
+
return 0
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class CountServers(BaseModel):
|
|
202
|
+
name: str
|
|
203
|
+
address: str
|
|
204
|
+
game_type: str
|
|
205
|
+
count_clients: int
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
class Master(BaseModel):
|
|
209
|
+
servers: list[Server]
|
|
210
|
+
|
|
211
|
+
def __len__(self) -> int:
|
|
212
|
+
return len(self.servers)
|
|
213
|
+
|
|
214
|
+
def get_clans(self, rm: list[str] = None) -> list[tuple[Any, int]]:
|
|
215
|
+
remove_list = rm_list.copy() if rm is None else rm
|
|
216
|
+
if not self.servers:
|
|
217
|
+
return []
|
|
218
|
+
|
|
219
|
+
dat: Counter[Any] = Counter(
|
|
220
|
+
client.clan
|
|
221
|
+
for server in self.servers
|
|
222
|
+
for client in server.info.clients
|
|
223
|
+
if client != ''
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
for i in remove_list:
|
|
227
|
+
del dat[i]
|
|
228
|
+
return sorted(dat.items(), key=lambda x: x[1], reverse=True)
|
|
229
|
+
|
|
230
|
+
@property
|
|
231
|
+
def count_servers(self) -> list[CountServers]:
|
|
232
|
+
return sorted(
|
|
233
|
+
(
|
|
234
|
+
CountServers(
|
|
235
|
+
count_clients=i.count_client,
|
|
236
|
+
address=i.addresses[0],
|
|
237
|
+
game_type=i.info.game_type,
|
|
238
|
+
name=i.info.name
|
|
239
|
+
)
|
|
240
|
+
if isinstance(i.addresses, list)
|
|
241
|
+
else CountServers(
|
|
242
|
+
count_clients=i.count_client,
|
|
243
|
+
address=i.addresses,
|
|
244
|
+
game_type=i.info.game_type,
|
|
245
|
+
name=i.info.name
|
|
246
|
+
)
|
|
247
|
+
for i in self.servers
|
|
248
|
+
), key=lambda x: x.count_clients, reverse=True)
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def count_clients(self) -> int:
|
|
252
|
+
return sum(i.count_client for i in self.servers)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
class ReleasesMapsData(BaseModel):
|
|
256
|
+
name: str
|
|
257
|
+
website: str
|
|
258
|
+
thumbnail: str
|
|
259
|
+
web_preview: str
|
|
260
|
+
type: str
|
|
261
|
+
points: int
|
|
262
|
+
difficulty: int
|
|
263
|
+
mapper: str
|
|
264
|
+
release: datetime | str
|
|
265
|
+
width: int
|
|
266
|
+
height: int
|
|
267
|
+
tiles: list[str]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
class ReleasesMaps(BaseModel):
|
|
271
|
+
maps: list[ReleasesMapsData]
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
class QueryData(BaseModel):
|
|
275
|
+
points: int
|
|
276
|
+
name: str
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class Query(BaseModel):
|
|
280
|
+
players: list[QueryData]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class QueryMapperData(BaseModel):
|
|
284
|
+
mapper: str
|
|
285
|
+
num_maps: int
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
class QueryMapper(BaseModel):
|
|
289
|
+
players: list[QueryMapperData]
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class QueryMapData(BaseModel):
|
|
293
|
+
name: str
|
|
294
|
+
type: str
|
|
295
|
+
mapper: str
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class QueryMap(BaseModel):
|
|
299
|
+
maps: list[QueryMapData]
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# ddstats.tw
|
|
2
|
-
from collections import Counter
|
|
3
2
|
from datetime import datetime
|
|
4
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
5
4
|
|
|
6
5
|
from pydantic import BaseModel, Field
|
|
7
6
|
|
|
@@ -199,131 +198,6 @@ class Player(BaseModel):
|
|
|
199
198
|
recent_top_10s: Optional[list[RecentTop10s]] = None
|
|
200
199
|
|
|
201
200
|
|
|
202
|
-
class Skin(BaseModel):
|
|
203
|
-
name: Optional[str] = None
|
|
204
|
-
color_body: Optional[int] = None
|
|
205
|
-
color_feet: Optional[int] = None
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
class Client(BaseModel):
|
|
209
|
-
name: str
|
|
210
|
-
clan: str
|
|
211
|
-
country: int
|
|
212
|
-
score: int
|
|
213
|
-
is_player: Optional[bool] = None
|
|
214
|
-
skin: Optional[Skin] = None
|
|
215
|
-
afk: Optional[bool] = None
|
|
216
|
-
team: Optional[int] = None
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
class Map(BaseModel):
|
|
220
|
-
name: str
|
|
221
|
-
sha256: Optional[str] = None
|
|
222
|
-
size: Optional[int] = None
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
class Community(BaseModel):
|
|
226
|
-
id: str
|
|
227
|
-
icon: str
|
|
228
|
-
admin: list[str]
|
|
229
|
-
public_key: Optional[str] = None
|
|
230
|
-
signature: Optional[str] = None
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
class Info(BaseModel):
|
|
234
|
-
max_clients: int
|
|
235
|
-
max_players: int
|
|
236
|
-
passworded: bool
|
|
237
|
-
game_type: str
|
|
238
|
-
name: str
|
|
239
|
-
map: Map
|
|
240
|
-
version: str
|
|
241
|
-
clients: Optional[list[Client]] = None
|
|
242
|
-
requires_login: Optional[bool] = None
|
|
243
|
-
community: Optional[Community] = None
|
|
244
|
-
|
|
245
|
-
def __len__(self) -> int:
|
|
246
|
-
return len(self.clients)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
class Server(BaseModel):
|
|
250
|
-
addresses: Union[list, str]
|
|
251
|
-
location: str = Field(default=None)
|
|
252
|
-
info: Info
|
|
253
|
-
|
|
254
|
-
def __len__(self) -> int:
|
|
255
|
-
return self.count_client
|
|
256
|
-
|
|
257
|
-
@property
|
|
258
|
-
def count_client(self) -> int:
|
|
259
|
-
if self.info.clients is not None:
|
|
260
|
-
return len(self.info.clients)
|
|
261
|
-
return 0
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
class CountServers(BaseModel):
|
|
265
|
-
name: str
|
|
266
|
-
address: str
|
|
267
|
-
game_type: str
|
|
268
|
-
count_clients: int
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
class Master(BaseModel):
|
|
272
|
-
servers: list[Server]
|
|
273
|
-
|
|
274
|
-
def __len__(self) -> int:
|
|
275
|
-
return len(self.servers)
|
|
276
|
-
|
|
277
|
-
def get_clans(self, rm: list[str] = None) -> list[tuple[Any, int]]:
|
|
278
|
-
remove_list = rm_list.copy() if rm is None else rm
|
|
279
|
-
if not self.servers:
|
|
280
|
-
return []
|
|
281
|
-
|
|
282
|
-
dat: Counter[Any] = Counter(
|
|
283
|
-
client.clan
|
|
284
|
-
for server in self.servers
|
|
285
|
-
for client in server.info.clients
|
|
286
|
-
if client != ''
|
|
287
|
-
)
|
|
288
|
-
|
|
289
|
-
for i in remove_list:
|
|
290
|
-
del dat[i]
|
|
291
|
-
return sorted(dat.items(), key=lambda x: x[1], reverse=True)
|
|
292
|
-
|
|
293
|
-
@property
|
|
294
|
-
def count_servers(self) -> list[CountServers]:
|
|
295
|
-
return sorted(
|
|
296
|
-
(
|
|
297
|
-
CountServers(
|
|
298
|
-
count_clients=i.count_client,
|
|
299
|
-
address=i.addresses[0],
|
|
300
|
-
game_type=i.info.game_type,
|
|
301
|
-
name=i.info.name
|
|
302
|
-
)
|
|
303
|
-
if isinstance(i.addresses, list)
|
|
304
|
-
else CountServers(
|
|
305
|
-
count_clients=i.count_client,
|
|
306
|
-
address=i.addresses,
|
|
307
|
-
game_type=i.info.game_type,
|
|
308
|
-
name=i.info.name
|
|
309
|
-
)
|
|
310
|
-
for i in self.servers
|
|
311
|
-
), key=lambda x: x.count_clients, reverse=True)
|
|
312
|
-
|
|
313
|
-
@property
|
|
314
|
-
def count_clients(self) -> int:
|
|
315
|
-
return sum(i.count_client for i in self.servers)
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
class QueryData(BaseModel):
|
|
319
|
-
points: int
|
|
320
|
-
name: str
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
class Query(BaseModel):
|
|
324
|
-
data: list[QueryData]
|
|
325
|
-
|
|
326
|
-
|
|
327
201
|
class DDStatusData(BaseModel):
|
|
328
202
|
name: str = None
|
|
329
203
|
type: str = None
|
|
@@ -37,6 +37,7 @@ class ClientTw(BaseModel):
|
|
|
37
37
|
team: int
|
|
38
38
|
createdAt: datetime
|
|
39
39
|
clan: DataTw | None
|
|
40
|
+
server: "ServerTw" = None
|
|
40
41
|
|
|
41
42
|
|
|
42
43
|
class ServerTw(BaseModel):
|
|
@@ -82,3 +83,49 @@ class Charts(BaseModel):
|
|
|
82
83
|
|
|
83
84
|
class MasterTw(BaseModel):
|
|
84
85
|
servers: list[ServerTw]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class Info(BaseModel):
|
|
89
|
+
app: str
|
|
90
|
+
version: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class BannedMasterData(BaseModel):
|
|
94
|
+
ip: str
|
|
95
|
+
isActive: bool
|
|
96
|
+
reason: str | None
|
|
97
|
+
unbanDate: datetime | None
|
|
98
|
+
createdAt: datetime
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class BannedMaster(BaseModel):
|
|
102
|
+
servers: list[BannedMasterData]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class ListData(BaseModel):
|
|
106
|
+
name: str
|
|
107
|
+
createdAt: datetime
|
|
108
|
+
servers: list[ServerTw]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class List(BaseModel):
|
|
112
|
+
data: list[ListData]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class ListPlData(BaseModel):
|
|
116
|
+
name: str
|
|
117
|
+
createdAt: datetime
|
|
118
|
+
players: list[ClientTw]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class ListPl(BaseModel):
|
|
122
|
+
data: list[ListPlData]
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class Stats(BaseModel):
|
|
126
|
+
numPlayers: int
|
|
127
|
+
numClans: int
|
|
128
|
+
numServers: int
|
|
129
|
+
numMaps: int
|
|
130
|
+
numGameTypes: int
|
|
131
|
+
numVersions: int
|
ddapi-0.8.1/ddapi/ddapi/main.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
from typing import Union
|
|
2
|
-
from urllib.parse import quote
|
|
3
|
-
|
|
4
|
-
from .api import API
|
|
5
|
-
from .scheme import DDPlayer, Master, Query, DMap, DDStatus, Player, MasterTw, ServerTwOne, ChartEnum, Charts
|
|
6
|
-
|
|
7
|
-
__all__ = (
|
|
8
|
-
"DDnetApi",
|
|
9
|
-
"DDstats",
|
|
10
|
-
"Status"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class DDnetApi(API):
|
|
15
|
-
def __init__(self):
|
|
16
|
-
super().__init__()
|
|
17
|
-
self.domain = "ddnet.org"
|
|
18
|
-
|
|
19
|
-
@staticmethod
|
|
20
|
-
def powered() -> str:
|
|
21
|
-
return DDnetApi().domain
|
|
22
|
-
|
|
23
|
-
async def player(self, player_name: str) -> DDPlayer:
|
|
24
|
-
"""Fetch player data from the API and return it as a dictionary."""
|
|
25
|
-
return await self._generate_model_instance(
|
|
26
|
-
f"https://{self.domain}/players/?json2={quote(player_name)}",
|
|
27
|
-
DDPlayer
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
async def status(self) -> Union[DDStatus, None]:
|
|
31
|
-
return await self._generate_model_instance(
|
|
32
|
-
f"https://{self.domain}/status/json/stats.json",
|
|
33
|
-
DDStatus
|
|
34
|
-
)
|
|
35
|
-
|
|
36
|
-
async def query(self, player: str) -> Union[Query, None]:
|
|
37
|
-
return await self._generate_model_instance(
|
|
38
|
-
f"https://{self.domain}/players/?query={quote(player)}",
|
|
39
|
-
Query,
|
|
40
|
-
"data"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
async def master(self) -> Union[Master, None]:
|
|
44
|
-
return await self._generate_model_instance(
|
|
45
|
-
f"https://master1.{self.domain}/ddnet/15/servers.json",
|
|
46
|
-
Master
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
async def map(self, map_name: str) -> Union[DMap, None]:
|
|
50
|
-
return await self._generate_model_instance(
|
|
51
|
-
f"https://{self.domain}/maps/?json={quote(map_name)}",
|
|
52
|
-
DMap
|
|
53
|
-
)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
class DDstats(API):
|
|
57
|
-
def __init__(self):
|
|
58
|
-
super().__init__()
|
|
59
|
-
self.domain = 'ddstats.tw'
|
|
60
|
-
|
|
61
|
-
@staticmethod
|
|
62
|
-
def powered() -> str:
|
|
63
|
-
return DDstats().domain
|
|
64
|
-
|
|
65
|
-
async def player(self, player_name: str) -> Player:
|
|
66
|
-
"""Fetch player data from the API and return it as a dictionary."""
|
|
67
|
-
return await self._generate_model_instance(
|
|
68
|
-
f"https://{self.domain}/player/json?player={quote(player_name)}",
|
|
69
|
-
Player
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class Status(API):
|
|
74
|
-
def __init__(self):
|
|
75
|
-
super().__init__()
|
|
76
|
-
self.domain = 'status.tw'
|
|
77
|
-
|
|
78
|
-
@staticmethod
|
|
79
|
-
def powered() -> str:
|
|
80
|
-
return Status().domain
|
|
81
|
-
|
|
82
|
-
async def info(self) -> None:
|
|
83
|
-
# https://api.status.tw/info
|
|
84
|
-
raise NotImplementedError()
|
|
85
|
-
|
|
86
|
-
async def master_bans(self) -> None:
|
|
87
|
-
# https://api.status.tw/master/bans/list
|
|
88
|
-
raise NotImplementedError()
|
|
89
|
-
|
|
90
|
-
async def map_list(self) -> None:
|
|
91
|
-
# https://api.status.tw/map/list
|
|
92
|
-
raise NotImplementedError()
|
|
93
|
-
|
|
94
|
-
async def map(self, name: str) -> None:
|
|
95
|
-
# https://api.status.tw/map/name/{name}
|
|
96
|
-
raise NotImplementedError()
|
|
97
|
-
|
|
98
|
-
async def gametype_list(self) -> None:
|
|
99
|
-
# https://api.status.tw/gametype/list
|
|
100
|
-
raise NotImplementedError()
|
|
101
|
-
|
|
102
|
-
async def gametype(self, name: str) -> None:
|
|
103
|
-
# https://api.status.tw/gametype/name/{name}
|
|
104
|
-
raise NotImplementedError()
|
|
105
|
-
|
|
106
|
-
async def version_list(self) -> None:
|
|
107
|
-
# https://api.status.tw/version/list
|
|
108
|
-
raise NotImplementedError()
|
|
109
|
-
|
|
110
|
-
async def version(self, name: str) -> None:
|
|
111
|
-
# https://api.status.tw/version/name/{name}
|
|
112
|
-
raise NotImplementedError()
|
|
113
|
-
|
|
114
|
-
async def server_list(self) -> MasterTw:
|
|
115
|
-
"""Fetch player data from the API and return it as a dictionary."""
|
|
116
|
-
return await self._generate_model_instance(
|
|
117
|
-
f"https://api.{self.domain}/server/list",
|
|
118
|
-
MasterTw,
|
|
119
|
-
"servers"
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
async def server(self, ip: str, port: Union[int, str]) -> ServerTwOne:
|
|
123
|
-
return await self._generate_model_instance(
|
|
124
|
-
f"https://api.{self.domain}/server/{quote(ip)}/{port}",
|
|
125
|
-
ServerTwOne,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
async def server_charts(self, ip: str, port: Union[int, str], chart: ChartEnum = ChartEnum.day) -> Charts:
|
|
129
|
-
return await self._generate_model_instance(
|
|
130
|
-
f"https://api.{self.domain}/server/{quote(ip)}/{port}/chart/{chart}",
|
|
131
|
-
Charts,
|
|
132
|
-
"charts"
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
async def server_history(self, ip: str, port: Union[int, str]) -> None:
|
|
136
|
-
# https://api.status.tw/server/{ip}/{port}/history
|
|
137
|
-
raise NotImplementedError()
|
|
138
|
-
|
|
139
|
-
async def server_stats(self, ip: str, port: Union[int, str]) -> None:
|
|
140
|
-
# https://api.status.tw/server/{ip}/{port}/stats
|
|
141
|
-
raise NotImplementedError()
|
|
142
|
-
|
|
143
|
-
async def clan_list(self) -> None:
|
|
144
|
-
# https://api.status.tw/clan/list
|
|
145
|
-
raise NotImplementedError()
|
|
146
|
-
|
|
147
|
-
async def clan(self, name: str) -> None:
|
|
148
|
-
# https://api.status.tw/clan/name/{name}
|
|
149
|
-
raise NotImplementedError()
|
|
150
|
-
|
|
151
|
-
async def player_list(self) -> None:
|
|
152
|
-
# https://api.status.tw/player/list
|
|
153
|
-
raise NotImplementedError()
|
|
154
|
-
|
|
155
|
-
async def player(self, name: str) -> None:
|
|
156
|
-
# https://api.status.tw/player/name/{name}
|
|
157
|
-
raise NotImplementedError()
|
|
158
|
-
|
|
159
|
-
async def player_history(self, name: str) -> None:
|
|
160
|
-
# https://api.status.tw/player/name/{name}/history
|
|
161
|
-
raise NotImplementedError()
|
|
162
|
-
|
|
163
|
-
async def stats(self) -> None:
|
|
164
|
-
# https://api.status.tw/stats
|
|
165
|
-
raise NotImplementedError()
|
|
166
|
-
|
|
167
|
-
async def stats_players(self, t: str) -> None:
|
|
168
|
-
# https://api.status.tw/stats/players/day week month
|
|
169
|
-
raise NotImplementedError()
|
|
170
|
-
|
|
171
|
-
async def stats_servers(self, t: str) -> None:
|
|
172
|
-
# https://api.status.tw/stats/servers/week month
|
|
173
|
-
raise NotImplementedError()
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# ddnet
|
|
2
|
-
from typing import Optional
|
|
3
|
-
|
|
4
|
-
from pydantic import BaseModel, Field
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class DPoints(BaseModel):
|
|
8
|
-
total: int
|
|
9
|
-
points: Optional[int] = Field(default=None)
|
|
10
|
-
rank: Optional[int] = Field(default=None)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
class MaxFinishes(BaseModel):
|
|
14
|
-
rank: int
|
|
15
|
-
player: str
|
|
16
|
-
num: int
|
|
17
|
-
time: float
|
|
18
|
-
min_timestamp: int
|
|
19
|
-
max_timestamp: int
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
class DDRanks(BaseModel):
|
|
23
|
-
rank: int
|
|
24
|
-
player: str
|
|
25
|
-
time: float
|
|
26
|
-
timestamp: int
|
|
27
|
-
country: str
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
class DDTeamRanks(BaseModel):
|
|
31
|
-
rank: int
|
|
32
|
-
players: list[str]
|
|
33
|
-
time: float
|
|
34
|
-
timestamp: int
|
|
35
|
-
country: str
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class DMap(BaseModel):
|
|
39
|
-
name: str
|
|
40
|
-
website: str
|
|
41
|
-
thumbnail: str
|
|
42
|
-
web_preview: str
|
|
43
|
-
type: str
|
|
44
|
-
points: int
|
|
45
|
-
difficulty: int
|
|
46
|
-
mapper: str
|
|
47
|
-
release: Optional[int] = None
|
|
48
|
-
median_time: float
|
|
49
|
-
first_finish: int
|
|
50
|
-
last_finish: int
|
|
51
|
-
finishes: int
|
|
52
|
-
finishers: int
|
|
53
|
-
biggest_team: int
|
|
54
|
-
width: int
|
|
55
|
-
height: int
|
|
56
|
-
tiles: list[str]
|
|
57
|
-
team_ranks: list[DDTeamRanks]
|
|
58
|
-
ranks: list[DDRanks]
|
|
59
|
-
max_finishes: list[MaxFinishes]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class DRank(BaseModel):
|
|
63
|
-
points: Optional[int] = None
|
|
64
|
-
rank: Optional[int] = None
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class FirstFinish(BaseModel):
|
|
68
|
-
timestamp: float
|
|
69
|
-
map: str
|
|
70
|
-
time: float
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
class LastFinish(BaseModel):
|
|
74
|
-
timestamp: float
|
|
75
|
-
map: str
|
|
76
|
-
time: float
|
|
77
|
-
country: str
|
|
78
|
-
type: Optional[str] = None
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class FavoritePartner(BaseModel):
|
|
82
|
-
name: str
|
|
83
|
-
finishes: int
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class DData(BaseModel):
|
|
87
|
-
points: Optional[DPoints] = Field(default=None)
|
|
88
|
-
team_rank: Optional[DRank] = Field(default=None)
|
|
89
|
-
rank: Optional[DRank] = Field(default=None)
|
|
90
|
-
maps: Optional[dict] = Field(default=None)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
class DDType(BaseModel):
|
|
94
|
-
Novice: Optional[DData] = None
|
|
95
|
-
Moderate: Optional[DData] = None
|
|
96
|
-
Brutal: Optional[DData] = None
|
|
97
|
-
Insane: Optional[DData] = None
|
|
98
|
-
Dummy: Optional[DData] = None
|
|
99
|
-
DDmaX_Easy: Optional[DData] = Field(default=None, validation_alias='DDmaX.Easy')
|
|
100
|
-
DDmaX_Next: Optional[DData] = Field(default=None, validation_alias='DDmaX.Next')
|
|
101
|
-
DDmaX_Nut: Optional[DData] = Field(default=None, validation_alias='DDmaX.Nut')
|
|
102
|
-
DDmaX_Pro: Optional[DData] = Field(default=None, validation_alias='DDmaX.Pro')
|
|
103
|
-
Oldschool: Optional[DData] = Field(default=None)
|
|
104
|
-
Race: Optional[DData] = None
|
|
105
|
-
Solo: Optional[DData] = None
|
|
106
|
-
total: Optional[DData] = None
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
class Activity(BaseModel):
|
|
110
|
-
date: str
|
|
111
|
-
hours_played: int
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
class FavoriteServer(BaseModel):
|
|
115
|
-
server: str
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
class DDPlayer(BaseModel):
|
|
119
|
-
player: str
|
|
120
|
-
points: DPoints
|
|
121
|
-
team_rank: Optional[DRank] = None
|
|
122
|
-
rank: Optional[DRank] = None
|
|
123
|
-
points_last_year: Optional[DRank] = None
|
|
124
|
-
points_last_month: Optional[DRank] = None
|
|
125
|
-
points_last_week: Optional[DRank] = None
|
|
126
|
-
favorite_server: FavoriteServer
|
|
127
|
-
first_finish: FirstFinish
|
|
128
|
-
last_finishes: list[LastFinish]
|
|
129
|
-
favorite_partners: Optional[list[FavoritePartner]] = None
|
|
130
|
-
types: DDType
|
|
131
|
-
activity: list[Activity]
|
|
132
|
-
hours_played_past_365_days: int
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|