serverwatcher 5.20__tar.gz → 5.22__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 (22) hide show
  1. {serverwatcher-5.20/src/serverwatcher.egg-info → serverwatcher-5.22}/PKG-INFO +1 -1
  2. {serverwatcher-5.20 → serverwatcher-5.22}/pyproject.toml +1 -1
  3. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/configclasses/config.py +1 -1
  4. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/configclasses/watcher.py +0 -6
  5. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/defaultconfigs/watcher.yaml +0 -4
  6. serverwatcher-5.22/src/serverwatcher/validator.py +218 -0
  7. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/watcher.py +74 -43
  8. {serverwatcher-5.20 → serverwatcher-5.22/src/serverwatcher.egg-info}/PKG-INFO +1 -1
  9. serverwatcher-5.20/src/serverwatcher/validator.py +0 -139
  10. {serverwatcher-5.20 → serverwatcher-5.22}/LICENSE +0 -0
  11. {serverwatcher-5.20 → serverwatcher-5.22}/README.md +0 -0
  12. {serverwatcher-5.20 → serverwatcher-5.22}/setup.cfg +0 -0
  13. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/__init__.py +0 -0
  14. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/__main__.py +0 -0
  15. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/configclasses/__init__.py +0 -0
  16. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/configclasses/messages.py +0 -0
  17. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/defaultconfigs/config.yaml +0 -0
  18. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher/defaultconfigs/messages.yaml +0 -0
  19. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher.egg-info/SOURCES.txt +0 -0
  20. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher.egg-info/dependency_links.txt +0 -0
  21. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher.egg-info/requires.txt +0 -0
  22. {serverwatcher-5.20 → serverwatcher-5.22}/src/serverwatcher.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 5.20
3
+ Version: 5.22
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: GPL-3.0
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "serverwatcher"
10
- version = "5.20"
10
+ version = "5.22"
11
11
  description = "A HungerLib-powered Minecraft server automation engine."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.14"
@@ -1,4 +1,4 @@
1
- from hungerlib import datamap, datamap_api
1
+ from mapres import datamap, datamap_api
2
2
 
3
3
  @datamap(syntax=datamap_api.braces, mode='config')
4
4
  class GlobalConfig:
@@ -7,9 +7,6 @@ class WatcherConfig:
7
7
 
8
8
  watch_interval: int = 'watch_interval'
9
9
 
10
- schedule_control: bool = 'schedule_control.enabled'
11
- restart_soon_id: int = 'schedule_control.restart_soon_id'
12
-
13
10
  threshold_ram: int = 'thresholds.ram'
14
11
  threshold_cpu: int = 'thresholds.cpu'
15
12
  threshold_uptime: int = 'thresholds.uptime'
@@ -39,9 +36,6 @@ class WatcherConfig:
39
36
  class fallbacks:
40
37
  watch_interval = 300
41
38
 
42
- schedule_control = False
43
- restart_soon_id = 0
44
-
45
39
  threshold_ram = 6
46
40
  threshold_cpu = 150
47
41
  threshold_uptime = 12
@@ -1,9 +1,5 @@
1
1
  watch_interval: 300 # in seconds
2
2
 
3
- schedule_control:
4
- enabled: False
5
- restart_soon_id: 0 # replace this with a real schedule id
6
-
7
3
  thresholds:
8
4
  ram: 6
9
5
  cpu: 150
