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.
Files changed (30) hide show
  1. {ddapi-0.13.1 → ddapi-0.14.1}/PKG-INFO +1 -1
  2. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/__init__.py +2 -1
  3. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/_api.py +20 -23
  4. ddapi-0.14.1/ddapi/ddapi/api/ddnet.py +56 -0
  5. ddapi-0.14.1/ddapi/ddapi/api/ddstats.py +38 -0
  6. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/api/status.py +7 -7
  7. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/cache/base.py +9 -6
  8. ddapi-0.14.1/ddapi/ddapi/cache/empty.py +13 -0
  9. ddapi-0.14.1/ddapi/ddapi/cache/memory.py +40 -0
  10. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/ddnet.py +105 -46
  11. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/ddstats_tw.py +84 -46
  12. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/status_tw.py +3 -3
  13. ddapi-0.14.1/ddapi/ddapi/util.py +18 -0
  14. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/PKG-INFO +1 -1
  15. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/SOURCES.txt +1 -0
  16. {ddapi-0.13.1 → ddapi-0.14.1}/setup.cfg +1 -1
  17. {ddapi-0.13.1 → ddapi-0.14.1}/setup.py +4 -2
  18. ddapi-0.13.1/ddapi/ddapi/api/ddnet.py +0 -72
  19. ddapi-0.13.1/ddapi/ddapi/api/ddstats.py +0 -44
  20. ddapi-0.13.1/ddapi/ddapi/cache/empty.py +0 -14
  21. ddapi-0.13.1/ddapi/ddapi/cache/memory.py +0 -28
  22. {ddapi-0.13.1 → ddapi-0.14.1}/LICENSE +0 -0
  23. {ddapi-0.13.1 → ddapi-0.14.1}/README.md +0 -0
  24. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/api/__init__.py +0 -0
  25. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/cache/__init__.py +0 -0
  26. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/enum.py +0 -0
  27. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/scheme/__init__.py +0 -0
  28. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi/types.py +0 -0
  29. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/dependency_links.txt +0 -0
  30. {ddapi-0.13.1 → ddapi-0.14.1}/ddapi/ddapi.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddapi
3
- Version: 0.13.1
3
+ Version: 0.14.1
4
4
  Summary: DDnet api
5
5
  Home-page: https://github.com/ByFox213/ddapi
6
6
  Author: ByFox
@@ -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.13.1"
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 abc import ABC
5
- from typing import Optional, Type, Any
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(ABC):
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 = EmptyCache(),
30
- json_loads: Any = DEFAULT_JSON_DECODER,
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) -> Optional[dict]:
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(f"Sending GET request to {url}.")
54
+ logger.debug("Sending GET request to %s.", url)
57
55
  async with self.__session.get(url) as req:
58
- if req.status == 200:
59
- logger.debug(f"Received response from {url}: {req.status}.")
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(f"Failed to get response from {url}: {req.status}.")
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: Type[T],
68
- k: Optional[str] = None,
65
+ model: type[T],
66
+ k: str | None = None,
69
67
  cache_timeout: int = 300,
70
- ) -> Optional[T]:
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(f"No data received from {url}.")
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 .._api import API
4
- from ..scheme import (
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 Any
3
+ from typing import Generic
4
4
 
5
+ from ddapi.types import T
5
6
 
6
- class CacheABC(ABC):
7
- def __init__(self):
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: Any = None) -> Any:
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: Any, timeout_seconds: int = None) -> bool:
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: Any = None) -> Any:
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 typing import Optional, Any
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: Optional[int] = None
13
- rank: Optional[int] = None
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: Optional[int] = Field(default=None)
19
- rank: Optional[int] = Field(default=None)
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: Optional[DPoints] = Field(default=None)
24
- team_rank: Optional[DRank] = Field(default=None)
25
- rank: Optional[DRank] = Field(default=None)
26
- maps: Optional[dict] = Field(default=None)
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: Optional[DData] = None
31
- Moderate: Optional[DData] = None
32
- Brutal: Optional[DData] = None
33
- Insane: Optional[DData] = None
34
- Dummy: Optional[DData] = None
35
- DDmaX_Easy: Optional[DData] = Field(default=None, validation_alias="DDmaX.Easy")
36
- DDmaX_Next: Optional[DData] = Field(default=None, validation_alias="DDmaX.Next")
37
- DDmaX_Nut: Optional[DData] = Field(default=None, validation_alias="DDmaX.Nut")
38
- DDmaX_Pro: Optional[DData] = Field(default=None, validation_alias="DDmaX.Pro")
39
- Oldschool: Optional[DData] = Field(default=None)
40
- Race: Optional[DData] = None
41
- Solo: Optional[DData] = None
42
- total: Optional[DData] = None
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: Optional[int] = None
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: Optional[str] = None
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: 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
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: Optional[list[FavoritePartner]] = None
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: Optional[str] = None
141
- color_body: Optional[int] = None
142
- color_feet: Optional[int] = None
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: Optional[bool] = None
151
- skin: Optional[Skin] = None
152
- afk: Optional[bool] = None
153
- team: Optional[int] = None
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: Optional[str] = None
159
- size: Optional[int] = None
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: Optional[str] = None
167
- signature: Optional[str] = None
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: Optional[list[Client]] = None
179
- requires_login: Optional[bool] = None
180
- community: Optional[Community] = None
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(self, rm: list[str] = None) -> list[tuple[Any, int]]:
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[Any] = 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: Optional[PData] = None
15
- Moderate: Optional[PData] = None
16
- Brutal: Optional[PData] = None
17
- Insane: Optional[PData] = None
18
- Dummy: Optional[PData] = None
19
- DDmaX_Easy: Optional[PData] = Field(default=None, validation_alias="DDmaX.Easy")
20
- DDmaX_Next: Optional[PData] = Field(default=None, validation_alias="DDmaX.Next")
21
- DDmaX_Nut: Optional[PData] = Field(default=None, validation_alias="DDmaX.Nut")
22
- DDmaX_Pro: Optional[PData] = Field(default=None, validation_alias="DDmaX.Pro")
23
- Oldschool: Optional[PData] = None
24
- Race: Optional[PData] = None
25
- Solo: Optional[PData] = None
26
- total: Optional[PData] = None
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: Optional[str]
40
- country: Optional[int]
41
- skin_name: Optional[str]
42
- skin_color_body: Optional[int] = None
43
- skin_color_feet: Optional[int] = None
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: Optional[datetime] = None
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: Optional[datetime]
112
+ timestamp: datetime | None
101
113
  server: str
