hungerlib 4.24.2__tar.gz → 4.25.dev4__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.
- {hungerlib-4.24.2/src/hungerlib.egg-info → hungerlib-4.25.dev4}/PKG-INFO +1 -1
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/pyproject.toml +1 -1
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/bridgeclient.py +150 -10
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/servers/minecraft.py +19 -21
- {hungerlib-4.24.2 → hungerlib-4.25.dev4/src/hungerlib.egg-info}/PKG-INFO +1 -1
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/LICENSE +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/README.md +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/setup.cfg +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/__init__.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/__init__.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/backups.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/command.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/databases.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/filemanager.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/schedule.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/api/startup.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/configloader.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/email/__init__.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/email/client.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/email/email_templates/restart.html +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/email/manager.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/email/message.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/messagerouter.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/panel.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/servers/__init__.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/servers/generic.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/utils/__init__.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/utils/buffer.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/utils/convert.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/utils/exceptions.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/utils/time.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/utils/utils.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/validator.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/webhook/__init__.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/webhook/client.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib/webhook/server.py +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib.egg-info/SOURCES.txt +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib.egg-info/dependency_links.txt +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib.egg-info/requires.txt +0 -0
- {hungerlib-4.24.2 → hungerlib-4.25.dev4}/src/hungerlib.egg-info/top_level.txt +0 -0
|
@@ -1,19 +1,163 @@
|
|
|
1
1
|
import time
|
|
2
|
+
import threading
|
|
3
|
+
import re
|
|
2
4
|
import requests
|
|
3
5
|
from .utils.exceptions import HungerBridgeError, InvalidLevelError, InvalidModeError
|
|
4
6
|
|
|
5
7
|
|
|
8
|
+
class Stream:
|
|
9
|
+
'''Streaming wrapper for HungerBridge /stream/logs SSE endpoint.'''
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
base_url: str,
|
|
13
|
+
headers: dict,
|
|
14
|
+
history_handler=None,
|
|
15
|
+
newline_handler=None
|
|
16
|
+
):
|
|
17
|
+
self.url = base_url.rstrip('/') + '/stream/logs'
|
|
18
|
+
self.headers = headers
|
|
19
|
+
|
|
20
|
+
self.history_handler = history_handler or self._default_history_handler
|
|
21
|
+
self.newline_handler = newline_handler or self._default_newline_handler
|
|
22
|
+
|
|
23
|
+
self.raw_stream = []
|
|
24
|
+
self.sanitized_stream = []
|
|
25
|
+
self.timestamped_stream = {}
|
|
26
|
+
|
|
27
|
+
self._thread = None
|
|
28
|
+
self._stop_event = None
|
|
29
|
+
self._session = None
|
|
30
|
+
|
|
31
|
+
def _default_history_handler(self, historic_lines: list):
|
|
32
|
+
for line in historic_lines:
|
|
33
|
+
clean = self.sanitize(line)
|
|
34
|
+
ts = self.extractTimestamp(clean)
|
|
35
|
+
self.raw_stream.append(line)
|
|
36
|
+
self.sanitized_stream.append(clean)
|
|
37
|
+
if ts is not None:
|
|
38
|
+
self.timestamped_stream[ts] = clean
|
|
39
|
+
|
|
40
|
+
def _default_newline_handler(self, line: str):
|
|
41
|
+
clean = self.sanitize(line)
|
|
42
|
+
ts = self.extractTimestamp(clean)
|
|
43
|
+
self.raw_stream.append(line)
|
|
44
|
+
self.sanitized_stream.append(clean)
|
|
45
|
+
if ts is not None:
|
|
46
|
+
self.timestamped_stream[ts] = clean
|
|
47
|
+
|
|
48
|
+
def connect(self, keepalive: int = 15):
|
|
49
|
+
if self._thread and self._thread.is_alive():
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
self._stop_event = threading.Event()
|
|
53
|
+
self._session = requests.Session()
|
|
54
|
+
|
|
55
|
+
def _run():
|
|
56
|
+
history_phase = True
|
|
57
|
+
history_lines = []
|
|
58
|
+
history_deadline = time.time() + keepalive
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with self._session.get(
|
|
62
|
+
self.url,
|
|
63
|
+
headers=self.headers,
|
|
64
|
+
stream=True
|
|
65
|
+
) as r:
|
|
66
|
+
if not r.ok:
|
|
67
|
+
raise HungerBridgeError(f'HungerBridge error {r.status_code}: {r.text}')
|
|
68
|
+
|
|
69
|
+
for raw in r.iter_lines(decode_unicode=True):
|
|
70
|
+
if self._stop_event.is_set():
|
|
71
|
+
break
|
|
72
|
+
if not raw:
|
|
73
|
+
continue
|
|
74
|
+
if not raw.startswith('data:'):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
line = raw[len('data:'):].lstrip()
|
|
78
|
+
|
|
79
|
+
if history_phase:
|
|
80
|
+
history_lines.append(line)
|
|
81
|
+
if time.time() >= history_deadline:
|
|
82
|
+
try:
|
|
83
|
+
self.history_handler(history_lines)
|
|
84
|
+
except Exception:
|
|
85
|
+
pass
|
|
86
|
+
history_phase = False
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
self.newline_handler(line)
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
except Exception as e:
|
|
95
|
+
raise HungerBridgeError(f'Log stream failed: {e}')
|
|
96
|
+
finally:
|
|
97
|
+
if self._session:
|
|
98
|
+
try:
|
|
99
|
+
self._session.close()
|
|
100
|
+
except Exception:
|
|
101
|
+
pass
|
|
102
|
+
self._thread = None
|
|
103
|
+
self._session = None
|
|
104
|
+
self._stop_event = None
|
|
105
|
+
|
|
106
|
+
self._thread = threading.Thread(
|
|
107
|
+
target=_run,
|
|
108
|
+
name='HungerBridgeStream',
|
|
109
|
+
daemon=True
|
|
110
|
+
)
|
|
111
|
+
self._thread.start()
|
|
112
|
+
|
|
113
|
+
def disconnect(self):
|
|
114
|
+
if self._stop_event:
|
|
115
|
+
self._stop_event.set()
|
|
116
|
+
if self._session:
|
|
117
|
+
try:
|
|
118
|
+
self._session.close()
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
self._thread = None
|
|
122
|
+
self._session = None
|
|
123
|
+
self._stop_event = None
|
|
124
|
+
|
|
125
|
+
def isConnected(self) -> bool: return self._thread is not None and self._thread.is_alive()
|
|
126
|
+
|
|
127
|
+
def getRaw(self) -> list: return list(self.raw_stream)
|
|
128
|
+
def getSanitized(self) -> list: return list(self.sanitized_stream)
|
|
129
|
+
def getTimestamped(self) -> dict: return dict(self.timestamped_stream)
|
|
130
|
+
|
|
131
|
+
@staticmethod
|
|
132
|
+
def sanitize(line: str) -> str:
|
|
133
|
+
ansi_re = re.compile(r'\x1b\[[0-9;]*m')
|
|
134
|
+
return ansi_re.sub('', line).rstrip
|
|
135
|
+
|
|
136
|
+
@staticmethod
|
|
137
|
+
def extractTimestamp(line: str):
|
|
138
|
+
m = re.match(r'\[([0-9]{2}:[0-9]{2}:[0-9]{2})\]', line)
|
|
139
|
+
if not m:
|
|
140
|
+
return None
|
|
141
|
+
return m.group(1)
|
|
142
|
+
|
|
143
|
+
|
|
6
144
|
class BridgeClient:
|
|
7
|
-
'''
|
|
8
|
-
|
|
9
|
-
'''
|
|
10
|
-
def __init__(self, url: str, token: str):
|
|
145
|
+
'''Python client for the HungerBridge v2 API'''
|
|
146
|
+
def __init__(self, url: str, token: str, history_handler=None, newline_handler=None):
|
|
11
147
|
self.base = url.rstrip('/') + '/v2/'
|
|
12
148
|
self.headers = {
|
|
13
149
|
'X-Auth-Key': token,
|
|
14
150
|
'Content-Type': 'application/json'
|
|
15
151
|
}
|
|
16
152
|
|
|
153
|
+
self.stream = Stream(
|
|
154
|
+
base_url=self.base,
|
|
155
|
+
headers=self.headers,
|
|
156
|
+
|
|
157
|
+
history_handler=history_handler,
|
|
158
|
+
newline_handler=newline_handler
|
|
159
|
+
)
|
|
160
|
+
|
|
17
161
|
# internal helpers
|
|
18
162
|
def _post(self, path: str, payload):
|
|
19
163
|
r = requests.post(self.base + path, headers=self.headers, json=payload)
|
|
@@ -87,9 +231,7 @@ class BridgeClient:
|
|
|
87
231
|
return None
|
|
88
232
|
|
|
89
233
|
def log(self, message: str, level: str = 'info') -> dict:
|
|
90
|
-
'''
|
|
91
|
-
Logs a message to the server console
|
|
92
|
-
'''
|
|
234
|
+
'''Logs a message to the server console'''
|
|
93
235
|
valid_levels = ['info', 'warn', 'error', None]
|
|
94
236
|
if level not in valid_levels:
|
|
95
237
|
raise InvalidLevelError(f'\'{level}\' is not a valid log level')
|
|
@@ -106,9 +248,7 @@ class BridgeClient:
|
|
|
106
248
|
|
|
107
249
|
# convenience getters
|
|
108
250
|
def getPing(self) -> int:
|
|
109
|
-
'''
|
|
110
|
-
Round-trip latency (ms) measured client-side.
|
|
111
|
-
'''
|
|
251
|
+
'''Round-trip latency (ms) measured client-side.'''
|
|
112
252
|
start = time.time()
|
|
113
253
|
self.v2_ping()
|
|
114
254
|
end = time.time()
|
|
@@ -16,8 +16,11 @@ class MinecraftServer(GenericServer):
|
|
|
16
16
|
server_id: str,
|
|
17
17
|
server_domain: str,
|
|
18
18
|
server_port: int,
|
|
19
|
+
|
|
19
20
|
bridge_url: str,
|
|
20
21
|
bridge_token: str,
|
|
22
|
+
history_handler=None,
|
|
23
|
+
newline_handler=None,
|
|
21
24
|
):
|
|
22
25
|
super().__init__(
|
|
23
26
|
name,
|
|
@@ -30,18 +33,17 @@ class MinecraftServer(GenericServer):
|
|
|
30
33
|
self.server_port = server_port
|
|
31
34
|
|
|
32
35
|
# HungerBridge client
|
|
33
|
-
self.bridge = BridgeClient(bridge_url, bridge_token)
|
|
36
|
+
self.bridge = BridgeClient(bridge_url, bridge_token, history_handler=history_handler, newline_handler=newline_handler)
|
|
34
37
|
|
|
35
38
|
|
|
36
|
-
# basic
|
|
37
|
-
|
|
38
|
-
def bridge_version(self) -> str | None:
|
|
39
|
+
# basic getter methods
|
|
40
|
+
def getBridgeVersion(self) -> str | None:
|
|
39
41
|
return self.bridge.getVersion()
|
|
40
|
-
|
|
41
|
-
def
|
|
42
|
+
|
|
43
|
+
def getVersion(self) -> str | None:
|
|
42
44
|
return self.bridge.getMinecraftVersion()
|
|
43
|
-
|
|
44
|
-
def
|
|
45
|
+
|
|
46
|
+
def getPlatform(self) -> str | None:
|
|
45
47
|
return self.bridge.getPlatform()
|
|
46
48
|
|
|
47
49
|
|
|
@@ -54,15 +56,7 @@ class MinecraftServer(GenericServer):
|
|
|
54
56
|
else:
|
|
55
57
|
raise InvalidModeError(f"Invalid mode: '{mode}'")
|
|
56
58
|
|
|
57
|
-
|
|
58
|
-
def player_count(self) -> int:
|
|
59
|
-
return self.getPlayers(mode='count') or 0
|
|
60
|
-
@property
|
|
61
|
-
def player_list(self) -> list[str]:
|
|
62
|
-
return self.getPlayers('list') or []
|
|
63
|
-
|
|
64
|
-
@property
|
|
65
|
-
def max_players(self) -> int:
|
|
59
|
+
def getMaxPlayers(self) -> int:
|
|
66
60
|
'''
|
|
67
61
|
Runs the 'list' command and extracts the max player count.
|
|
68
62
|
Expected format:
|
|
@@ -96,10 +90,6 @@ class MinecraftServer(GenericServer):
|
|
|
96
90
|
return None
|
|
97
91
|
return round(value, rounding) if rounding is not None else value
|
|
98
92
|
|
|
99
|
-
@property
|
|
100
|
-
def tps(self) -> float | None:
|
|
101
|
-
return self.getTPS()
|
|
102
|
-
|
|
103
93
|
|
|
104
94
|
# commands
|
|
105
95
|
def sendConsoleCommand(
|
|
@@ -122,3 +112,11 @@ class MinecraftServer(GenericServer):
|
|
|
122
112
|
safe = message.replace('"', '\\"')
|
|
123
113
|
cmd = f'tellraw @a {{"text":"{safe}"}}'
|
|
124
114
|
return self.bridge.runCommand(cmd, show_console=True)
|
|
115
|
+
|
|
116
|
+
# stream passthrough methods
|
|
117
|
+
def connectStream(self, keepalive: int=15): self.bridge.stream.connect(keepalive)
|
|
118
|
+
def disconnectStream(self): self.bridge.stream.disconnect()
|
|
119
|
+
def isStreamConnected(self): self.bridge.stream.isConnected()
|
|
120
|
+
def getRawStream(self): self.bridge.streamgetRaw()
|
|
121
|
+
def getSanitizedStream(self): self.bridge.stream.getSanitized()
|
|
122
|
+
def getTimestampedStream(self): self.bridge.stream.getTimestamped()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|