@@ -0,0 +1,218 @@
1
+ import sys
2
+ from dataclasses import fields
3
+
4
+ from hungerlib import utils, loadConfig
5
+
6
+ from serverwatcher.configclasses.config import GlobalConfig
7
+ from serverwatcher.configclasses.messages import MessagesConfig
8
+ from serverwatcher.configclasses.watcher import WatcherConfig
9
+
10
+
11
+ utils.clearTerminal()
12
+ errors = []
13
+ warnings = []
14
+ defaults = []
15
+
16
+
17
+ def validate_key_types(config_obj, schema):
18
+ for f in fields(schema):
19
+ if f.name.startswith("__"):
20
+ continue
21
+
22
+ expected_type = f.type
23
+ value = getattr(config_obj, f.name, None)
24
+
25
+ # allow None (missing + no fallback) to be handled by other validators
26
+ if value is None:
27
+ continue
28
+
29
+ if not isinstance(value, expected_type):
30
+ errors.append(
31
+ f'{schema.__name__}.{f.name}: expected {expected_type.__name__}, '
32
+ f'got "{type(value).__name__}" ({value!r})'
33
+ )
34
+
35
+
36
+ def check_field(config_obj, name, allow_fallback=True):
37
+ """
38
+ Unified check for:
39
+ - missing YAML key (config.raw.<name> is None)
40
+ - fallback usage (config.<name> == config.fallbacks.<name>)
41
+ - whether fallback is allowed or not
42
+ """
43
+ raw = config_obj.raw
44
+ fb = config_obj.fallbacks
45
+
46
+ val = getattr(config_obj, name)
47
+ raw_val = getattr(raw, name)
48
+ fb_val = getattr(fb, name)
49
+
50
+ # 1) YAML key missing
51
+ if raw_val is None:
52
+ if allow_fallback:
53
+ warnings.append(f'{name}: key does not exist, using fallback "{fb_val}"')
54
+ else:
55
+ errors.append(f'{name}: key does not exist and fallback is not allowed')
56
+ return
57
+
58
+ # 2) YAML key exists but value equals fallback
59
+ if val == fb_val and not allow_fallback:
60
+ defaults.append(f'{name}: must not be left default or empty (got "{val}")')
61
+
62
+
63
+ def validate_global_config(config):
64
+ c = config
65
+
66
+ # Fallback policy:
67
+ # - NOT allowed (must be set by user): panel_url, panel_api_key,
68
+ # server_id, server_domain, bridge_token
69
+ # - Allowed: everything else
70
+
71
+ # timezone
72
+ check_field(c, "timezone")
73
+ if c.timezone == "":
74
+ errors.append('timezone: must not be empty')
75
+
76
+ # panel
77
+ check_field(c, "panel_name")
78
+ check_field(c, "panel_url", allow_fallback=False)
79
+ check_field(c, "panel_api_key", allow_fallback=False)
80
+
81
+ if c.panel_url and not (c.panel_url.startswith("http://") or c.panel_url.startswith("https://")):
82
+ errors.append(f'panel_url: must start with "http://" or "https://" (got "{c.panel_url}")')
83
+
84
+ if c.panel_api_key and not c.panel_api_key.startswith("ptlc_"):
85
+ errors.append(f'panel_api_key: must be a valid Pterodactyl client API key (got "{c.panel_api_key}")')
86
+ if c.panel_api_key and c.panel_api_key.startswith("plta_"):
87
+ errors.append(f'panel_api_key: should not be an application key (got "{c.panel_api_key}")')
88
+
89
+ # server
90
+ check_field(c, "server_name")
91
+ check_field(c, "server_id", allow_fallback=False)
92
+ check_field(c, "server_domain", allow_fallback=False)
93
+ check_field(c, "server_port")
94
+ if c.server_domain and (c.server_domain.startswith("http://") or c.server_domain.startswith("https://")):
95
+ errors.append(f'server_domain: must not contain protocol (got "{c.server_domain}")')
96
+ if c.server_port is not None and not (1 <= c.server_port <= 65535):
97
+ errors.append(f'server_port: must be 1–65535 (got "{c.server_port}")')
98
+
99
+ # tps_command
100
+ check_field(c, "tps_command")
101
+
102
+ # hungerbridge
103
+ check_field(c, "bridge_token", allow_fallback=False)
104
+ check_field(c, "bridge_port")
105
+ if c.bridge_port is not None and not (1 <= c.bridge_port <= 65535):
106
+ errors.append(f'bridge_port: must be 1–65535 (got "{c.bridge_port}")')
107
+
108
+ # logger
109
+ check_field(c, "enable_logging")
110
+ check_field(c, "logger_name")
111
+ check_field(c, "log_path")
112
+ check_field(c, "info_prefix")
113
+ check_field(c, "warn_prefix")
114
+ check_field(c, "error_prefix")
115
+
116
+ # terminal
117
+ check_field(c, "clear_terminal")
118
+ check_field(c, "handle_keyboard_interrupt")
119
+
120
+
121
+ def validate_watcher_config(watcherconfig):
122
+ c = watcherconfig
123
+ raw = c.raw
124
+ fb = c.fallbacks
125
+
126
+ # basic numeric sanity
127
+ if c.restart_wait_seconds <= 0:
128
+ errors.append(f'restart_wait_seconds: must be > 0 (got {c.restart_wait_seconds})')
129
+
130
+ if c.threshold_cpu <= 0:
131
+ errors.append(f'threshold_cpu: must be > 0 (got {c.threshold_cpu})')
132
+
133
+ if c.threshold_ram <= 0:
134
+ errors.append(f'threshold_ram: must be > 0 (got {c.threshold_ram})')
135
+
136
+ if c.threshold_tps <= 0 or c.threshold_tps > 20:
137
+ errors.append(f'threshold_tps: must be 1–20 (got {c.threshold_tps})')
138
+
139
+ # snap_minutes: must be a non-empty list of ints 0–59
140
+ if raw.snap_minutes is None:
141
+ warnings.append(f'snap_minutes: key does not exist, using fallback "{fb.snap_minutes}"')
142
+
143
+ if not isinstance(c.snap_minutes, list) or not c.snap_minutes:
144
+ errors.append(f'snap_minutes: must be a non-empty list of minute marks (got {c.snap_minutes!r})')
145
+ else:
146
+ bad = [m for m in c.snap_minutes if not isinstance(m, int) or not (0 <= m <= 59)]
147
+ if bad:
148
+ errors.append(f'snap_minutes: all values must be integers 0–59 (bad values: {bad})')
149
+
150
+
151
+ def validate_messages_config(messages):
152
+ pass
153
+ # m = messages
154
+
155
+ # for f in fields(MessagesConfig):
156
+ # if f.name.startswith("__"):
157
+ # continue
158
+ # value = getattr(m, f.name)
159
+ # if not isinstance(value, str) or value.strip() == "":
160
+ # errors.append(f'MessagesConfig.{f.name}: must be a non-empty string')
161
+
162
+
163
+ def validate_all():
164
+ config = loadConfig(GlobalConfig)
165
+ messages = loadConfig(MessagesConfig)
166
+ watcherconfig = loadConfig(WatcherConfig)
167
+
168
+ # type checks
169
+ validate_key_types(config, GlobalConfig)
170
+ validate_key_types(messages, MessagesConfig)
171
+ validate_key_types(watcherconfig, WatcherConfig)
172
+
173
+ # semantic checks
174
+ validate_global_config(config)
175
+ validate_watcher_config(watcherconfig)
176
+ validate_messages_config(messages)
177
+
178
+ # if too many critical defaults, assume "not configured at all"
179
+ critical_default_keys = [
180
+ "panel_url",
181
+ "panel_api_key",
182
+ "server_id",
183
+ "server_domain",
184
+ "bridge_token",
185
+ ]
186
+ critical_defaults_used = [
187
+ d for d in defaults
188
+ if any(d.startswith(k) for k in critical_default_keys)
189
+ ]
190
+
191
+ if len(critical_defaults_used) >= 3:
192
+ print('❌ CONFIG VALIDATION FAILED:\nIt looks like you haven\'t configured this yet! Please change these defaults:')
193
+ for d in critical_defaults_used:
194
+ print(' -', d)
195
+ sys.exit(1)
196
+
197
+ if errors or defaults:
198
+ print('❌ CONFIG VALIDATION FAILED:')
199
+ for e in errors:
200
+ print(' -', e)
201
+ for d in defaults:
202
+ print(' -', d)
203
+ if warnings:
204
+ print('\nWarnings:')
205
+ for w in warnings:
206
+ print(' -', w)
207
+ sys.exit(1)
208
+
209
+ if warnings:
210
+ print('⚠️ CONFIG VALIDATION WARNINGS:')
211
+ for w in warnings:
212
+ print(' -', w)
213
+
214
+ print('✅ All configs are valid.')
215
+
216
+
217
+ if __name__ == '__main__':
218
+ validate_all()
@@ -2,40 +2,31 @@ import os
2
2
  import time
