ddapi 0.13.1__tar.gz → 0.14.1__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.1 → ddapi-0.14.1}/PKG-INFO +1 -1
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/__init__.py +2 -1
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/_api.py +20 -23
- ddapi-0.14.1/ddapi/ddapi/api/ddnet.py +56 -0
- ddapi-0.14.1/ddapi/ddapi/api/ddstats.py +38 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/api/status.py +7 -7
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/cache/base.py +9 -6
- ddapi-0.14.1/ddapi/ddapi/cache/empty.py +13 -0
- ddapi-0.14.1/ddapi/ddapi/cache/memory.py +40 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/ddnet.py +105 -46
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/ddstats_tw.py +84 -46
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/status_tw.py +3 -3
- ddapi-0.14.1/ddapi/ddapi/util.py +18 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/PKG-INFO +1 -1
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/SOURCES.txt +1 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/setup.cfg +1 -1
- {ddapi-0.13.1 → ddapi-0.14.1}/setup.py +4 -2
- ddapi-0.13.1/ddapi/ddapi/api/ddnet.py +0 -72
- ddapi-0.13.1/ddapi/ddapi/api/ddstats.py +0 -44
- ddapi-0.13.1/ddapi/ddapi/cache/empty.py +0 -14
- ddapi-0.13.1/ddapi/ddapi/cache/memory.py +0 -28
- {ddapi-0.13.1 → ddapi-0.14.1}/LICENSE +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/README.md +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/api/__init__.py +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/cache/__init__.py +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/enum.py +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/__init__.py +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/types.py +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/dependency_links.txt +0 -0
- {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/top_level.txt +0 -0
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
from .api import DDnetApi, DDstats, Status
|
|
4
4
|
from .enum import MasterEnum
|
|
5
5
|
from .cache import CacheABC, EmptyCache, MemoryCache
|
|
6
|
+
from .util import slugify2
|
|
6
7
|
from .scheme import * # noqa: F403
|
|
7
8
|
|
|
8
|
-
__version__ = "0.
|
|
9
|
+
__version__ = "0.14.1"
|
|
9
10
|
__author__ = "ByFox"
|
|
10
11
|
__LICENSE__ = "MIT"
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import atexit
|
|
3
3
|
import logging
|
|
4
|
-
from
|
|
5
|
-
from
|
|
4
|
+
from http import HTTPStatus
|
|
5
|
+
from json import JSONDecoder
|
|
6
|
+
from typing import Optional
|
|
6
7
|
|
|
7
8
|
import aiohttp
|
|
8
9
|
from aiohttp import ClientSession
|
|
@@ -20,54 +21,51 @@ except ImportError:
|
|
|
20
21
|
logger = logging.getLogger(__name__)
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
class API
|
|
24
|
+
class API:
|
|
24
25
|
def __init__(
|
|
25
26
|
self,
|
|
26
27
|
session: ClientSession = None,
|
|
27
28
|
*,
|
|
28
29
|
autoclose: bool = True,
|
|
29
|
-
cache: CacheABC =
|
|
30
|
-
json_loads:
|
|
31
|
-
):
|
|
30
|
+
cache: CacheABC = None,
|
|
31
|
+
json_loads: JSONDecoder = DEFAULT_JSON_DECODER,
|
|
32
|
+
) -> None:
|
|
32
33
|
self.__session = session
|
|
33
|
-
self.cache = cache
|
|
34
|
+
self.cache = cache or EmptyCache()
|
|
34
35
|
self.json_loads = json_loads
|
|
35
36
|
self.autoclose = autoclose
|
|
36
37
|
|
|
37
38
|
if self.__session is None:
|
|
38
39
|
self.__session = aiohttp.ClientSession()
|
|
39
|
-
logger.debug("Created a new aiohttp ClientSession.")
|
|
40
40
|
|
|
41
41
|
atexit.register(self.close_api)
|
|
42
42
|
|
|
43
|
-
async def __aenter__(self):
|
|
43
|
+
async def __aenter__(self) -> "API":
|
|
44
44
|
if self.__session is None:
|
|
45
45
|
self.__session = aiohttp.ClientSession()
|
|
46
|
-
logger.debug("Created a new aiohttp ClientSession on enter.")
|
|
47
46
|
return self
|
|
48
47
|
|
|
49
|
-
async def __aexit__(self, _exc_type, _exc_val, _exc_tb):
|
|
48
|
+
async def __aexit__(self, _exc_type, _exc_val, _exc_tb) -> None: # noqa: ANN001
|
|
50
49
|
if self.autoclose:
|
|
51
|
-
logger.debug("Closing aiohttp ClientSession on exit.")
|
|
52
50
|
await self.close()
|
|
53
51
|
|
|
54
|
-
async def _get(self, url: str) ->
|
|
52
|
+
async def _get(self, url: str) -> dict | None:
|
|
55
53
|
"""Send a GET request to the given URL and return the response as JSON."""
|
|
56
|
-
logger.debug(
|
|
54
|
+
logger.debug("Sending GET request to %s.", url)
|
|
57
55
|
async with self.__session.get(url) as req:
|
|
58
|
-
if req.status ==
|
|
59
|
-
logger.debug(
|
|
56
|
+
if req.status == HTTPStatus.OK.value:
|
|
57
|
+
logger.debug("Received response from %s: %s.", url, req.status)
|
|
60
58
|
return await req.json(loads=self.json_loads)
|
|
61
|
-
logger.error(
|
|
59
|
+
logger.error("Failed to get response from %s: %s.", url, req.status)
|
|
62
60
|
return None
|
|
63
61
|
|
|
64
62
|
async def _generate_model_instance(
|
|
65
63
|
self,
|
|
66
64
|
url: str,
|
|
67
|
-
model:
|
|
68
|
-
k:
|
|
65
|
+
model: type[T],
|
|
66
|
+
k: str | None = None,
|
|
69
67
|
cache_timeout: int = 300,
|
|
70
|
-
) ->
|
|
68
|
+
) -> T | None:
|
|
71
69
|
"""Generate a model instance from the given URL and optional keyword arguments.
|
|
72
70
|
|
|
73
71
|
Args:
|
|
@@ -84,7 +82,7 @@ class API(ABC):
|
|
|
84
82
|
|
|
85
83
|
dat = await self._get(url)
|
|
86
84
|
if dat is None or not dat:
|
|
87
|
-
logger.warning(
|
|
85
|
+
logger.warning("No data received from %s.", url)
|
|
88
86
|
return None
|
|
89
87
|
|
|
90
88
|
data = model(**{k: dat} if k is not None else dat)
|
|
@@ -98,7 +96,6 @@ class API(ABC):
|
|
|
98
96
|
async def close(self) -> None:
|
|
99
97
|
"""Close the aiohttp session."""
|
|
100
98
|
if self.__session is not None:
|
|
101
|
-
logger.debug("Closing aiohttp ClientSession.")
|
|
102
99
|
await self.__session.close()
|
|
103
100
|
self.__session = None
|
|
104
101
|
self.cache = None
|
|
@@ -111,5 +108,5 @@ class API(ABC):
|
|
|
111
108
|
def powered() -> None:
|
|
112
109
|
return None
|
|
113
110
|
|
|
114
|
-
def close_api(self):
|
|
111
|
+
def close_api(self) -> None:
|
|
115
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,48 @@
|
|
|
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 import slugify2
|
|
9
|
+
from ddapi.enum import MasterEnum
|
|
10
|
+
|
|
8
11
|
rm_list = ["DD-Persian", "/vDQMHSss8W", ""]
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class DRank(BaseModel):
|
|
12
|
-
points:
|
|
13
|
-
rank:
|
|
15
|
+
points: int | None = None
|
|
16
|
+
rank: int | None = None
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
class DPoints(BaseModel):
|
|
17
20
|
total: int
|
|
18
|
-
points:
|
|
19
|
-
rank:
|
|
21
|
+
points: int | None = Field(default=None)
|
|
22
|
+
rank: int | None = Field(default=None)
|
|
20
23
|
|
|
21
24
|
|
|
22
25
|
class DData(BaseModel):
|
|
23
|
-
points:
|
|
24
|
-
team_rank:
|
|
25
|
-
rank:
|
|
26
|
-
maps:
|
|
26
|
+
points: DPoints | None = Field(default=None)
|
|
27
|
+
team_rank: DRank | None = Field(default=None)
|
|
28
|
+
rank: DRank | None = Field(default=None)
|
|
29
|
+
maps: dict | None = Field(default=None)
|
|
27
30
|
|
|
28
31
|
|
|
29
32
|
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:
|
|
33
|
+
Novice: DData | None = None
|
|
34
|
+
Moderate: DData | None = None
|
|
35
|
+
Brutal: DData | None = None
|
|
36
|
+
Insane: DData | None = None
|
|
37
|
+
Dummy: DData | None = None
|
|
38
|
+
DDmaX_Easy: DData | None = Field(default=None, validation_alias="DDmaX.Easy")
|
|
39
|
+
DDmaX_Next: DData | None = Field(default=None, validation_alias="DDmaX.Next")
|
|
40
|
+
DDmaX_Nut: DData | None = Field(default=None, validation_alias="DDmaX.Nut")
|
|
41
|
+
DDmaX_Pro: DData | None = Field(default=None, validation_alias="DDmaX.Pro")
|
|
42
|
+
Oldschool: DData | None = Field(default=None)
|
|
43
|
+
Race: DData | None = None
|
|
44
|
+
Solo: DData | None = None
|
|
45
|
+
total: DData | None = None
|
|
43
46
|
|
|
44
47
|
|
|
45
48
|
class MaxFinishes(BaseModel):
|
|
@@ -76,7 +79,7 @@ class DMap(BaseModel):
|
|
|
76
79
|
points: int
|
|
77
80
|
difficulty: int
|
|
78
81
|
mapper: str
|
|
79
|
-
release:
|
|
82
|
+
release: int | None = None
|
|
80
83
|
median_time: float
|
|
81
84
|
first_finish: int
|
|
82
85
|
last_finish: int
|
|
@@ -90,6 +93,17 @@ class DMap(BaseModel):
|
|
|
90
93
|
ranks: list[DDRanks]
|
|
91
94
|
max_finishes: list[MaxFinishes]
|
|
92
95
|
|
|
96
|
+
def url(self) -> str:
|
|
97
|
+
return f"https://ddnet.org/maps/{quote(slugify2(self.name))}"
|
|
98
|
+
|
|
99
|
+
@staticmethod
|
|
100
|
+
def url_with_name(map_name: str) -> str:
|
|
101
|
+
return f"https://ddnet.org/maps/{quote(slugify2(map_name))}"
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def api(map_name: str) -> str:
|
|
105
|
+
return f"https://ddnet.org/maps/?json={quote(map_name)}"
|
|
106
|
+
|
|
93
107
|
|
|
94
108
|
class FirstFinish(BaseModel):
|
|
95
109
|
timestamp: float
|
|
@@ -102,7 +116,7 @@ class LastFinish(BaseModel):
|
|
|
102
116
|
map: str
|
|
103
117
|
time: float
|
|
104
118
|
country: str
|
|
105
|
-
type:
|
|
119
|
+
type: str | None = None
|
|
106
120
|
|
|
107
121
|
|
|
108
122
|
class FavoritePartner(BaseModel):
|
|
@@ -122,24 +136,35 @@ class FavoriteServer(BaseModel):
|
|
|
122
136
|
class DDPlayer(BaseModel):
|
|
123
137
|
player: str
|
|
124
138
|
points: DPoints
|
|
125
|
-
team_rank:
|
|
126
|
-
rank:
|
|
127
|
-
points_last_year:
|
|
128
|
-
points_last_month:
|
|
129
|
-
points_last_week:
|
|
139
|
+
team_rank: DRank | None = None
|
|
140
|
+
rank: DRank | None = None
|
|
141
|
+
points_last_year: DRank | None = None
|
|
142
|
+
points_last_month: DRank | None = None
|
|
143
|
+
points_last_week: DRank | None = None
|
|
130
144
|
favorite_server: FavoriteServer
|
|
131
145
|
first_finish: FirstFinish
|
|
132
146
|
last_finishes: list[LastFinish]
|
|
133
|
-
favorite_partners:
|
|
147
|
+
favorite_partners: list[FavoritePartner] | None = None
|
|
134
148
|
types: DDType
|
|
135
149
|
activity: list[Activity]
|
|
136
150
|
hours_played_past_365_days: int
|
|
137
151
|
|
|
152
|
+
def url(self) -> str:
|
|
153
|
+
return f"https://ddnet.org/players/{quote(slugify2(self.player))}"
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def url_with_name(player: str) -> str:
|
|
157
|
+
return f"https://ddnet.org/players/{quote(slugify2(player))}"
|
|
158
|
+
|
|
159
|
+
@staticmethod
|
|
160
|
+
def api(player: str) -> str:
|
|
161
|
+
return f"https://ddnet.org/players/?json2={quote(player)}"
|
|
162
|
+
|
|
138
163
|
|
|
139
164
|
class Skin(BaseModel):
|
|
140
|
-
name:
|
|
141
|
-
color_body:
|
|
142
|
-
color_feet:
|
|
165
|
+
name: str | None = None
|
|
166
|
+
color_body: int | None = None
|
|
167
|
+
color_feet: int | None = None
|
|
143
168
|
|
|
144
169
|
|
|
145
170
|
class Client(BaseModel):
|
|
@@ -147,24 +172,24 @@ class Client(BaseModel):
|
|
|
147
172
|
clan: str
|
|
148
173
|
country: int
|
|
149
174
|
score: int
|
|
150
|
-
is_player:
|
|
151
|
-
skin:
|
|
152
|
-
afk:
|
|
153
|
-
team:
|
|
175
|
+
is_player: bool | None = None
|
|
176
|
+
skin: Skin | None = None
|
|
177
|
+
afk: bool | None = None
|
|
178
|
+
team: int | None = None
|
|
154
179
|
|
|
155
180
|
|
|
156
181
|
class Map(BaseModel):
|
|
157
182
|
name: str
|
|
158
|
-
sha256:
|
|
159
|
-
size:
|
|
183
|
+
sha256: str | None = None
|
|
184
|
+
size: int | None = None
|
|
160
185
|
|
|
161
186
|
|
|
162
187
|
class Community(BaseModel):
|
|
163
188
|
id: str
|
|
164
189
|
icon: str
|
|
165
190
|
admin: list[str]
|
|
166
|
-
public_key:
|
|
167
|
-
signature:
|
|
191
|
+
public_key: str | None = None
|
|
192
|
+
signature: str | None = None
|
|
168
193
|
|
|
169
194
|
|
|
170
195
|
class Info(BaseModel):
|
|
@@ -175,9 +200,9 @@ class Info(BaseModel):
|
|
|
175
200
|
name: str = None
|
|
176
201
|
map: Map = None
|
|
177
202
|
version: str = None
|
|
178
|
-
clients:
|
|
179
|
-
requires_login:
|
|
180
|
-
community:
|
|
203
|
+
clients: list[Client] | None = None
|
|
204
|
+
requires_login: bool | None = None
|
|
205
|
+
community: Community | None = None
|
|
181
206
|
|
|
182
207
|
def __len__(self) -> int:
|
|
183
208
|
return len(self.clients)
|
|
@@ -211,12 +236,14 @@ class Master(BaseModel):
|
|
|
211
236
|
def __len__(self) -> int:
|
|
212
237
|
return len(self.servers)
|
|
213
238
|
|
|
214
|
-
def get_clans(
|
|
239
|
+
def get_clans(
|
|
240
|
+
self, rm: list[str] | None = None
|
|
241
|
+
) -> list[str] | list[tuple[str, int]]:
|
|
215
242
|
remove_list = rm_list.copy() if rm is None else rm
|
|
216
243
|
if not self.servers:
|
|
217
244
|
return []
|
|
218
245
|
|
|
219
|
-
dat: Counter[
|
|
246
|
+
dat: Counter[str] = Counter(
|
|
220
247
|
client.clan
|
|
221
248
|
for server in self.servers
|
|
222
249
|
for client in server.info.clients
|
|
@@ -254,6 +281,10 @@ class Master(BaseModel):
|
|
|
254
281
|
def count_clients(self) -> int:
|
|
255
282
|
return sum(i.count_client for i in self.servers)
|
|
256
283
|
|
|
284
|
+
@staticmethod
|
|
285
|
+
def api(server: MasterEnum) -> str:
|
|
286
|
+
return f"https://master{server}.ddnet.org/ddnet/15/servers.json"
|
|
287
|
+
|
|
257
288
|
|
|
258
289
|
class ReleasesMapsData(BaseModel):
|
|
259
290
|
name: str
|
|
@@ -273,6 +304,14 @@ class ReleasesMapsData(BaseModel):
|
|
|
273
304
|
class ReleasesMaps(BaseModel):
|
|
274
305
|
maps: list[ReleasesMapsData]
|
|
275
306
|
|
|
307
|
+
@staticmethod
|
|
308
|
+
def url() -> str:
|
|
309
|
+
return "https://ddnet.org/releases"
|
|
310
|
+
|
|
311
|
+
@staticmethod
|
|
312
|
+
def api() -> str:
|
|
313
|
+
return "https://ddnet.org/releases/maps.json"
|
|
314
|
+
|
|
276
315
|
|
|
277
316
|
class QueryData(BaseModel):
|
|
278
317
|
points: int
|
|
@@ -282,6 +321,10 @@ class QueryData(BaseModel):
|
|
|
282
321
|
class Query(BaseModel):
|
|
283
322
|
players: list[QueryData]
|
|
284
323
|
|
|
324
|
+
@staticmethod
|
|
325
|
+
def api(player: str) -> str:
|
|
326
|
+
return f"https://ddnet.org/players/?query={quote(player)}"
|
|
327
|
+
|
|
285
328
|
|
|
286
329
|
class QueryMapperData(BaseModel):
|
|
287
330
|
mapper: str
|
|
@@ -291,6 +334,10 @@ class QueryMapperData(BaseModel):
|
|
|
291
334
|
class QueryMapper(BaseModel):
|
|
292
335
|
players: list[QueryMapperData]
|
|
293
336
|
|
|
337
|
+
@staticmethod
|
|
338
|
+
def api(player: str) -> str:
|
|
339
|
+
return f"https://ddnet.org/maps/?qmapper={quote(player)}"
|
|
340
|
+
|
|
294
341
|
|
|
295
342
|
class QueryMapData(BaseModel):
|
|
296
343
|
name: str
|
|
@@ -301,6 +348,10 @@ class QueryMapData(BaseModel):
|
|
|
301
348
|
class QueryMap(BaseModel):
|
|
302
349
|
maps: list[QueryMapData]
|
|
303
350
|
|
|
351
|
+
@staticmethod
|
|
352
|
+
def api(map_name: str) -> str:
|
|
353
|
+
return f"https://ddnet.org/maps/?query={quote(map_name)}"
|
|
354
|
+
|
|
304
355
|
|
|
305
356
|
class DDStatusData(BaseModel):
|
|
306
357
|
name: str = None
|
|
@@ -326,3 +377,11 @@ class DDStatusData(BaseModel):
|
|
|
326
377
|
|
|
327
378
|
class DDStatus(BaseModel):
|
|
328
379
|
servers: list[DDStatusData]
|
|
380
|
+
|
|
381
|
+
@staticmethod
|
|
382
|
+
def url() -> str:
|
|
383
|
+
return "https://ddnet.org/status"
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def api() -> str:
|
|
387
|
+
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]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
__all__ = ("slugify2",)
|
|
2
|
+
|
|
3
|
+
slugify2_symbols = "[\t !\"#$%&'()*-/<=>?@[\\]^_`{|},.:]+"
|
|
4
|
+
NON_ASCII_CHARACTER_THRESHOLD = 128
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def slugify2(_nickname: str) -> str:
|
|
8
|
+
"""Needed for a link generator."""
|
|
9
|
+
string = ""
|
|
10
|
+
for symbol in _nickname:
|
|
11
|
+
string += (
|
|
12
|
+
f"-{ord(symbol)}-"
|
|
13
|
+
if symbol in slugify2_symbols
|
|
14
|
+
or ord(symbol) >= NON_ASCII_CHARACTER_THRESHOLD
|
|
15
|
+
else symbol
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
return string
|
|
@@ -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.1",
|
|
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,28 +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
|
-
self._cache[key] = value
|
|
15
|
-
if timeout_seconds is not None:
|
|
16
|
-
asyncio.create_task(self._remove_after_timeout(key, timeout_seconds))
|
|
17
|
-
return True
|
|
18
|
-
|
|
19
|
-
async def delete(self, key: str) -> bool:
|
|
20
|
-
async with self._lock:
|
|
21
|
-
if key in self._cache:
|
|
22
|
-
del self._cache[key]
|
|
23
|
-
return True
|
|
24
|
-
return False
|
|
25
|
-
|
|
26
|
-
async def _remove_after_timeout(self, key: str, timeout: int):
|
|
27
|
-
await asyncio.sleep(timeout)
|
|
28
|
-
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
|