102
114
  rank: int
103
- team_rank: Optional[int] = None
104
- seconds_played: Optional[int] = None
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: Optional[int] = None
111
- median_time: Optional[float] = None
122
+ finishes_rank: int | None = None
123
+ median_time: float | None = None
112
124
 
113
125
 
114
126
  class Points(BaseModel):
115
- weekly_points: Optional[PData] = None
116
- monthly_points: Optional[PData] = None
117
- yearly_points: Optional[PData] = None
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: Optional[str] = None
136
- skin_color_body: Optional[int] = None
137
- skin_color_feet: Optional[int] = None
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: Optional[DDMap] = None
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: Optional[datetime] = None
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: Optional[list[FavouriteTeammates]] = None
203
+ favourite_teammates: list[FavouriteTeammates] | None = None
192
204
  profile: Profile
193
205
  is_mapper: bool
194
206
  finishes: list[Finishes]
195
- unfinished_maps: Optional[list[UnfinishedMap]] = None
207
+ unfinished_maps: list[UnfinishedMap] | None = None
196
208
  points: Points
197
- recent_activity: Optional[list[RecentActivity]] = None
198
- recent_player_info: Optional[list[RecentPlayerInfo]] = None
199
- most_played_maps: Optional[list[MostPlayedMaps]] = None
200
- most_played_gametypes: Optional[list[MostPlayed]] = None
201
- most_played_categories: Optional[list[MostPlayed]] = None
202
- most_played_locations: Optional[list[MostPlayed]] = None
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: Optional[GeneralActivity] = None
205
- favourite_rank1s_teammates: Optional[list[FavouriteRank1sTeammates]] = None
206
- all_top_10s: Optional[list[AllTop10s]] = None
207
- recent_top_10s: Optional[list[RecentTop10s]] = None
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: Optional[datetime] = None
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: Optional[datetime] = None
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: Optional[str] = None
60
- discord_invite: Optional[str] = Field(None, alias="discordInvite")
61
- description: Optional[str] = None
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,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddapi
3
- Version: 0.13.1
3
+ Version: 0.14.1
4
4
  Summary: DDnet api
5
5
  Home-page: https://github.com/ByFox213/ddapi
6
6
  Author: ByFox
@@ -6,6 +6,7 @@ ddapi/ddapi/__init__.py
6
6
  ddapi/ddapi/_api.py
7
7
  ddapi/ddapi/enum.py
8
8
  ddapi/ddapi/types.py
9
+ ddapi/ddapi/util.py
9
10
  ddapi/ddapi.egg-info/PKG-INFO
10
11
  ddapi/ddapi.egg-info/SOURCES.txt
11
12
  ddapi/ddapi.egg-info/dependency_links.txt
@@ -1,6 +1,6 @@
1
1
  [metadata]
2
2
  name = ddapi
3
- version = 0.13.1
3
+ version = 0.14.1
4
4
  author = ByFox
5
5
  description = DDnet api
6
6
  long_description = file: README.md
@@ -1,11 +1,13 @@
1
+ from pathlib import Path
2
+
1
3
  import setuptools
2
4
 
3
- with open("README.md", "r", encoding="utf-8") as fh:
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.13.1",
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