3
3
  from zoneinfo import ZoneInfo
4
4
 
5
- from hungerlib import servers, MessageRouter, loadConfig, utils, datamap_api, mapit
5
+ from hungerlib import servers, MessageRouter, loadConfig, utils
6
+ from mapres import MapResolver, maps
6
7
 
7
8
  from serverwatcher.configclasses.config import GlobalConfig
8
9
  from serverwatcher.configclasses.messages import MessagesConfig
9
10
  from serverwatcher.configclasses.watcher import WatcherConfig
10
11
 
11
12
 
12
-
13
13
  class ServerWatcher:
14
14
  def __init__(self):
15
15
  self.config = loadConfig(GlobalConfig)
16
16
  self.messages = loadConfig(MessagesConfig)
17
17
  self.watcherconfig = loadConfig(WatcherConfig)
18
18
 
19
- datamap_api.setGlobalMaps(
20
- utils.ASCII_COLOR_MAP,
21
- utils.TIME_MAP(self.config.timezone),
22
- self.config,
23
- self.messages,
24
- self.watcherconfig,
25
- )
19
+ # resolver for internal mapping
20
+ self.resolver = MapResolver()
21
+ self.res = self.resolver.res
26
22
 
23
+ # panel + server
27
24
  self.panel = servers.Panel(
28
25
  name=self.config.panel_name,
29
26
  url=self.config.panel_url,
30
27
  api_key=self.config.panel_api_key,
31
28
  )
