ddapi 0.13.0__tar.gz → 0.14.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.13.0 → ddapi-0.14.0}/PKG-INFO +1 -1
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/__init__.py +1 -1
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/_api.py +26 -32
- ddapi-0.14.0/ddapi/ddapi/api/ddnet.py +56 -0
- ddapi-0.14.0/ddapi/ddapi/api/ddstats.py +38 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/api/status.py +7 -7
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/cache/base.py +9 -6
- ddapi-0.14.0/ddapi/ddapi/cache/empty.py +13 -0
- ddapi-0.14.0/ddapi/ddapi/cache/memory.py +40 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/scheme/ddnet.py +107 -49
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/scheme/ddstats_tw.py +84 -46
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/scheme/status_tw.py +3 -3
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi.egg-info/PKG-INFO +1 -1
- {ddapi-0.13.0 → ddapi-0.14.0}/setup.cfg +1 -1
- {ddapi-0.13.0 → ddapi-0.14.0}/setup.py +4 -2
- ddapi-0.13.0/ddapi/ddapi/api/ddnet.py +0 -72
- ddapi-0.13.0/ddapi/ddapi/api/ddstats.py +0 -44
- ddapi-0.13.0/ddapi/ddapi/cache/empty.py +0 -14
- ddapi-0.13.0/ddapi/ddapi/cache/memory.py +0 -29
- {ddapi-0.13.0 → ddapi-0.14.0}/LICENSE +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/README.md +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/api/__init__.py +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/cache/__init__.py +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/enum.py +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/scheme/__init__.py +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi/types.py +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi.egg-info/SOURCES.txt +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi.egg-info/dependency_links.txt +0 -0
- {ddapi-0.13.0 → ddapi-0.14.0}/ddapi/ddapi.egg-info/top_level.txt +0 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import atexit
|
|
2
3
|
import logging
|
|
3
|
-
from
|
|
4
|
-
from
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from json import JSONDecoder
|
|
6
|
+
from typing import Optional
|
|
5
7
|
|
|
6
8
|
import aiohttp
|
|
7
9
|
from aiohttp import ClientSession
|
|
@@ -18,62 +20,52 @@ except ImportError:
|
|
|
18
20
|
|
|
19
21
|
logger = logging.getLogger(__name__)
|
|
20
22
|
|
|
21
|
-
|
|
23
|
+
|
|
24
|
+
class API:
|
|
22
25
|
def __init__(
|
|
23
26
|
self,
|
|
24
27
|
session: ClientSession = None,
|
|
25
28
|
*,
|
|
26
29
|
autoclose: bool = True,
|
|
27
|
-
cache: CacheABC =
|
|
28
|
-
json_loads:
|
|
29
|
-
):
|
|
30
|
+
cache: CacheABC = None,
|
|
31
|
+
json_loads: JSONDecoder = DEFAULT_JSON_DECODER,
|
|
32
|
+
) -> None:
|
|
30
33
|
self.__session = session
|
|
31
|
-
self.cache = cache
|
|
34
|
+
self.cache = cache or EmptyCache()
|
|
32
35
|
self.json_loads = json_loads
|
|
33
36
|
self.autoclose = autoclose
|
|
34
37
|
|
|
35
38
|
if self.__session is None:
|
|
36
39
|
self.__session = aiohttp.ClientSession()
|
|
37
|
-
logger.debug("Created a new aiohttp ClientSession.")
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
atexit.register(self.close_api)
|
|
42
|
+
|
|
43
|
+
async def __aenter__(self) -> "API":
|
|
40
44
|
if self.__session is None:
|
|
41
45
|
self.__session = aiohttp.ClientSession()
|
|
42
|
-
logger.debug("Created a new aiohttp ClientSession on enter.")
|
|
43
46
|
return self
|
|
44
47
|
|
|
45
|
-
async def __aexit__(self, _exc_type, _exc_val, _exc_tb):
|
|
48
|
+
async def __aexit__(self, _exc_type, _exc_val, _exc_tb) -> None: # noqa: ANN001
|
|
46
49
|
if self.autoclose:
|
|
47
|
-
logger.debug("Closing aiohttp ClientSession on exit.")
|
|
48
50
|
await self.close()
|
|
49
51
|
|
|
50
|
-
def
|
|
51
|
-
if self.__session is not None and not self.__session.closed:
|
|
52
|
-
loop = asyncio.get_event_loop()
|
|
53
|
-
if loop.is_running():
|
|
54
|
-
logger.warning("Closing aiohttp ClientSession in the background.")
|
|
55
|
-
loop.create_task(self.close())
|
|
56
|
-
else:
|
|
57
|
-
logger.warning("Closing aiohttp ClientSession synchronously.")
|
|
58
|
-
loop.run_until_complete(self.close())
|
|
59
|
-
|
|
60
|
-
async def _get(self, url: str) -> Optional[dict]:
|
|
52
|
+
async def _get(self, url: str) -> dict | None:
|
|
61
53
|
"""Send a GET request to the given URL and return the response as JSON."""
|
|
62
|
-
logger.debug(
|
|
54
|
+
logger.debug("Sending GET request to %s.", url)
|
|
63
55
|
async with self.__session.get(url) as req:
|
|
64
|
-
if req.status ==
|
|
65
|
-
logger.debug(
|
|
56
|
+
if req.status == HTTPStatus.OK.value:
|
|
57
|
+
logger.debug("Received response from %s: %s.", url, req.status)
|
|
66
58
|
return await req.json(loads=self.json_loads)
|
|
67
|
-
logger.error(
|
|
59
|
+
logger.error("Failed to get response from %s: %s.", url, req.status)
|
|
68
60
|
return None
|
|
69
61
|
|
|
70
62
|
async def _generate_model_instance(
|
|
71
63
|
self,
|
|
72
64
|
url: str,
|
|
73
|
-
model:
|
|
74
|
-
k:
|
|
65
|
+
model: type[T],
|
|
66
|
+
k: str | None = None,
|
|
75
67
|
cache_timeout: int = 300,
|
|
76
|
-
) ->
|
|
68
|
+
) -> T | None:
|
|
77
69
|
"""Generate a model instance from the given URL and optional keyword arguments.
|
|
78
70
|
|
|
79
71
|
Args:
|
|
@@ -90,7 +82,7 @@ class API(ABC):
|
|
|
90
82
|
|
|
91
83
|
dat = await self._get(url)
|
|
92
84
|
if dat is None or not dat:
|
|
93
|
-
logger.warning(
|
|
85
|
+
logger.warning("No data received from %s.", url)
|
|
94
86
|
return None
|
|
95
87
|
|
|
96
88
|
data = model(**{k: dat} if k is not None else dat)
|
|
@@ -104,7 +96,6 @@ class API(ABC):
|
|
|
104
96
|
async def close(self) -> None:
|
|
105
97
|
"""Close the aiohttp session."""
|
|
106
98
|
if self.__session is not None:
|
|
107
|
-
logger.debug("Closing aiohttp ClientSession.")
|
|
108
99
|
await self.__session.close()
|
|
109
100
|
self.__session = None
|
|
110
101
|
self.cache = None
|
|
@@ -116,3 +107,6 @@ class API(ABC):
|
|
|
116
107
|
@staticmethod
|
|
117
108
|
def powered() -> None:
|
|
118
109
|
return None
|
|
110
|
+
|
|
111
|
+
def close_api(self) -> None:
|
|
112
|
+
asyncio.run(self.close())
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from ddapi._api import API
|
|
2
|
+
from ddapi.scheme import (
|
|
3
|
+
DDPlayer,
|
|
4
|
+
DDStatus,
|
|
5
|
+
ReleasesMaps,
|
|
6
|
+
Query,
|
|
7
|
+
QueryMap,
|
|
8
|
+
QueryMapper,
|
|
9
|
+
Master,
|
|
10
|
+
DMap,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = ("DDnetApi",)
|
|
14
|
+
|
|
15
|
+
from ddapi.enum import MasterEnum
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class DDnetApi(API):
|
|
19
|
+
@property
|
|
20
|
+
def domain(self) -> str:
|
|
21
|
+
return "ddnet.org"
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def powered() -> str:
|
|
25
|
+
return DDnetApi().domain
|
|
26
|
+
|
|
27
|
+
async def player(self, player: str) -> DDPlayer:
|
|
28
|
+
"""Fetch player data from the API and return it as a dictionary."""
|
|
29
|
+
return await self._generate_model_instance(DDPlayer.api(player), DDPlayer)
|
|
30
|
+
|
|
31
|
+
async def status(self) -> DDStatus:
|
|
32
|
+
return await self._generate_model_instance(DDStatus.api(), DDStatus)
|
|
33
|
+
|
|
34
|
+
async def releases_map(self) -> ReleasesMaps:
|
|
35
|
+
return await self._generate_model_instance(
|
|
36
|
+
ReleasesMaps.api(), ReleasesMaps, "maps"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def query(self, player: str) -> Query:
|
|
40
|
+
return await self._generate_model_instance(Query.api(player), Query, "players")
|
|
41
|
+
|
|
42
|
+
async def query_map(self, map_name: str) -> QueryMap:
|
|
43
|
+
return await self._generate_model_instance(
|
|
44
|
+
QueryMap.api(map_name), QueryMap, "maps"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
async def query_mapper(self, player: str) -> QueryMapper:
|
|
48
|
+
return await self._generate_model_instance(
|
|
49
|
+
QueryMapper.api(player), QueryMapper, "players"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
async def master(self, master: MasterEnum = MasterEnum.master1) -> Master:
|
|
53
|
+
return await self._generate_model_instance(Master.api(master), Master)
|
|
54
|
+
|
|
55
|
+
async def map(self, map_name: str) -> DMap:
|
|
56
|
+
return await self._generate_model_instance(DMap.api(map_name), DMap)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from urllib.parse import quote
|
|
2
|
+
|
|
3
|
+
from ddapi._api import API
|
|
4
|
+
from ddapi.scheme import (
|
|
5
|
+
DProfile,
|
|
6
|
+
SMap,
|
|
7
|
+
Maps,
|
|
8
|
+
Player,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = ("DDstats",)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class DDstats(API):
|
|
15
|
+
@property
|
|
16
|
+
def domain(self) -> str:
|
|
17
|
+
return "ddstats.tw"
|
|
18
|
+
|
|
19
|
+
@staticmethod
|
|
20
|
+
def powered() -> str:
|
|
21
|
+
return DDstats().domain
|
|
22
|
+
|
|
23
|
+
async def profile(self, player: str) -> DProfile:
|
|
24
|
+
return await self._generate_model_instance(
|
|
25
|
+
DProfile.api(player),
|
|
26
|
+
DProfile,
|
|
27
|
+
"profile",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
async def map(self, map_name: str) -> SMap:
|
|
31
|
+
return await self._generate_model_instance(SMap.api(map_name), SMap)
|
|
32
|
+
|
|
33
|
+
async def maps(self) -> Maps:
|
|
34
|
+
return await self._generate_model_instance(Maps.api(), Maps, "maps")
|
|
35
|
+
|
|
36
|
+
async def player(self, player: str) -> Player:
|
|
37
|
+
"""Fetch player data from the API and return it as a dictionary."""
|
|
38
|
+
return await self._generate_model_instance(Player.api(player), Player)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from urllib.parse import quote
|
|
2
2
|
|
|
3
|
-
from
|
|
4
|
-
from
|
|
3
|
+
from ddapi._api import API
|
|
4
|
+
from ddapi.scheme import (
|
|
5
5
|
Info,
|
|
6
6
|
BannedMaster,
|
|
7
7
|
List,
|
|
@@ -90,11 +90,11 @@ class Status(API):
|
|
|
90
90
|
|
|
91
91
|
async def server_history(self, ip: str, port: int | str) -> None:
|
|
92
92
|
# TODO: https://api.status.tw/server/{ip}/{port}/history
|
|
93
|
-
raise NotImplementedError
|
|
93
|
+
raise NotImplementedError
|
|
94
94
|
|
|
95
95
|
async def server_stats(self, ip: str, port: int | str) -> None:
|
|
96
96
|
# TODO: https://api.status.tw/server/{ip}/{port}/stats
|
|
97
|
-
raise NotImplementedError
|
|
97
|
+
raise NotImplementedError
|
|
98
98
|
|
|
99
99
|
async def clan_list(self) -> ListPl:
|
|
100
100
|
return await self._generate_model_instance(
|
|
@@ -118,7 +118,7 @@ class Status(API):
|
|
|
118
118
|
|
|
119
119
|
async def player_history(self, name: str) -> None:
|
|
120
120
|
# TODO: https://api.status.tw/player/name/{name}/history
|
|
121
|
-
raise NotImplementedError
|
|
121
|
+
raise NotImplementedError
|
|
122
122
|
|
|
123
123
|
async def stats(self) -> Stats:
|
|
124
124
|
return await self._generate_model_instance(
|
|
@@ -127,8 +127,8 @@ class Status(API):
|
|
|
127
127
|
|
|
128
128
|
async def stats_players(self, t: str) -> None:
|
|
129
129
|
# TODO: https://api.status.tw/stats/players/day | week | month
|
|
130
|
-
raise NotImplementedError
|
|
130
|
+
raise NotImplementedError
|
|
131
131
|
|
|
132
132
|
async def stats_servers(self, t: str) -> None:
|
|
133
133
|
# TODO: https://api.status.tw/stats/servers/week | month
|
|
134
|
-
raise NotImplementedError
|
|
134
|
+
raise NotImplementedError
|
|
@@ -1,26 +1,29 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from abc import ABC, abstractmethod
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Generic
|
|
4
4
|
|
|
5
|
+
from ddapi.types import T
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
|
|
8
|
+
class CacheABC(ABC, Generic[T]):
|
|
9
|
+
def __init__(self) -> None:
|
|
8
10
|
self._cache = {}
|
|
9
11
|
self._lock = asyncio.Lock()
|
|
12
|
+
self._remove_tasks: dict[str, asyncio.Task] = {}
|
|
10
13
|
|
|
11
14
|
@abstractmethod
|
|
12
|
-
async def get(self, key: str, default:
|
|
15
|
+
async def get(self, key: str, default: T = None) -> T:
|
|
13
16
|
return default
|
|
14
17
|
|
|
15
18
|
@abstractmethod
|
|
16
|
-
async def set(self, key: str, value:
|
|
19
|
+
async def set(self, key: str, value: T, delay: int | None = None) -> bool:
|
|
17
20
|
return True
|
|
18
21
|
|
|
19
22
|
@abstractmethod
|
|
20
23
|
def delete(self, key: str) -> bool:
|
|
21
24
|
return True
|
|
22
25
|
|
|
23
|
-
async def get_or_set(self, key: str, value:
|
|
26
|
+
async def get_or_set(self, key: str, value: T = None) -> T:
|
|
24
27
|
result = await self.get(key)
|
|
25
28
|
if result is not None:
|
|
26
29
|
return result
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .base import CacheABC
|
|
2
|
+
from ddapi.types import T
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class EmptyCache(CacheABC):
|
|
6
|
+
async def get(self, key: str, default: T = None) -> T: # noqa: ARG002
|
|
7
|
+
return default
|
|
8
|
+
|
|
9
|
+
async def set(self, key: str, value: T, delay: int | None = None) -> bool: # noqa: ARG002
|
|
10
|
+
return True
|
|
11
|
+
|
|
12
|
+
def delete(self, key: str) -> bool: # noqa: ARG002
|
|
13
|
+
return True
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from .base import CacheABC
|
|
4
|
+
from ddapi.types import T
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MemoryCache(CacheABC):
|
|
8
|
+
async def get(self, key: str, default: T = None) -> T:
|
|
9
|
+
async with self._lock:
|
|
10
|
+
return self._cache.get(key, default)
|
|
11
|
+
|
|
12
|
+
async def set(self, key: str, value: T, delay: int | None = None) -> bool:
|
|
13
|
+
async with self._lock:
|
|
14
|
+
self._cache[key] = value
|
|
15
|
+
if delay is not None:
|
|
16
|
+
if existing_task := self._remove_tasks.get(key):
|
|
17
|
+
existing_task.cancel()
|
|
18
|
+
|
|
19
|
+
self._remove_tasks[key] = asyncio.create_task(
|
|
20
|
+
self._remove_after_sleep(key, delay)
|
|
21
|
+
)
|
|
22
|
+
return True
|
|
23
|
+
|
|
24
|
+
async def delete(self, key: str) -> bool:
|
|
25
|
+
async with self._lock:
|
|
26
|
+
if key in self._cache:
|
|
27
|
+
self._cache.pop(key, None)
|
|
28
|
+
await self._remove_tasks.pop(key, None)
|
|
29
|
+
if existing_task := self._remove_tasks.get(key):
|
|
30
|
+
existing_task.cancel()
|
|
31
|
+
return True
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
async def _remove_after_sleep(self, key: str, delay: int) -> None:
|
|
35
|
+
try:
|
|
36
|
+
await asyncio.sleep(delay)
|
|
37
|
+
self._cache.pop(key, None)
|
|
38
|
+
await self._remove_tasks.pop(key, None)
|
|
39
|
+
except asyncio.CancelledError:
|
|
40
|
+
pass
|
|
@@ -1,45 +1,47 @@
|
|
|
1
1
|
# ddnet
|
|
2
2
|
from collections import Counter
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from
|
|
4
|
+
from urllib.parse import quote
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
8
|
+
from ddapi.enum import MasterEnum
|
|
9
|
+
|
|
8
10
|
rm_list = ["DD-Persian", "/vDQMHSss8W", ""]
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
class DRank(BaseModel):
|
|
12
|
-
points:
|
|
13
|
-
rank:
|
|
14
|
+
points: int | None = None
|
|
15
|
+
rank: int | None = None
|
|
14
16
|
|
|
15
17
|
|
|
16
18
|
class DPoints(BaseModel):
|
|
17
19
|
total: int
|
|
18
|
-
points:
|
|
19
|
-
rank:
|
|
20
|
+
points: int | None = Field(default=None)
|
|
21
|
+
rank: int | None = Field(default=None)
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class DData(BaseModel):
|
|
23
|
-
points:
|
|
24
|
-
team_rank:
|
|
25
|
-
rank:
|
|
26
|
-
maps:
|
|
25
|
+
points: DPoints | None = Field(default=None)
|
|
26
|
+
team_rank: DRank | None = Field(default=None)
|
|
27
|
+
rank: DRank | None = Field(default=None)
|
|
28
|
+
maps: dict | None = Field(default=None)
|
|
27
29
|
|
|
28
30
|
|
|
29
31
|
class DDType(BaseModel):
|
|
30
|
-
Novice:
|
|
31
|
-
Moderate:
|
|
32
|
-
Brutal:
|
|
33
|
-
Insane:
|
|
34
|
-
Dummy:
|
|
35
|
-
DDmaX_Easy:
|
|
36
|
-
DDmaX_Next:
|
|
37
|
-
DDmaX_Nut:
|
|
38
|
-
DDmaX_Pro:
|
|
39
|
-
Oldschool:
|
|
40
|
-
Race:
|
|
41
|
-
Solo:
|
|
42
|
-
total:
|
|
32
|
+
Novice: DData | None = None
|
|
33
|
+
Moderate: DData | None = None
|
|
34
|
+
Brutal: DData | None = None
|
|
35
|
+
Insane: DData | None = None
|
|
36
|
+
Dummy: DData | None = None
|
|
37
|
+
DDmaX_Easy: DData | None = Field(default=None, validation_alias="DDmaX.Easy")
|
|
38
|
+
DDmaX_Next: DData | None = Field(default=None, validation_alias="DDmaX.Next")
|
|
39
|
+
DDmaX_Nut: DData | None = Field(default=None, validation_alias="DDmaX.Nut")
|
|
40
|
+
DDmaX_Pro: DData | None = Field(default=None, validation_alias="DDmaX.Pro")
|
|
41
|
+
Oldschool: DData | None = Field(default=None)
|
|
42
|
+
Race: DData | None = None
|
|
43
|
+
Solo: DData | None = None
|
|
44
|
+
total: DData | None = None
|
|
43
45
|
|
|
44
46
|
|
|
45
47
|
class MaxFinishes(BaseModel):
|
|
@@ -76,7 +78,7 @@ class DMap(BaseModel):
|
|
|
76
78
|
points: int
|
|
77
79
|
difficulty: int
|
|
78
80
|
mapper: str
|
|
79
|
-
release:
|
|
81
|
+
release: int | None = None
|
|
80
82
|
median_time: float
|
|
81
83
|
first_finish: int
|
|
82
84
|
last_finish: int
|
|
@@ -90,6 +92,17 @@ class DMap(BaseModel):
|
|
|
90
92
|
ranks: list[DDRanks]
|
|
91
93
|
max_finishes: list[MaxFinishes]
|
|
92
94
|
|
|
95
|
+
def url(self) -> str:
|
|
96
|
+
return f"https://ddnet.org/maps/{self.name}"
|
|
97
|
+
|
|
98
|
+
@staticmethod
|
|
99
|
+
def url_with_name(map_name: str) -> str:
|
|
100
|
+
return f"https://ddnet.org/maps/{map_name}"
|
|
101
|
+
|
|
102
|
+
@staticmethod
|
|
103
|
+
def api(map_name: str) -> str:
|
|
104
|
+
return f"https://ddnet.org/maps/?json={quote(map_name)}"
|
|
105
|
+
|
|
93
106
|
|
|
94
107
|
class FirstFinish(BaseModel):
|
|
95
108
|
timestamp: float
|
|
@@ -102,7 +115,7 @@ class LastFinish(BaseModel):
|
|
|
102
115
|
map: str
|
|
103
116
|
time: float
|
|
104
117
|
country: str
|
|
105
|
-
type:
|
|
118
|
+
type: str | None = None
|
|
106
119
|
|
|
107
120
|
|
|
108
121
|
class FavoritePartner(BaseModel):
|
|
@@ -122,24 +135,35 @@ class FavoriteServer(BaseModel):
|
|
|
122
135
|
class DDPlayer(BaseModel):
|
|
123
136
|
player: str
|
|
124
137
|
points: DPoints
|
|
125
|
-
team_rank:
|
|
126
|
-
rank:
|
|
127
|
-
points_last_year:
|
|
128
|
-
points_last_month:
|
|
129
|
-
points_last_week:
|
|
138
|
+
team_rank: DRank | None = None
|
|
139
|
+
rank: DRank | None = None
|
|
140
|
+
points_last_year: DRank | None = None
|
|
141
|
+
points_last_month: DRank | None = None
|
|
142
|
+
points_last_week: DRank | None = None
|
|
130
143
|
favorite_server: FavoriteServer
|
|
131
144
|
first_finish: FirstFinish
|
|
132
145
|
last_finishes: list[LastFinish]
|
|
133
|
-
favorite_partners:
|
|
146
|
+
favorite_partners: list[FavoritePartner] | None = None
|
|
134
147
|
types: DDType
|
|
135
148
|
activity: list[Activity]
|
|
136
149
|
hours_played_past_365_days: int
|
|
137
150
|
|
|
151
|
+
def url(self) -> str:
|
|
152
|
+
return f"https://ddnet.org/players/{self.player}"
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def url_with_name(player: str) -> str:
|
|
156
|
+
return f"https://ddnet.org/players/{player}"
|
|
157
|
+
|
|
158
|
+
@staticmethod
|
|
159
|
+
def api(player: str) -> str:
|
|
160
|
+
return f"https://ddnet.org/players/?json2={quote(player)}"
|
|
161
|
+
|
|
138
162
|
|
|
139
163
|
class Skin(BaseModel):
|
|
140
|
-
name:
|
|
141
|
-
color_body:
|
|
142
|
-
color_feet:
|
|
164
|
+
name: str | None = None
|
|
165
|
+
color_body: int | None = None
|
|
166
|
+
color_feet: int | None = None
|
|
143
167
|
|
|
144
168
|
|
|
145
169
|
class Client(BaseModel):
|
|
@@ -147,24 +171,24 @@ class Client(BaseModel):
|
|
|
147
171
|
clan: str
|
|
148
172
|
country: int
|
|
149
173
|
score: int
|
|
150
|
-
is_player:
|
|
151
|
-
skin:
|
|
152
|
-
afk:
|
|
153
|
-
team:
|
|
174
|
+
is_player: bool | None = None
|
|
175
|
+
skin: Skin | None = None
|
|
176
|
+
afk: bool | None = None
|
|
177
|
+
team: int | None = None
|
|
154
178
|
|
|
155
179
|
|
|
156
180
|
class Map(BaseModel):
|
|
157
181
|
name: str
|
|
158
|
-
sha256:
|
|
159
|
-
size:
|
|
182
|
+
sha256: str | None = None
|
|
183
|
+
size: int | None = None
|
|
160
184
|
|
|
161
185
|
|
|
162
186
|
class Community(BaseModel):
|
|
163
187
|
id: str
|
|
164
188
|
icon: str
|
|
165
189
|
admin: list[str]
|
|
166
|
-
public_key:
|
|
167
|
-
signature:
|
|
190
|
+
public_key: str | None = None
|
|
191
|
+
signature: str | None = None
|
|
168
192
|
|
|
169
193
|
|
|
170
194
|
class Info(BaseModel):
|
|
@@ -175,9 +199,9 @@ class Info(BaseModel):
|
|
|
175
199
|
name: str = None
|
|
176
200
|
map: Map = None
|
|
177
201
|
version: str = None
|
|
178
|
-
clients:
|
|
179
|
-
requires_login:
|
|
180
|
-
community:
|
|
202
|
+
clients: list[Client] | None = None
|
|
203
|
+
requires_login: bool | None = None
|
|
204
|
+
community: Community | None = None
|
|
181
205
|
|
|
182
206
|
def __len__(self) -> int:
|
|
183
207
|
return len(self.clients)
|
|
@@ -211,12 +235,14 @@ class Master(BaseModel):
|
|
|
211
235
|
def __len__(self) -> int:
|
|
212
236
|
return len(self.servers)
|
|
213
237
|
|
|
214
|
-
def get_clans(
|
|
238
|
+
def get_clans(
|
|
239
|
+
self, rm: list[str] | None = None
|
|
240
|
+
) -> list[str] | list[tuple[str, int]]:
|
|
215
241
|
remove_list = rm_list.copy() if rm is None else rm
|
|
216
242
|
if not self.servers:
|
|
217
243
|
return []
|
|
218
244
|
|
|
219
|
-
dat: Counter[
|
|
245
|
+
dat: Counter[str] = Counter(
|
|
220
246
|
client.clan
|
|
221
247
|
for server in self.servers
|
|
222
248
|
for client in server.info.clients
|
|
@@ -254,6 +280,10 @@ class Master(BaseModel):
|
|
|
254
280
|
def count_clients(self) -> int:
|
|
255
281
|
return sum(i.count_client for i in self.servers)
|
|
256
282
|
|
|
283
|
+
@staticmethod
|
|
284
|
+
def api(server: MasterEnum) -> str:
|
|
285
|
+
return f"https://master{server}.ddnet.org/ddnet/15/servers.json"
|
|
286
|
+
|
|
257
287
|
|
|
258
288
|
class ReleasesMapsData(BaseModel):
|
|
259
289
|
name: str
|
|
@@ -265,14 +295,22 @@ class ReleasesMapsData(BaseModel):
|
|
|
265
295
|
difficulty: int
|
|
266
296
|
mapper: str
|
|
267
297
|
release: datetime | str
|
|
268
|
-
width: int
|
|
269
|
-
height: int
|
|
270
|
-
tiles: list[str]
|
|
298
|
+
width: int = None
|
|
299
|
+
height: int = None
|
|
300
|
+
tiles: list[str] = None
|
|
271
301
|
|
|
272
302
|
|
|
273
303
|
class ReleasesMaps(BaseModel):
|
|
274
304
|
maps: list[ReleasesMapsData]
|
|
275
305
|
|
|
306
|
+
@staticmethod
|
|
307
|
+
def url() -> str:
|
|
308
|
+
return "https://ddnet.org/releases"
|
|
309
|
+
|
|
310
|
+
@staticmethod
|
|
311
|
+
def api() -> str:
|
|
312
|
+
return "https://ddnet.org/releases/maps.json"
|
|
313
|
+
|
|
276
314
|
|
|
277
315
|
class QueryData(BaseModel):
|
|
278
316
|
points: int
|
|
@@ -282,6 +320,10 @@ class QueryData(BaseModel):
|
|
|
282
320
|
class Query(BaseModel):
|
|
283
321
|
players: list[QueryData]
|
|
284
322
|
|
|
323
|
+
@staticmethod
|
|
324
|
+
def api(player: str) -> str:
|
|
325
|
+
return f"https://ddnet.org/players/?query={quote(player)}"
|
|
326
|
+
|
|
285
327
|
|
|
286
328
|
class QueryMapperData(BaseModel):
|
|
287
329
|
mapper: str
|
|
@@ -291,6 +333,10 @@ class QueryMapperData(BaseModel):
|
|
|
291
333
|
class QueryMapper(BaseModel):
|
|
292
334
|
players: list[QueryMapperData]
|
|
293
335
|
|
|
336
|
+
@staticmethod
|
|
337
|
+
def api(player: str) -> str:
|
|
338
|
+
return f"https://ddnet.org/maps/?qmapper={quote(player)}"
|
|
339
|
+
|
|
294
340
|
|
|
295
341
|
class QueryMapData(BaseModel):
|
|
296
342
|
name: str
|
|
@@ -301,6 +347,10 @@ class QueryMapData(BaseModel):
|
|
|
301
347
|
class QueryMap(BaseModel):
|
|
302
348
|
maps: list[QueryMapData]
|
|
303
349
|
|
|
350
|
+
@staticmethod
|
|
351
|
+
def api(map_name: str) -> str:
|
|
352
|
+
return f"https://ddnet.org/maps/?query={quote(map_name)}"
|
|
353
|
+
|
|
304
354
|
|
|
305
355
|
class DDStatusData(BaseModel):
|
|
306
356
|
name: str = None
|
|
@@ -326,3 +376,11 @@ class DDStatusData(BaseModel):
|
|
|
326
376
|
|
|
327
377
|
class DDStatus(BaseModel):
|
|
328
378
|
servers: list[DDStatusData]
|
|
379
|
+
|
|
380
|
+
@staticmethod
|
|
381
|
+
def url() -> str:
|
|
382
|
+
return "https://ddnet.org/status"
|
|
383
|
+
|
|
384
|
+
@staticmethod
|
|
385
|
+
def api() -> str:
|
|
386
|
+
return "https://ddnet.org/status/json/stats.json"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# ddstats.tw
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from typing import Optional
|
|
4
|
+
from urllib.parse import quote
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
@@ -11,19 +12,19 @@ class PData(BaseModel):
|
|
|
11
12
|
|
|
12
13
|
|
|
13
14
|
class PPoints(BaseModel):
|
|
14
|
-
Novice:
|
|
15
|
-
Moderate:
|
|
16
|
-
Brutal:
|
|
17
|
-
Insane:
|
|
18
|
-
Dummy:
|
|
19
|
-
DDmaX_Easy:
|
|
20
|
-
DDmaX_Next:
|
|
21
|
-
DDmaX_Nut:
|
|
22
|
-
DDmaX_Pro:
|
|
23
|
-
Oldschool:
|
|
24
|
-
Race:
|
|
25
|
-
Solo:
|
|
26
|
-
total:
|
|
15
|
+
Novice: PData | None = None
|
|
16
|
+
Moderate: PData | None = None
|
|
17
|
+
Brutal: PData | None = None
|
|
18
|
+
Insane: PData | None = None
|
|
19
|
+
Dummy: PData | None = None
|
|
20
|
+
DDmaX_Easy: PData | None = Field(default=None, validation_alias="DDmaX.Easy")
|
|
21
|
+
DDmaX_Next: PData | None = Field(default=None, validation_alias="DDmaX.Next")
|
|
22
|
+
DDmaX_Nut: PData | None = Field(default=None, validation_alias="DDmaX.Nut")
|
|
23
|
+
DDmaX_Pro: PData | None = Field(default=None, validation_alias="DDmaX.Pro")
|
|
24
|
+
Oldschool: PData | None = None
|
|
25
|
+
Race: PData | None = None
|
|
26
|
+
Solo: PData | None = None
|
|
27
|
+
total: PData | None = None
|
|
27
28
|
|
|
28
29
|
|
|
29
30
|
class PointG(BaseModel):
|
|
@@ -36,16 +37,27 @@ class PointG(BaseModel):
|
|
|
36
37
|
class Profile(BaseModel):
|
|
37
38
|
name: str
|
|
38
39
|
points: int
|
|
39
|
-
clan:
|
|
40
|
-
country:
|
|
41
|
-
skin_name:
|
|
42
|
-
skin_color_body:
|
|
43
|
-
skin_color_feet:
|
|
40
|
+
clan: str | None
|
|
41
|
+
country: int | None
|
|
42
|
+
skin_name: str | None
|
|
43
|
+
skin_color_body: int | None = None
|
|
44
|
+
skin_color_feet: int | None = None
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
class DProfile(BaseModel):
|
|
47
48
|
profile: Profile
|
|
48
49
|
|
|
50
|
+
def url(self) -> str:
|
|
51
|
+
return f"https://ddstats.tw/player/{quote(self.name)}"
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def url_with_name(player: str) -> str:
|
|
55
|
+
return f"https://ddstats.tw/player/{quote(player)}"
|
|
56
|
+
|
|
57
|
+
@staticmethod
|
|
58
|
+
def api(player: str) -> str:
|
|
59
|
+
return f"https://ddstats.tw/profile/json?player={quote(player)}"
|
|
60
|
+
|
|
49
61
|
|
|
50
62
|
class DDMap(BaseModel):
|
|
51
63
|
map: str
|
|
@@ -53,7 +65,7 @@ class DDMap(BaseModel):
|
|
|
53
65
|
points: int
|
|
54
66
|
stars: int
|
|
55
67
|
mapper: str
|
|
56
|
-
timestamp:
|
|
68
|
+
timestamp: datetime | None = None
|
|
57
69
|
|
|
58
70
|
|
|
59
71
|
class RecentFinishes(BaseModel):
|
|
@@ -97,24 +109,24 @@ class Finishes(BaseModel):
|
|
|
97
109
|
map: DDMap
|
|
98
110
|
name: str
|
|
99
111
|
time: float
|
|
100
|
-
timestamp:
|
|
112
|
+
timestamp: datetime | None
|
|
101
113
|
server: str
|
|
102
114
|
rank: int
|
|
103
|
-
team_rank:
|
|
104
|
-
seconds_played:
|
|
115
|
+
team_rank: int | None = None
|
|
116
|
+
seconds_played: int | None = None
|
|
105
117
|
|
|
106
118
|
|
|
107
119
|
class UnfinishedMap(BaseModel):
|
|
108
120
|
map: DDMap
|
|
109
121
|
finishes: int
|
|
110
|
-
finishes_rank:
|
|
111
|
-
median_time:
|
|
122
|
+
finishes_rank: int | None = None
|
|
123
|
+
median_time: float | None = None
|
|
112
124
|
|
|
113
125
|
|
|
114
126
|
class Points(BaseModel):
|
|
115
|
-
weekly_points:
|
|
116
|
-
monthly_points:
|
|
117
|
-
yearly_points:
|
|
127
|
+
weekly_points: PData | None = None
|
|
128
|
+
monthly_points: PData | None = None
|
|
129
|
+
yearly_points: PData | None = None
|
|
118
130
|
points: PPoints
|
|
119
131
|
rank_points: PPoints
|
|
120
132
|
team_points: PPoints
|
|
@@ -132,9 +144,9 @@ class RecentPlayerInfo(BaseModel):
|
|
|
132
144
|
name: str
|
|
133
145
|
clan: str
|
|
134
146
|
country: int
|
|
135
|
-
skin_name:
|
|
136
|
-
skin_color_body:
|
|
137
|
-
skin_color_feet:
|
|
147
|
+
skin_name: str | None = None
|
|
148
|
+
skin_color_body: int | None = None
|
|
149
|
+
skin_color_feet: int | None = None
|
|
138
150
|
last_seen: str
|
|
139
151
|
seconds_played: int
|
|
140
152
|
|
|
@@ -142,7 +154,7 @@ class RecentPlayerInfo(BaseModel):
|
|
|
142
154
|
class MostPlayedMaps(BaseModel):
|
|
143
155
|
map_name: str
|
|
144
156
|
seconds_played: int
|
|
145
|
-
map:
|
|
157
|
+
map: DDMap | None = None
|
|
146
158
|
|
|
147
159
|
|
|
148
160
|
class MostPlayed(BaseModel):
|
|
@@ -175,7 +187,7 @@ class RecentTop10s(BaseModel):
|
|
|
175
187
|
map: str
|
|
176
188
|
time: float
|
|
177
189
|
rank: int
|
|
178
|
-
timestamp:
|
|
190
|
+
timestamp: datetime | None = None
|
|
179
191
|
server: str
|
|
180
192
|
|
|
181
193
|
|
|
@@ -188,28 +200,43 @@ class GeneralActivity(BaseModel):
|
|
|
188
200
|
class Player(BaseModel):
|
|
189
201
|
points_graph: list[PointG]
|
|
190
202
|
recent_finishes: list[RecentFinishes]
|
|
191
|
-
favourite_teammates:
|
|
203
|
+
favourite_teammates: list[FavouriteTeammates] | None = None
|
|
192
204
|
profile: Profile
|
|
193
205
|
is_mapper: bool
|
|
194
206
|
finishes: list[Finishes]
|
|
195
|
-
unfinished_maps:
|
|
207
|
+
unfinished_maps: list[UnfinishedMap] | None = None
|
|
196
208
|
points: Points
|
|
197
|
-
recent_activity:
|
|
198
|
-
recent_player_info:
|
|
199
|
-
most_played_maps:
|
|
200
|
-
most_played_gametypes:
|
|
201
|
-
most_played_categories:
|
|
202
|
-
most_played_locations:
|
|
209
|
+
recent_activity: list[RecentActivity] | None = None
|
|
210
|
+
recent_player_info: list[RecentPlayerInfo] | None = None
|
|
211
|
+
most_played_maps: list[MostPlayedMaps] | None = None
|
|
212
|
+
most_played_gametypes: list[MostPlayed] | None = None
|
|
213
|
+
most_played_categories: list[MostPlayed] | None = None
|
|
214
|
+
most_played_locations: list[MostPlayed] | None = None
|
|
203
215
|
playtime_per_month: list[PlaytimePerMonth]
|
|
204
|
-
general_activity:
|
|
205
|
-
favourite_rank1s_teammates:
|
|
206
|
-
all_top_10s:
|
|
207
|
-
recent_top_10s:
|
|
216
|
+
general_activity: GeneralActivity | None = None
|
|
217
|
+
favourite_rank1s_teammates: list[FavouriteRank1sTeammates] | None = None
|
|
218
|
+
all_top_10s: list[AllTop10s] | None = None
|
|
219
|
+
recent_top_10s: list[RecentTop10s] | None = None
|
|
220
|
+
|
|
221
|
+
def url(self) -> str:
|
|
222
|
+
return f"https://ddstats.tw/player/{quote(self.profile.name)}"
|
|
223
|
+
|
|
224
|
+
@staticmethod
|
|
225
|
+
def url_with_name(player: str) -> str:
|
|
226
|
+
return f"https://ddstats.tw/player/{quote(player)}"
|
|
227
|
+
|
|
228
|
+
@staticmethod
|
|
229
|
+
def api(player: str) -> str:
|
|
230
|
+
return f"https://ddstats.tw/player/json?player={quote(player)}"
|
|
208
231
|
|
|
209
232
|
|
|
210
233
|
class Maps(BaseModel):
|
|
211
234
|
maps: list[DDMap]
|
|
212
235
|
|
|
236
|
+
@staticmethod
|
|
237
|
+
def api() -> str:
|
|
238
|
+
return "https://ddstats.tw/maps/json"
|
|
239
|
+
|
|
213
240
|
|
|
214
241
|
class InfoSMap(BaseModel):
|
|
215
242
|
map: DDMap
|
|
@@ -220,7 +247,7 @@ class InfoSMap(BaseModel):
|
|
|
220
247
|
|
|
221
248
|
class RankingSMap(BaseModel):
|
|
222
249
|
rank: int
|
|
223
|
-
timestamp:
|
|
250
|
+
timestamp: datetime | None = None
|
|
224
251
|
name: str
|
|
225
252
|
time: float
|
|
226
253
|
map: str
|
|
@@ -229,7 +256,7 @@ class RankingSMap(BaseModel):
|
|
|
229
256
|
|
|
230
257
|
class TeamRankingSMap(BaseModel):
|
|
231
258
|
rank: int
|
|
232
|
-
timestamp:
|
|
259
|
+
timestamp: datetime | None = None
|
|
233
260
|
id: list[int]
|
|
234
261
|
players: list[str]
|
|
235
262
|
time: float
|
|
@@ -278,3 +305,14 @@ class SMap(BaseModel):
|
|
|
278
305
|
team_rankings: list[TeamRankingSMap]
|
|
279
306
|
time_cps: list[TimeCpsSMap]
|
|
280
307
|
playtime: list[PlaytimeSMap]
|
|
308
|
+
|
|
309
|
+
def url(self) -> str:
|
|
310
|
+
return f"https://ddstats.tw/map/{quote(self.info.map.map)}"
|
|
311
|
+
|
|
312
|
+
@staticmethod
|
|
313
|
+
def url_with_name(map_name: str) -> str:
|
|
314
|
+
return f"https://ddstats.tw/map/{quote(map_name)}"
|
|
315
|
+
|
|
316
|
+
@staticmethod
|
|
317
|
+
def api(map_name: str) -> str:
|
|
318
|
+
return f"https://ddstats.tw/map/json?map={quote(map_name)}"
|
|
@@ -56,9 +56,9 @@ class ServerTw(BaseModel):
|
|
|
56
56
|
client_score_kind: str | None = Field(alias="clientScoreKind")
|
|
57
57
|
logo_url: str = Field(alias="logoUrl")
|
|
58
58
|
map: DataTw
|
|
59
|
-
website:
|
|
60
|
-
discord_invite:
|
|
61
|
-
description:
|
|
59
|
+
website: str | None = None
|
|
60
|
+
discord_invite: str | None = Field(None, alias="discordInvite")
|
|
61
|
+
description: str | None = None
|
|
62
62
|
game_type: DataTw = Field(alias="gameType")
|
|
63
63
|
version: VersionTw
|
|
64
64
|
clients: list[ClientTw]
|
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
1
3
|
import setuptools
|
|
2
4
|
|
|
3
|
-
with open("README.md",
|
|
5
|
+
with Path.open(Path("README.md"), encoding="utf-8") as fh:
|
|
4
6
|
long_description = fh.read()
|
|
5
7
|
|
|
6
8
|
setuptools.setup(
|
|
7
9
|
name="ddapi",
|
|
8
|
-
version="0.
|
|
10
|
+
version="0.14.0",
|
|
9
11
|
author="ByFox",
|
|
10
12
|
description="DDnet api",
|
|
11
13
|
long_description=long_description,
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
from urllib.parse import quote
|
|
2
|
-
|
|
3
|
-
from .._api import API
|
|
4
|
-
from ..scheme import (
|
|
5
|
-
DDPlayer,
|
|
6
|
-
DDStatus,
|
|
7
|
-
ReleasesMaps,
|
|
8
|
-
Query,
|
|
9
|
-
QueryMap,
|
|
10
|
-
QueryMapper,
|
|
11
|
-
Master,
|
|
12
|
-
DMap,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
__all__ = ("DDnetApi",)
|
|
16
|
-
|
|
17
|
-
from ..enum import MasterEnum
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class DDnetApi(API):
|
|
21
|
-
@property
|
|
22
|
-
def domain(self) -> str:
|
|
23
|
-
return "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)}", DDPlayer
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
async def status(self) -> DDStatus:
|
|
36
|
-
return await self._generate_model_instance(
|
|
37
|
-
f"https://{self.domain}/status/json/stats.json", DDStatus
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
async def releases_map(self) -> ReleasesMaps:
|
|
41
|
-
return await self._generate_model_instance(
|
|
42
|
-
f"https://{self.domain}/releases/maps.json", ReleasesMaps, "maps"
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
async def query(self, player: str) -> Query:
|
|
46
|
-
return await self._generate_model_instance(
|
|
47
|
-
f"https://{self.domain}/players/?query={quote(player)}", Query, "players"
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
async def query_map(self, map_name: str) -> QueryMap:
|
|
51
|
-
return await self._generate_model_instance(
|
|
52
|
-
f"https://{self.domain}/maps/?query={quote(map_name)}",
|
|
53
|
-
QueryMap,
|
|
54
|
-
"maps",
|
|
55
|
-
)
|
|
56
|
-
|
|
57
|
-
async def query_mapper(self, player: str) -> QueryMapper:
|
|
58
|
-
return await self._generate_model_instance(
|
|
59
|
-
f"https://{self.domain}/maps/?qmapper={quote(player)}",
|
|
60
|
-
QueryMapper,
|
|
61
|
-
"players",
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
async def master(self, number: MasterEnum = MasterEnum.master1) -> Master:
|
|
65
|
-
return await self._generate_model_instance(
|
|
66
|
-
f"https://master{number}.{self.domain}/ddnet/15/servers.json", Master
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
async def map(self, map_name: str) -> DMap:
|
|
70
|
-
return await self._generate_model_instance(
|
|
71
|
-
f"https://{self.domain}/maps/?json={quote(map_name)}", DMap
|
|
72
|
-
)
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
from urllib.parse import quote
|
|
2
|
-
|
|
3
|
-
from .._api import API
|
|
4
|
-
from ..scheme import (
|
|
5
|
-
DProfile,
|
|
6
|
-
SMap,
|
|
7
|
-
Maps,
|
|
8
|
-
Player,
|
|
9
|
-
)
|
|
10
|
-
|
|
11
|
-
__all__ = ("DDstats",)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class DDstats(API):
|
|
15
|
-
@property
|
|
16
|
-
def domain(self) -> str:
|
|
17
|
-
return "ddstats.tw"
|
|
18
|
-
|
|
19
|
-
@staticmethod
|
|
20
|
-
def powered() -> str:
|
|
21
|
-
return DDstats().domain
|
|
22
|
-
|
|
23
|
-
async def profile(self, player_name: str) -> DProfile:
|
|
24
|
-
return await self._generate_model_instance(
|
|
25
|
-
f"https://{self.domain}/profile/json?player={quote(player_name)}",
|
|
26
|
-
DProfile,
|
|
27
|
-
"profile",
|
|
28
|
-
)
|
|
29
|
-
|
|
30
|
-
async def map(self, map_name: str) -> SMap:
|
|
31
|
-
return await self._generate_model_instance(
|
|
32
|
-
f"https://{self.domain}/map/json?map={quote(map_name)}", SMap
|
|
33
|
-
)
|
|
34
|
-
|
|
35
|
-
async def maps(self) -> Maps:
|
|
36
|
-
return await self._generate_model_instance(
|
|
37
|
-
f"https://{self.domain}/maps/json", Maps, "maps"
|
|
38
|
-
)
|
|
39
|
-
|
|
40
|
-
async def player(self, player_name: str) -> Player:
|
|
41
|
-
"""Fetch player data from the API and return it as a dictionary."""
|
|
42
|
-
return await self._generate_model_instance(
|
|
43
|
-
f"https://{self.domain}/player/json?player={quote(player_name)}", Player
|
|
44
|
-
)
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from ddapi.cache import CacheABC
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
class EmptyCache(CacheABC):
|
|
7
|
-
async def get(self, key: str, default: Any = None) -> Any:
|
|
8
|
-
return default
|
|
9
|
-
|
|
10
|
-
async def set(self, key: str, value: Any, timeout_seconds: int = None) -> bool:
|
|
11
|
-
return True
|
|
12
|
-
|
|
13
|
-
def delete(self, key: str) -> bool:
|
|
14
|
-
return True
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import asyncio
|
|
2
|
-
from typing import Any
|
|
3
|
-
|
|
4
|
-
from .base import CacheABC
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class MemoryCache(CacheABC):
|
|
8
|
-
async def get(self, key: str, default: Any = None) -> Any:
|
|
9
|
-
async with self._lock:
|
|
10
|
-
return self._cache.get(key, default)
|
|
11
|
-
|
|
12
|
-
async def set(self, key: str, value: Any, timeout_seconds: int = None) -> bool:
|
|
13
|
-
async with self._lock:
|
|
14
|
-
if timeout_seconds is None:
|
|
15
|
-
self._cache[key] = value
|
|
16
|
-
return True
|
|
17
|
-
asyncio.create_task(self._remove_after_timeout(key, timeout_seconds))
|
|
18
|
-
return True
|
|
19
|
-
|
|
20
|
-
async def delete(self, key: str) -> bool:
|
|
21
|
-
async with self._lock:
|
|
22
|
-
if key in self._cache:
|
|
23
|
-
del self._cache[key]
|
|
24
|
-
return True
|
|
25
|
-
return False
|
|
26
|
-
|
|
27
|
-
async def _remove_after_timeout(self, key: str, timeout: int):
|
|
28
|
-
await asyncio.sleep(timeout)
|
|
29
|
-
await self.delete(key)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|