noxaeapi-sdk 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- noxaeapi_sdk/__init__.py +41 -0
- noxaeapi_sdk/client.py +170 -0
- noxaeapi_sdk/errors.py +132 -0
- noxaeapi_sdk/http_engine.py +259 -0
- noxaeapi_sdk/modules/__init__.py +0 -0
- noxaeapi_sdk/modules/economy.py +75 -0
- noxaeapi_sdk/modules/leaderboard.py +39 -0
- noxaeapi_sdk/modules/luckperms.py +87 -0
- noxaeapi_sdk/modules/misc.py +29 -0
- noxaeapi_sdk/modules/network.py +91 -0
- noxaeapi_sdk/modules/network_hub.py +100 -0
- noxaeapi_sdk/modules/noxauth.py +35 -0
- noxaeapi_sdk/modules/players.py +63 -0
- noxaeapi_sdk/modules/plugins.py +37 -0
- noxaeapi_sdk/modules/server.py +126 -0
- noxaeapi_sdk/modules/skills.py +35 -0
- noxaeapi_sdk/modules/worlds.py +57 -0
- noxaeapi_sdk/py.typed +0 -0
- noxaeapi_sdk/socket.py +167 -0
- noxaeapi_sdk/types.py +305 -0
- noxaeapi_sdk-0.4.0.dist-info/METADATA +176 -0
- noxaeapi_sdk-0.4.0.dist-info/RECORD +24 -0
- noxaeapi_sdk-0.4.0.dist-info/WHEEL +4 -0
- noxaeapi_sdk-0.4.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.parse
|
|
4
|
+
from typing import Any, Dict, List
|
|
5
|
+
|
|
6
|
+
from ..http_engine import HttpEngine
|
|
7
|
+
from ..types import Weather, World
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _q(value: str) -> str:
|
|
11
|
+
return urllib.parse.quote(value, safe="")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class WorldsModule:
|
|
15
|
+
def __init__(self, http: HttpEngine) -> None:
|
|
16
|
+
self._http = http
|
|
17
|
+
|
|
18
|
+
def list(self) -> List[World]:
|
|
19
|
+
"""List all worlds."""
|
|
20
|
+
return self._http.request("GET", "worlds")
|
|
21
|
+
|
|
22
|
+
def save_all(self) -> None:
|
|
23
|
+
"""Save all worlds to disk."""
|
|
24
|
+
return self._http.request("POST", "worlds/save")
|
|
25
|
+
|
|
26
|
+
def download_all(self) -> Dict[str, str]:
|
|
27
|
+
"""Get a download link/stream reference for all worlds."""
|
|
28
|
+
return self._http.request("GET", "worlds/download")
|
|
29
|
+
|
|
30
|
+
def get(self, uuid: str) -> World:
|
|
31
|
+
"""Get a single world by UUID."""
|
|
32
|
+
return self._http.request("GET", f"worlds/{_q(uuid)}")
|
|
33
|
+
|
|
34
|
+
def save(self, uuid: str) -> None:
|
|
35
|
+
"""Save a specific world to disk."""
|
|
36
|
+
return self._http.request("POST", f"worlds/{_q(uuid)}/save")
|
|
37
|
+
|
|
38
|
+
def download(self, uuid: str) -> Dict[str, str]:
|
|
39
|
+
"""Get a download link/stream reference for a specific world."""
|
|
40
|
+
return self._http.request("GET", f"worlds/{_q(uuid)}/download")
|
|
41
|
+
|
|
42
|
+
def set_time(self, uuid: str, time: int) -> None:
|
|
43
|
+
"""Set the in-game time for a world (0-24000)."""
|
|
44
|
+
return self._http.request("POST", f"worlds/{_q(uuid)}/time", body={"time": time}, form=True)
|
|
45
|
+
|
|
46
|
+
def set_weather(self, uuid: str, weather: Weather) -> None:
|
|
47
|
+
"""Set the weather for a world.
|
|
48
|
+
|
|
49
|
+
Server-side (``WorldApi.setWorldWeather``) reads a single
|
|
50
|
+
``weather`` enum string — "clear" | "rain" | "thunder" — not
|
|
51
|
+
separate storm/thundering booleans.
|
|
52
|
+
"""
|
|
53
|
+
return self._http.request("POST", f"worlds/{_q(uuid)}/weather", body={"weather": weather}, form=True)
|
|
54
|
+
|
|
55
|
+
def get_entities(self, uuid: str) -> Dict[str, Any]:
|
|
56
|
+
"""Get entity counts within a specific world."""
|
|
57
|
+
return self._http.request("GET", f"worlds/{_q(uuid)}/entities")
|
noxaeapi_sdk/py.typed
ADDED
|
File without changes
|
noxaeapi_sdk/socket.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Realtime console/event socket.
|
|
2
|
+
|
|
3
|
+
Requires the optional ``websocket-client`` dependency:
|
|
4
|
+
|
|
5
|
+
pip install noxaeapi-sdk[ws]
|
|
6
|
+
|
|
7
|
+
The import is lazy — importing :mod:`noxaeapi_sdk` itself never requires
|
|
8
|
+
``websocket-client`` to be installed; only calling
|
|
9
|
+
:meth:`~noxaeapi_sdk.client.NoxAeApiClient.connect` does.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import random
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
import urllib.parse
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from typing import Any, Callable, Dict, List, Optional, Set
|
|
21
|
+
|
|
22
|
+
Listener = Callable[[Any], None]
|
|
23
|
+
|
|
24
|
+
WS_EVENTS = ("open", "close", "error", "console", "event", "message")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _to_ws_url(base_url: str, route: str, api_key: Optional[str] = None) -> str:
|
|
28
|
+
parsed = urllib.parse.urlsplit(f"{base_url.rstrip('/')}/v1/ws/{route.lstrip('/')}")
|
|
29
|
+
scheme = "wss" if parsed.scheme == "https" else "ws"
|
|
30
|
+
query = dict(urllib.parse.parse_qsl(parsed.query))
|
|
31
|
+
if api_key:
|
|
32
|
+
query["key"] = api_key
|
|
33
|
+
new_query = urllib.parse.urlencode(query)
|
|
34
|
+
return urllib.parse.urlunsplit((scheme, parsed.netloc, parsed.path, new_query, ""))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class NoxAeApiWsOptions:
|
|
39
|
+
"""Options for :class:`NoxAeApiSocket`."""
|
|
40
|
+
|
|
41
|
+
base_url: str
|
|
42
|
+
api_key: Optional[str] = None
|
|
43
|
+
route: str = "events"
|
|
44
|
+
"""Route suffix under the websocket base, e.g. "console" or "events"."""
|
|
45
|
+
auto_reconnect: bool = True
|
|
46
|
+
max_reconnect_delay: float = 30.0
|
|
47
|
+
"""Max reconnect delay in seconds."""
|
|
48
|
+
extra: Dict[str, Any] = field(default_factory=dict)
|
|
49
|
+
"""Extra kwargs forwarded to ``websocket.WebSocketApp``."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class NoxAeApiSocket:
|
|
53
|
+
"""Thin wrapper around the server's WebSocket endpoints (console tail and event broadcasts).
|
|
54
|
+
|
|
55
|
+
Runs the connection on a background thread and handles reconnection
|
|
56
|
+
with exponential backoff, so consumers can just attach listeners and
|
|
57
|
+
not think about the socket lifecycle.
|
|
58
|
+
|
|
59
|
+
Usage::
|
|
60
|
+
|
|
61
|
+
ws = client.connect(route="console")
|
|
62
|
+
ws.on("console", lambda line: print(line))
|
|
63
|
+
ws.on("close", lambda _: print("disconnected"))
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
def __init__(self, options: NoxAeApiWsOptions) -> None:
|
|
67
|
+
try:
|
|
68
|
+
import websocket # type: ignore[import-untyped]
|
|
69
|
+
except ImportError as exc: # pragma: no cover - exercised only when dep missing
|
|
70
|
+
raise ImportError(
|
|
71
|
+
"NoxAeApiSocket requires the optional 'websocket-client' package. "
|
|
72
|
+
"Install it with: pip install noxaeapi-sdk[ws]"
|
|
73
|
+
) from exc
|
|
74
|
+
|
|
75
|
+
self._websocket_module = websocket
|
|
76
|
+
self._options = options
|
|
77
|
+
self._listeners: Dict[str, Set[Listener]] = {event: set() for event in WS_EVENTS}
|
|
78
|
+
self._reconnect_attempt = 0
|
|
79
|
+
self._closed_by_user = False
|
|
80
|
+
self._app: Optional[Any] = None
|
|
81
|
+
self._thread: Optional[threading.Thread] = None
|
|
82
|
+
self._open()
|
|
83
|
+
|
|
84
|
+
# -- connection lifecycle -------------------------------------------------
|
|
85
|
+
|
|
86
|
+
def _open(self) -> None:
|
|
87
|
+
url = _to_ws_url(self._options.base_url, self._options.route, self._options.api_key)
|
|
88
|
+
|
|
89
|
+
def on_open(_ws: Any) -> None:
|
|
90
|
+
self._reconnect_attempt = 0
|
|
91
|
+
self._emit("open", None)
|
|
92
|
+
|
|
93
|
+
def on_message(_ws: Any, message: Any) -> None:
|
|
94
|
+
text = message if isinstance(message, str) else message.decode("utf-8", errors="replace")
|
|
95
|
+
parsed = _safe_parse(text)
|
|
96
|
+
self._emit("message", parsed)
|
|
97
|
+
if isinstance(parsed, dict) and parsed.get("type") in ("console", "event"):
|
|
98
|
+
self._emit(parsed["type"], parsed)
|
|
99
|
+
|
|
100
|
+
def on_error(_ws: Any, error: Any) -> None:
|
|
101
|
+
self._emit("error", error)
|
|
102
|
+
|
|
103
|
+
def on_close(_ws: Any, _status_code: Any, _msg: Any) -> None:
|
|
104
|
+
self._emit("close", None)
|
|
105
|
+
if not self._closed_by_user and self._options.auto_reconnect:
|
|
106
|
+
self._schedule_reconnect()
|
|
107
|
+
|
|
108
|
+
self._app = self._websocket_module.WebSocketApp(
|
|
109
|
+
url,
|
|
110
|
+
on_open=on_open,
|
|
111
|
+
on_message=on_message,
|
|
112
|
+
on_error=on_error,
|
|
113
|
+
on_close=on_close,
|
|
114
|
+
**self._options.extra,
|
|
115
|
+
)
|
|
116
|
+
self._thread = threading.Thread(target=self._app.run_forever, daemon=True)
|
|
117
|
+
self._thread.start()
|
|
118
|
+
|
|
119
|
+
def _schedule_reconnect(self) -> None:
|
|
120
|
+
max_delay = self._options.max_reconnect_delay
|
|
121
|
+
delay = min(max_delay, 0.5 * (2 ** self._reconnect_attempt))
|
|
122
|
+
self._reconnect_attempt += 1
|
|
123
|
+
|
|
124
|
+
def _reconnect() -> None:
|
|
125
|
+
time.sleep(delay)
|
|
126
|
+
if not self._closed_by_user:
|
|
127
|
+
self._open()
|
|
128
|
+
|
|
129
|
+
threading.Thread(target=_reconnect, daemon=True).start()
|
|
130
|
+
|
|
131
|
+
# -- listeners --------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
def on(self, event: str, listener: Listener) -> Callable[[], None]:
|
|
134
|
+
"""Register a listener for ``event``. Returns an unsubscribe callable."""
|
|
135
|
+
if event not in self._listeners:
|
|
136
|
+
self._listeners[event] = set()
|
|
137
|
+
self._listeners[event].add(listener)
|
|
138
|
+
return lambda: self._listeners.get(event, set()).discard(listener)
|
|
139
|
+
|
|
140
|
+
def off(self, event: str, listener: Listener) -> None:
|
|
141
|
+
self._listeners.get(event, set()).discard(listener)
|
|
142
|
+
|
|
143
|
+
def _emit(self, event: str, payload: Any) -> None:
|
|
144
|
+
for listener in list(self._listeners.get(event, set())):
|
|
145
|
+
listener(payload)
|
|
146
|
+
|
|
147
|
+
# -- send/close ---------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
def send(self, payload: Any) -> None:
|
|
150
|
+
"""Send a raw payload over the socket, JSON-encoded if not already a string."""
|
|
151
|
+
if self._app is None or self._app.sock is None or not self._app.sock.connected:
|
|
152
|
+
raise RuntimeError("Cannot send: socket is not open")
|
|
153
|
+
data = payload if isinstance(payload, str) else json.dumps(payload)
|
|
154
|
+
self._app.send(data)
|
|
155
|
+
|
|
156
|
+
def close(self) -> None:
|
|
157
|
+
"""Close the socket and stop reconnecting."""
|
|
158
|
+
self._closed_by_user = True
|
|
159
|
+
if self._app is not None:
|
|
160
|
+
self._app.close()
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _safe_parse(text: str) -> Any:
|
|
164
|
+
try:
|
|
165
|
+
return json.loads(text)
|
|
166
|
+
except (json.JSONDecodeError, TypeError):
|
|
167
|
+
return text
|
noxaeapi_sdk/types.py
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Response shape hints for the NoxAeApi REST API.
|
|
2
|
+
|
|
3
|
+
These are :class:`typing.TypedDict` definitions, not runtime-validated
|
|
4
|
+
models — every SDK method still just returns whatever JSON the server
|
|
5
|
+
sent, parsed into plain ``dict``/``list``/scalar values. The TypedDicts
|
|
6
|
+
exist purely so editors and type checkers (mypy/pyright) can give you
|
|
7
|
+
autocomplete and structural checks against the documented shapes.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, List, Literal, Optional, Tuple, TypedDict, Union
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class OnlinePlayer(TypedDict):
|
|
16
|
+
uuid: str
|
|
17
|
+
displayName: str
|
|
18
|
+
address: Optional[str]
|
|
19
|
+
port: Optional[int]
|
|
20
|
+
exhaustion: float
|
|
21
|
+
exp: float
|
|
22
|
+
expLevel: int
|
|
23
|
+
whitelisted: bool
|
|
24
|
+
banned: bool
|
|
25
|
+
op: bool
|
|
26
|
+
balance: Optional[float]
|
|
27
|
+
location: Optional[Tuple[float, float, float]]
|
|
28
|
+
dimension: Optional[str]
|
|
29
|
+
health: float
|
|
30
|
+
hunger: int
|
|
31
|
+
saturation: float
|
|
32
|
+
gamemode: str
|
|
33
|
+
lastPlayed: int
|
|
34
|
+
authenticated: Optional[bool]
|
|
35
|
+
registered: Optional[bool]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class OfflinePlayer(TypedDict):
|
|
39
|
+
uuid: str
|
|
40
|
+
displayName: str
|
|
41
|
+
whitelisted: bool
|
|
42
|
+
banned: bool
|
|
43
|
+
op: bool
|
|
44
|
+
balance: Optional[float]
|
|
45
|
+
lastPlayed: int
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ServerHealth(TypedDict):
|
|
49
|
+
cpus: int
|
|
50
|
+
uptime: int
|
|
51
|
+
totalMemory: int
|
|
52
|
+
maxMemory: int
|
|
53
|
+
freeMemory: int
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ServerBan(TypedDict):
|
|
57
|
+
target: str
|
|
58
|
+
source: Optional[str]
|
|
59
|
+
reason: Optional[str]
|
|
60
|
+
expiration: Optional[str]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class WhitelistEntry(TypedDict):
|
|
64
|
+
uuid: str
|
|
65
|
+
name: str
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ServerInfo(TypedDict):
|
|
69
|
+
name: str
|
|
70
|
+
motd: str
|
|
71
|
+
version: str
|
|
72
|
+
bukkitVersion: str
|
|
73
|
+
tps: str
|
|
74
|
+
health: ServerHealth
|
|
75
|
+
bannedIps: List[ServerBan]
|
|
76
|
+
bannedPlayers: List[ServerBan]
|
|
77
|
+
whitelistedPlayers: List[WhitelistEntry]
|
|
78
|
+
maxPlayers: int
|
|
79
|
+
onlinePlayers: int
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class World(TypedDict):
|
|
83
|
+
name: str
|
|
84
|
+
uuid: str
|
|
85
|
+
time: int
|
|
86
|
+
storm: bool
|
|
87
|
+
thundering: bool
|
|
88
|
+
generateStructures: bool
|
|
89
|
+
allowAnimals: bool
|
|
90
|
+
allowMonsters: bool
|
|
91
|
+
difficulty: str
|
|
92
|
+
environment: str
|
|
93
|
+
seed: str
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Score(TypedDict):
|
|
97
|
+
entry: str
|
|
98
|
+
value: int
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Objective(TypedDict):
|
|
102
|
+
name: str
|
|
103
|
+
displayName: str
|
|
104
|
+
criterion: str
|
|
105
|
+
scores: List[Score]
|
|
106
|
+
displaySlot: Optional[str]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Scoreboard(TypedDict):
|
|
110
|
+
objectives: List[str]
|
|
111
|
+
entries: List[str]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class InventoryItem(TypedDict):
|
|
115
|
+
id: str
|
|
116
|
+
count: int
|
|
117
|
+
slot: int
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class Plugin(TypedDict):
|
|
121
|
+
name: str
|
|
122
|
+
enabled: bool
|
|
123
|
+
version: str
|
|
124
|
+
website: Optional[str]
|
|
125
|
+
authors: List[str]
|
|
126
|
+
depends: List[str]
|
|
127
|
+
softDepends: List[str]
|
|
128
|
+
apiVersion: Optional[str]
|
|
129
|
+
description: Optional[str]
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class EconomyInfo(TypedDict, total=False):
|
|
133
|
+
available: bool
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class PlayerBalance(TypedDict):
|
|
137
|
+
uuid: str
|
|
138
|
+
balance: float
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class TopBalanceEntry(TypedDict):
|
|
142
|
+
uuid: str
|
|
143
|
+
balance: float
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class GroupInfo(TypedDict):
|
|
147
|
+
name: str
|
|
148
|
+
permissions: List[str]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class PermissionNode(TypedDict):
|
|
152
|
+
permission: str
|
|
153
|
+
value: bool
|
|
154
|
+
expiry: int
|
|
155
|
+
server: Optional[str]
|
|
156
|
+
world: Optional[str]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class Advancement(TypedDict):
|
|
160
|
+
key: str
|
|
161
|
+
criteria: List[str]
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class NoxAuthPlayerInfo(TypedDict):
|
|
165
|
+
uuid: str
|
|
166
|
+
name: str
|
|
167
|
+
registered: bool
|
|
168
|
+
authenticated: bool
|
|
169
|
+
lastIp: Optional[str]
|
|
170
|
+
lastLoginTime: Optional[int]
|
|
171
|
+
countryCode: Optional[str]
|
|
172
|
+
countryName: Optional[str]
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class PasswordCheckResult(TypedDict):
|
|
176
|
+
name: str
|
|
177
|
+
valid: bool
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class PlayerStats(TypedDict):
|
|
181
|
+
uuid: str
|
|
182
|
+
name: str
|
|
183
|
+
kills: int
|
|
184
|
+
deaths: int
|
|
185
|
+
playtime: int
|
|
186
|
+
blocksPlaced: int
|
|
187
|
+
blocksBroken: int
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class SkillInfo(TypedDict):
|
|
191
|
+
uuid: str
|
|
192
|
+
skills: dict
|
|
193
|
+
"""Skill name -> level."""
|
|
194
|
+
powerLevel: int
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class CurrencyBalance(TypedDict):
|
|
198
|
+
uuid: str
|
|
199
|
+
name: Optional[str]
|
|
200
|
+
currency: str
|
|
201
|
+
balance: float
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class CurrencyTopEntry(TypedDict):
|
|
205
|
+
uuid: str
|
|
206
|
+
name: str
|
|
207
|
+
balance: float
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class LeaderboardSourceInfo(TypedDict):
|
|
211
|
+
id: str
|
|
212
|
+
displayName: str
|
|
213
|
+
available: bool
|
|
214
|
+
onlineOnly: bool
|
|
215
|
+
"""True if this source can only rank currently-online players (e.g. mcMMO)."""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class LeaderboardEntry(TypedDict):
|
|
219
|
+
uuid: str
|
|
220
|
+
name: str
|
|
221
|
+
value: float
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class NetworkServerStatus(TypedDict):
|
|
225
|
+
id: str
|
|
226
|
+
label: str
|
|
227
|
+
online: bool
|
|
228
|
+
server: Optional[ServerInfo]
|
|
229
|
+
players: List[OnlinePlayer]
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class NetworkPlayersServerEntry(TypedDict):
|
|
233
|
+
id: str
|
|
234
|
+
label: str
|
|
235
|
+
online: bool
|
|
236
|
+
players: List[OnlinePlayer]
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
class NetworkPlayersResponse(TypedDict):
|
|
240
|
+
total: int
|
|
241
|
+
servers: List[NetworkPlayersServerEntry]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class NetworkFindPlayerResponse(TypedDict, total=False):
|
|
245
|
+
found: bool
|
|
246
|
+
server: str
|
|
247
|
+
player: OnlinePlayer
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class NetworkHealthServerEntry(TypedDict, total=False):
|
|
251
|
+
id: str
|
|
252
|
+
label: str
|
|
253
|
+
online: bool
|
|
254
|
+
tps: Any
|
|
255
|
+
health: Any
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
class NetworkHealthResponse(TypedDict):
|
|
259
|
+
servers: List[NetworkHealthServerEntry]
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
NetworkBroadcastResponse = dict
|
|
263
|
+
"""Per-server "success" | "error" result, keyed by network server ID."""
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
class NetworkHubNode(TypedDict):
|
|
267
|
+
id: str
|
|
268
|
+
label: str
|
|
269
|
+
online: bool
|
|
270
|
+
tps: str
|
|
271
|
+
onlinePlayers: int
|
|
272
|
+
maxPlayers: int
|
|
273
|
+
health: Any
|
|
274
|
+
"""Opaque payload the backend reported in its last heartbeat."""
|
|
275
|
+
lastHeartbeatAt: int
|
|
276
|
+
"""Unix epoch ms of the last heartbeat received, or 0 if never."""
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
class NetworkHubStatusResponse(TypedDict):
|
|
280
|
+
network: List[NetworkHubNode]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
class NetworkHubPlayer(TypedDict, total=False):
|
|
284
|
+
uuid: str
|
|
285
|
+
name: str
|
|
286
|
+
server: str
|
|
287
|
+
"""Backend server ID the player is currently connected to, if known."""
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
class NetworkHubPlayersResponse(TypedDict):
|
|
291
|
+
total: int
|
|
292
|
+
players: List[NetworkHubPlayer]
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
class NetworkHubFindPlayerResponse(TypedDict, total=False):
|
|
296
|
+
found: bool
|
|
297
|
+
player: NetworkHubPlayer
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
class NetworkHubBroadcastResponse(TypedDict):
|
|
301
|
+
delivered: int
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
Gamemode = Literal["survival", "creative", "adventure", "spectator"]
|
|
305
|
+
Weather = Literal["clear", "rain", "thunder"]
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: noxaeapi-sdk
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Typed Python SDK for the NoxAeApi REST + WebSocket API (Fabric, Bukkit/Spigot/Paper) and the NoxAeApi-Velocity network hub
|
|
5
|
+
Project-URL: Homepage, https://github.com/WumXPro/noxaeapi-sdk
|
|
6
|
+
Project-URL: Documentation, https://noxapi.noxlydev.xyz
|
|
7
|
+
Project-URL: Repository, https://github.com/WumXPro/noxaeapi-sdk
|
|
8
|
+
Project-URL: Issues, https://github.com/WumXPro/noxaeapi-sdk/issues
|
|
9
|
+
Author: WumX Labs
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: bukkit,fabric,minecraft,noxaeapi,noxlydev,paper,rest-client,sdk,spigot,velocity,wumx
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Games/Entertainment
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Provides-Extra: dev
|
|
28
|
+
Requires-Dist: mypy>=1.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
30
|
+
Provides-Extra: ws
|
|
31
|
+
Requires-Dist: websocket-client>=1.6; extra == 'ws'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# noxaeapi-sdk
|
|
35
|
+
|
|
36
|
+
Typed Python SDK for [NoxAeApi](https://github.com/WumXPro/noxaeapi-sdk), a REST + WebSocket API plugin/mod for Minecraft servers — ships for both Fabric and Bukkit/Spigot/PaperMC. The REST surface is identical across platforms, so this SDK works against either without any platform-specific configuration.
|
|
37
|
+
|
|
38
|
+
Zero required runtime dependencies — uses only the Python standard library (`urllib`) for HTTP. The realtime socket needs one optional extra.
|
|
39
|
+
|
|
40
|
+
📖 **Docs:** [noxapi.noxlydev.xyz](https://noxapi.noxlydev.xyz)
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install noxaeapi-sdk
|
|
46
|
+
|
|
47
|
+
# with realtime console/event socket support:
|
|
48
|
+
pip install "noxaeapi-sdk[ws]"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Usage
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from noxaeapi_sdk import NoxAeApiClient
|
|
55
|
+
|
|
56
|
+
client = NoxAeApiClient(base_url="http://localhost:8080", api_key="your-api-key")
|
|
57
|
+
|
|
58
|
+
players = client.players.list()
|
|
59
|
+
balance = client.economy.get_balance(players[0]["uuid"])
|
|
60
|
+
client.server.broadcast("Hello from the SDK!")
|
|
61
|
+
|
|
62
|
+
# Multi-currency (ExcellentEconomy), leaderboards, and network aggregator:
|
|
63
|
+
coins = client.economy.get_currency_balance("coins", players[0]["uuid"])
|
|
64
|
+
top = client.leaderboards.get_top("mcmmo-power", limit=10)
|
|
65
|
+
network = client.network.status_all()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### From environment variables
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
# Reads NOXAEAPI_BASE_URL and NOXAEAPI_KEY from os.environ.
|
|
72
|
+
# If you keep those in a .env file, load it yourself first (e.g. with
|
|
73
|
+
# python-dotenv) — the SDK never reads .env files or os.environ implicitly
|
|
74
|
+
# outside this method.
|
|
75
|
+
client = NoxAeApiClient.from_env()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Realtime (console tail / events)
|
|
79
|
+
|
|
80
|
+
Requires the `ws` extra (`pip install "noxaeapi-sdk[ws]"`).
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
ws = client.connect(route="console")
|
|
84
|
+
ws.on("console", lambda line: print(line))
|
|
85
|
+
ws.on("close", lambda _: print("disconnected"))
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The socket runs on a background thread and auto-reconnects with exponential backoff on unexpected disconnects.
|
|
89
|
+
|
|
90
|
+
## Error handling
|
|
91
|
+
|
|
92
|
+
All non-2xx responses raise a subclass of `NoxAeApiError`:
|
|
93
|
+
|
|
94
|
+
- `NoxAeApiUnauthorizedError` — 401, missing/invalid API key
|
|
95
|
+
- `NoxAeApiForbiddenError` — 403, key valid but not permitted for this endpoint
|
|
96
|
+
- `NoxAeApiNotFoundError` — 404
|
|
97
|
+
- `NoxAeApiRateLimitError` — 429 (SDK auto-retries these by default; raised only once retries are exhausted)
|
|
98
|
+
- `NoxAeApiServerError` — 5xx (also auto-retried by default)
|
|
99
|
+
- `NoxAeApiNetworkError` — request never completed (timeout, DNS, connection refused)
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
from noxaeapi_sdk import NoxAeApiForbiddenError
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
client.server.restart()
|
|
106
|
+
except NoxAeApiForbiddenError:
|
|
107
|
+
print("This API key isn't allowed to restart the server.")
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Request encoding
|
|
111
|
+
|
|
112
|
+
The server is a Javalin app, and most endpoints read their body with `ctx.formParam(...)` — i.e. `application/x-www-form-urlencoded` — rather than JSON. The SDK follows the same split:
|
|
113
|
+
|
|
114
|
+
- **Form-urlencoded**: everything in `economy` (including the `economy.get_currency_balance`/`.pay_currency`/etc ExcellentEconomy methods), `players`, `server` (except `luckperms`/`noxauth`), `worlds`, `plugins`, `placeholders`, and `network.broadcast`.
|
|
115
|
+
- **JSON**: `client.luckperms.*` and `client.noxauth.check_password` only — these are read server-side with `ctx.bodyAsClass(...)`.
|
|
116
|
+
- **N/A (GET only)**: `client.leaderboards.*` and most of `client.network.*` are read-only.
|
|
117
|
+
|
|
118
|
+
If you're extending the SDK, check which encoding the corresponding Javalin handler uses before wiring up a new method, and pass `form=True` to `http.request(...)` if it's form-urlencoded (this is also the more common case). Getting this wrong won't raise a type error — the request just silently sends the wrong content type and the server won't see the field.
|
|
119
|
+
|
|
120
|
+
## Optional modules
|
|
121
|
+
|
|
122
|
+
Some modules only work depending on the target server's setup:
|
|
123
|
+
|
|
124
|
+
- `client.luckperms.*` — requires the LuckPerms mod to be loaded on the server
|
|
125
|
+
- `client.noxauth.*` — requires `noxauth.enabled: true` in the server's `noxaeapi-config.yml`
|
|
126
|
+
- `client.economy.get_currency_balance()`/`.pay_currency()`/`.debit_currency()`/`.set_currency_balance()`/`.get_currency_top()`/`.list_currencies()` — requires ExcellentEconomy (raises a 424 error if it isn't installed)
|
|
127
|
+
- `client.network.*` — requires `network.enabled: true` with at least one backend server configured in the server's config
|
|
128
|
+
|
|
129
|
+
Calling these against a server without the corresponding feature enabled will fail (typically 404).
|
|
130
|
+
|
|
131
|
+
## NoxAeApi-Velocity network hub
|
|
132
|
+
|
|
133
|
+
If your network runs the **NoxAeApi-Velocity** proxy plugin, use `NoxAeApiNetworkHubClient` instead of (or alongside) `NoxAeApiClient` — point it at the hub's own REST port, not a backend server's port. Backend servers push register/heartbeat updates to the hub over WebSocket, so hub calls answer from its in-memory registry rather than fanning out live requests the way `client.network.*` above does — and the response shapes differ accordingly (e.g. `players()` is one flat proxy-wide list, not a per-backend breakdown, and there's no hub equivalent of `network/health` — see each node's `health` field in `status_all()`/`status_by_id()` instead).
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from noxaeapi_sdk import NoxAeApiNetworkHubClient
|
|
137
|
+
|
|
138
|
+
hub = NoxAeApiNetworkHubClient(base_url="http://localhost:9090", api_key="your-hub-key") # the hub's api-port, not a backend's port
|
|
139
|
+
|
|
140
|
+
status = hub.network.status_all()
|
|
141
|
+
players = hub.network.players()
|
|
142
|
+
found = hub.network.find_player(players["players"][0]["uuid"]) if players["players"] else None
|
|
143
|
+
hub.network.broadcast("Hello from the hub!")
|
|
144
|
+
|
|
145
|
+
# Reach a specific backend's own REST routes through the hub:
|
|
146
|
+
hub.network.forward("survival", "POST", "server/exec", body={"command": "say hi"}, form=True)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Or from environment variables (`NOXAEAPI_HUB_BASE_URL` / `NOXAEAPI_HUB_KEY`, kept separate from `NoxAeApiClient.from_env()`'s `NOXAEAPI_*` vars so a process can hold both clients at once):
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
hub = NoxAeApiNetworkHubClient.from_env()
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Configuration
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from noxaeapi_sdk import NoxAeApiClient, RetryOptions
|
|
159
|
+
|
|
160
|
+
client = NoxAeApiClient(
|
|
161
|
+
base_url="https://mc.example.com",
|
|
162
|
+
api_key="...",
|
|
163
|
+
timeout=10.0, # per-request timeout in seconds, default 10
|
|
164
|
+
retry=RetryOptions(
|
|
165
|
+
attempts=3, # total attempts including the first, default 3
|
|
166
|
+
base_delay_ms=300,
|
|
167
|
+
max_delay_ms=5000,
|
|
168
|
+
),
|
|
169
|
+
# retry=False, # disable retries entirely
|
|
170
|
+
headers={"X-Extra": "..."},
|
|
171
|
+
)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
MIT
|