32
29
 
33
- self.origin = servers.Generic(
34
- name="Origin",
35
- panel=self.panel,
36
- server_id=self.config.origin_server_id
37
- )
38
-
39
30
  self.server = servers.Minecraft(
40
31
  name=self.config.server_name,
41
32
  panel=self.panel,
@@ -47,18 +38,54 @@ class ServerWatcher:
47
38
  tpsCommand=self.config.tps_command,
48
39
  )
49
40
 
50
- logger_name = mapit(self.config.logger_name, server_name=self.config.server_name)
51
-
41
+ # logger name
42
+ logger_name = self.res(
43
+ self.config.logger_name,
44
+ server_name=self.config.server_name
45
+ )
46
+
47
+ # router using mapres
52
48
  self.router = MessageRouter(
53
49
  name=logger_name,
54
50
  Servers=[self.server],
55
51
  log_path=self.config.log_path,
56
52
 
57
- origin_maps = [utils.ASCII_COLOR_MAP, utils.TIME_MAP(self.config.timezone), self.config, self.messages, self.watcherconfig],
58
- destination_maps = [utils.ASCII_COLOR_MAP, utils.TIME_MAP(self.config.timezone), self.config, self.messages, self.watcherconfig],
59
- broadcast_maps = [utils.MC_COLOR_MAP, utils.TIME_MAP(self.config.timezone), self.config, self.messages, self.watcherconfig],
60
- file_maps = [utils.STRIP_COLOR_MAP, utils.TIME_MAP(self.config.timezone), self.config, self.messages, self.watcherconfig],
61
- prefix_maps = [utils.ASCII_COLOR_MAP, utils.TIME_MAP(self.config.timezone)],
53
+ origin_maps=[
54
+ maps.ascii_colors,
55
+ maps.time_tk(self.config.timezone),
56
+ self.config,
57
+ self.messages,
58
+ self.watcherconfig
59
+ ],
60
+
61
+ destination_maps=[
62
+ maps.ascii_colors,
63
+ maps.time_tk(self.config.timezone),
64
+ self.config,
65
+ self.messages,
66
+ self.watcherconfig
67
+ ],
68
+
69
+ broadcast_maps=[
70
+ maps.mc_colors,
71
+ maps.time_tk(self.config.timezone),
72
+ self.config,
73
+ self.messages,
74
+ self.watcherconfig
75
+ ],
76
+
77
+ file_maps=[
78
+ maps.strip_colors,
79
+ maps.time_tk(self.config.timezone),
80
+ self.config,
81
+ self.messages,
82
+ self.watcherconfig
83
+ ],
84
+
85
+ prefix_maps=[
86
+ maps.ascii_colors,
87
+ maps.time_tk(self.config.timezone)
88
+ ],
62
89
 
63
90
  info_prefix=self.config.info_prefix,
64
91
  warn_prefix=self.config.warn_prefix,
@@ -72,9 +99,6 @@ class ServerWatcher:
72
99
  raise SystemExit
73
100
 
74
101
  def restart_and_wait(self):
75
- if self.watcherconfig.schedule_control:
76
- self.origin.disableSchedule(self.watcherconfig.restart_soon_id)
77
-
78
102
  self.server.restart()
79
103
  self.router.info(self.messages.restart_action_sent)
80
104
  time.sleep(self.watcherconfig.restart_wait_seconds)
@@ -93,7 +117,10 @@ class ServerWatcher:
93
117
  self.router.error(self.messages.server_failed_restart)
94
118
 
95
119
  def schedule_restart(self, minutes):
96
- info = utils.snapSchedule(minimumMinutes=minutes, snapMinutes=tuple(sorted(self.watcherconfig.snap_minutes)))
120
+ info = utils.snapSchedule(
121
+ minimumMinutes=minutes,
122
+ snapMinutes=tuple(sorted(self.watcherconfig.snap_minutes))
123
+ )
97
124
  scheduled = info["scheduled"]
98
125
 
99
126
  local_time = scheduled.astimezone(self.tz)
@@ -101,34 +128,38 @@ class ServerWatcher:
101
128
 
102
129
  self.router.broadcast(self.messages.broadcast_restart_at, time=time_str)
103
130
 
131
+ # minute callbacks
104
132
  minute_callbacks = {
105
133
  int(k.split("_")[1]): (
106
134
  lambda raw=self.messages.as_map()[k]:
107
- self.router.broadcast(mapit(raw)),
108
- self.router.origin(raw)
135
+ (self.router.broadcast(self.res(raw)),
136
+ self.router.origin(raw))
109
137
  )
110
138
  for k in self.messages.as_map()
111
139
  if k.startswith("minute_")
112
140
  }
113
141
 
142
+ # second callbacks
114
143
  second_callbacks = {
115
144
  int(k.split("_")[1]): (
116
145
  lambda raw=self.messages.as_map()[k]:
117
- self.router.broadcast(mapit(raw)),
118
- self.router.origin(raw)
146
+ (self.router.broadcast(self.res(raw)),
147
+ self.router.origin(raw))
119
148
  )
120
149
  for k in self.messages.as_map()
121
150
  if k.startswith("second_")
122
151
  }
123
152
 
124
- utils.runCountdownEvents(target_time=scheduled, minute_callbacks=minute_callbacks, second_callbacks=second_callbacks)
153
+ utils.runCountdownEvents(
154
+ target_time=scheduled,
155
+ minute_callbacks=minute_callbacks,
156
+ second_callbacks=second_callbacks
157
+ )
125
158
 
126
159
  def evaluate(self):
127
- # Lets Pterodactyl know the server is online, then clears terminal and uses user-defined startup key
128
160
  self.router.info("ServerWatcher is running!")
129
161
  utils.clearTerminal()
130
162
 
131
- # Configurable start message
132
163
  self.router.info(self.messages.startup)
133
164
 
134
165
  if not utils.validateAll(self.panel, self.server):
@@ -143,36 +174,36 @@ class ServerWatcher:
143
174
  restart_reasons = []
144
175
  no_restart_reasons = []
145
176
 
146
- if self.watcherconfig.schedule_control and self.server.getSchedule(self.watcherconfig.restart_soon_id)["is_active"]:
147
- restart_reasons.append(self.messages.reason_restart_soon)
148
- pro += self.watcherconfig.weight_restart_soon
149
-
150
177
  if snap.ram >= self.watcherconfig.threshold_ram:
151
- restart_reasons.append(mapit(self.messages.reason_ram, ram=snap.ram, threshold=self.watcherconfig.threshold_ram))
178
+ restart_reasons.append(
179
+ self.res(self.messages.reason_ram, ram=snap.ram, threshold=self.watcherconfig.threshold_ram)
180
+ )
152
181
  pro += int(round(snap.ram, 0) - (self.watcherconfig.threshold_ram - 1))
153
182
 
154
183
  if snap.cpu >= self.watcherconfig.threshold_cpu:
155
- restart_reasons.append(mapit(self.messages.reason_cpu, cpu=snap.cpu, threshold=self.watcherconfig.threshold_cpu))
184
+ restart_reasons.append(
185
+ self.res(self.messages.reason_cpu, cpu=snap.cpu, threshold=self.watcherconfig.threshold_cpu)
186
+ )
156
187
  pro += self.watcherconfig.weight_cpu
157
188
 
158
189
  if snap.uptime // 3600 >= self.watcherconfig.threshold_uptime:
159
190
  restart_reasons.append(
160
- mapit(self.messages.reason_uptime, uptime=snap.uptime_formatted, threshold=self.watcherconfig.threshold_uptime)
191
+ self.res(self.messages.reason_uptime, uptime=snap.uptime_formatted, threshold=self.watcherconfig.threshold_uptime)
161
192
  )
162
193
  pro += self.watcherconfig.weight_uptime
163
194
 
164
195
  if (snap.tps if snap.tps is not None else 20) <= self.watcherconfig.threshold_tps:
165
- restart_reasons.append(mapit(self.messages.reason_tps, tps=snap.tps, threshold=self.watcherconfig.threshold_tps))
196
+ restart_reasons.append(self.res(self.messages.reason_tps, tps=snap.tps, threshold=self.watcherconfig.threshold_tps))
166
197
  pro += self.watcherconfig.weight_tps
167
198
 
168
199
  if snap.uptime // 60 < self.watcherconfig.threshold_min_uptime:
169
- no_restart_reasons.append(mapit(self.messages.reason_low_uptime, uptime=snap.uptime_formatted))
200
+ no_restart_reasons.append(self.res(self.messages.reason_low_uptime, uptime=snap.uptime_formatted))
170
201
  anti += self.watcherconfig.weight_low_uptime
171
202
 
172
203
  if snap.players > 0:
173
204
  verb = "are" if snap.players != 1 else "is"
174
205
  plural = "players" if snap.players != 1 else "player"
175
- no_restart_reasons.append(mapit(self.messages.reason_players, verb=verb, count=snap.players, plural=plural))
206
+ no_restart_reasons.append(self.res(self.messages.reason_players, verb=verb, count=snap.players, plural=plural))
176
207
  anti += snap.players * self.watcherconfig.weight_per_player
177
208
 
178
209
  if restart_reasons:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 5.20
3
+ Version: 5.22
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: GPL-3.0
@@ -1,139 +0,0 @@
1
- import sys
2
- from dataclasses import fields
3
- from hungerlib import utils, loadConfig
4
-
5
- from serverwatcher.configclasses.config import GlobalConfig
6
- from serverwatcher.configclasses.messages import MessagesConfig
7
- from serverwatcher.configclasses.watcher import WatcherConfig
8
-
9
- utils.clearTerminal()
10
-
11
- def deep_get_attr(obj, dotted):
12
- parts = dotted.split('.')
13
- cur = obj
14
- for p in parts:
15
- if not hasattr(cur, p):
16
- return None
17
- cur = getattr(cur, p)
18
- return cur
19
-
20
- def validate_type(name, value, expected_type, errors):
21
- if not isinstance(value, expected_type):
22
- errors.append(f'{name}: expected {expected_type.__name__}, got {type(value).__name__}')
23
-
24
- def validate_positive(name, value, errors):
25
- if isinstance(value, (int, float)) and value < 0:
26
- errors.append(f'{name}: must be >= 0 (got {value})')
27
-
28
- def validate_nonempty(name, value, errors):
29
- if isinstance(value, str) and value.strip() == '':
30
- errors.append(f'{name}: cannot be empty')
31
-
32
- def validate_dataclass(config_obj, schema, errors):
33
- for f in fields(schema):
34
- name = f.name
35
- expected_type = f.type
36
- value = deep_get_attr(config_obj, name)
37
-
38
- validate_type(name, value, expected_type, errors)
39
-
40
- if expected_type is str:
41
- validate_nonempty(name, value, errors)
42
-
43
- if expected_type in (int, float):
44
- validate_positive(name, value, errors)
45
-
46
- def validate_global_config(config, errors):
47
- if config.server_port <= 0 or config.server_port > 65535:
48
- errors.append(f'server_port: must be 1–65535 (got {config.server_port})')
49
-
50
- if config.bridge_port <= 0 or config.bridge_port > 65535:
51
- errors.append(f'bridge_port: must be 1–65535 (got {config.bridge_port})')
52
-
53
- if len(config.bridge_token) < 8:
54
- errors.append(f'bridge_token: must be 8 or more characters (got {config.bridge_token}, {len(config.bridge_token)} characters)')
55
-
56
- def validate_watcher_config(watcherconfig, errors):
57
- if watcherconfig.restart_wait_seconds < 1:
58
- errors.append(f'restart_wait_seconds: must be > 0 (got {watcherconfig.restart_wait_seconds})')
59
-
60
- if watcherconfig.threshold_cpu <= 0:
61
- errors.append(f'threshold_cpu: must be > 0 (got {watcherconfig.threshold_cpu})')
62
-
63
- if watcherconfig.threshold_ram <= 0:
64
- errors.append(f'threshold_ram: must be > 0 (got {watcherconfig.threshold_ram})')
65
-
66
- if watcherconfig.threshold_tps <= 0 or watcherconfig.threshold_tps > 20:
67
- errors.append(f'threshold_tps: must be 1–20 (got {watcherconfig.threshold_tps})')
68
-
69
- def validate_messages_config(messages, errors):
70
- for f in fields(MessagesConfig):
71
- value = getattr(messages, f.name)
72
- if isinstance(value, str) and '{prefix}' not in value:
73
- pass
74
-
75
-
76
- # fallback detection
77
- def ensure_no_global_defaults(config, defaults):
78
- fb = config.fallbacks
79
-
80
- if config.panel_url == fb.panel_url:
81
- defaults.append('panel_url')
82
-
83
- if config.panel_api_key == fb.panel_api_key:
84
- defaults.append('panel_api_key')
85
-
86
- if config.server_id == fb.server_id:
87
- defaults.append('server_id')
88
-
89
- if config.server_domain == fb.server_domain:
90
- defaults.append('server_domain')
91
-
92
- if config.bridge_token == fb.bridge_token:
93
- defaults.append('bridge_token')
94
-
95
- def ensure_no_watcher_defaults(watcherconfig, defaults):
96
- fb = watcherconfig.fallbacks
97
-
98
- if watcherconfig.schedule_control and watcherconfig.restart_soon_id == fb.restart_soon_id:
99
- defaults.append('restart_soon_id')
100
-
101
- # main validation entrypoint
102
- def validate_all():
103
- errors = []
104
- defaults = []
105
-
106
- config = loadConfig(GlobalConfig)
107
- messages = loadConfig(MessagesConfig)
108
- watcher = loadConfig(WatcherConfig)
109
-
110
- validate_dataclass(config, GlobalConfig, errors)
111
- validate_dataclass(messages, MessagesConfig, errors)
112
- validate_dataclass(watcher, WatcherConfig, errors)
113
-
114
- validate_global_config(config, errors)
115
- validate_messages_config(messages, errors)
116
- validate_watcher_config(watcher, errors)
117
-
118
- ensure_no_global_defaults(config, defaults)
119
- ensure_no_watcher_defaults(watcher, defaults)
120
-
121
- if len(defaults) >= 5:
122
- print('❌ CONFIG VALIDATION FAILED:\nIt looks like you haven\'t configured this yet! Please change these defaults:')
123
- for d in defaults:
124
- print(' -', d)
125
- sys.exit(1)
126
-
127
- if errors or defaults:
128
- print('❌ CONFIG VALIDATION FAILED:')
129
- for e in errors:
130
- print(' -', e)
131
- for d in defaults:
132
- print(' -', d, ': must not be left default')
133
- sys.exit(1)
134
-
135
- print('✅ All configs are valid.')
136
-
137
-
138
- if __name__ == '__main__':
139
- validate_all()
File without changes
File without changes
File without changes