serverwatcher 2.2.4__tar.gz → 3.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 2.2.4
3
+ Version: 3.0
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: MIT
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
7
7
 
8
8
  [project]
9
9
  name = "serverwatcher"
10
- version = "2.2.4"
10
+ version = "3.0"
11
11
  description = "A HungerLib-powered Minecraft server automation engine."
12
12
  readme = "README.md"
13
13
  requires-python = ">=3.10"
@@ -9,6 +9,7 @@ except PackageNotFoundError:
9
9
  from .watcher import ServerWatcher
10
10
  from .config import WatcherConfig
11
11
  from .messages import WatcherMessages
12
+ from .schema import validate_config_schema, validate_messages_schema
12
13
 
13
14
 
14
15
  __all__ = [
@@ -16,4 +17,6 @@ __all__ = [
16
17
  'ServerWatcher',
17
18
  'WatcherConfig',
18
19
  'WatcherMessages',
20
+ 'validate_config_schema',
21
+ 'validate_messages_schema',
19
22
  ]
@@ -31,3 +31,9 @@ class WatcherConfig:
31
31
  restart_wait_seconds: int = 45
32
32
  restart_online_timeout: int = 120
33
33
  restart_online_interval: int = 2
34
+
35
+ # logger / misc
36
+ logger_name_template: str = "ServerWatcher-{server_name}"
37
+ log_path: str = "/home/container/logs/"
38
+ console_backspaces: int = 8
39
+ timezone: str = "America/Chicago"
@@ -0,0 +1,45 @@
1
+ from typing import Dict, Any, List
2
+
3
+ from hungerlib.addons import validate_required_keys
4
+
5
+
6
+ def validate_config_schema(raw: Dict[str, Any]) -> list[str]:
7
+ errors: List[str] = []
8
+
9
+ # top-level sections
10
+ errors += validate_required_keys(raw, ["panel", "origin", "server", "watcher"], "root")
11
+
12
+ panel = raw.get("panel", {})
13
+ origin = raw.get("origin", {})
14
+ server = raw.get("server", {})
15
+ watcher = raw.get("watcher", {})
16
+
17
+ errors += validate_required_keys(panel, ["name", "url", "api_key"], "panel")
18
+ errors += validate_required_keys(origin, ["server_id"], "origin")
19
+ errors += validate_required_keys(
20
+ server,
21
+ ["name", "server_id", "domain", "port", "rcon_port", "rcon_password", "tps_command"],
22
+ "server",
23
+ )
24
+
25
+ # simple sanity checks (optional but nice)
26
+ if "port" in server and not isinstance(server["port"], int):
27
+ errors.append("[server] 'port' must be an integer")
28
+ if "rcon_port" in server and not isinstance(server["rcon_port"], int):
29
+ errors.append("[server] 'rcon_port' must be an integer")
30
+
31
+ # watcher numeric sanity
32
+ if "ram_threshold" in watcher and watcher["ram_threshold"] <= 0:
33
+ errors.append("[watcher] 'ram_threshold' must be > 0")
34
+ if "cpu_threshold" in watcher and watcher["cpu_threshold"] <= 0:
35
+ errors.append("[watcher] 'cpu_threshold' must be > 0")
36
+
37
+ return errors
38
+
39
+
40
+ def validate_messages_schema(raw: Dict[str, Any]) -> list[str]:
41
+ # you can make this as strict as you want; for now just ensure prefix exists
42
+ errors: List[str] = []
43
+ if "prefix" not in raw:
44
+ errors.append("[messages] Missing required key: 'prefix'")
45
+ return errors
@@ -12,11 +12,17 @@ from hungerlib.addons import (
12
12
  runCountdownEvents,
13
13
  ensure_yaml,
14
14
  load_yaml,
15
- map_to_dataclass
15
+ map_to_dataclass,
16
+ write_default_yaml,
16
17
  )
17
18
 
18
19
  from .config import WatcherConfig
19
20
  from .messages import WatcherMessages
21
+ from .schema import validate_config_schema, validate_messages_schema
22
+
23
+
24
+ CONFIG_PATH = "config.yaml"
25
+ MESSAGES_PATH = "messages.yaml"
20
26
 
21
27
 
22
28
  DEFAULT_CONFIG = {
@@ -38,38 +44,98 @@ DEFAULT_CONFIG = {
38
44
  "tps_command": "ticks",
39
45
  },
40
46
  "watcher": {},
41
- "messages": {},
47
+ }
48
+
49
+
50
+ DEFAULT_MESSAGES = {
51
+ "prefix": "<aqua>[Server Watcher]",
52
+ "broadcast_restart_at": "{prefix} The server will restart at {time} CDT.",
53
+ "broadcast_minute": {
54
+ 120: "{prefix} Restart in 2 hours!",
55
+ 60: "{prefix} Restart in 1 hour!",
56
+ 45: "{prefix} Restart in 45 minutes!",
57
+ 30: "{prefix} Restart in 30 minutes!",
58
+ 15: "{prefix} Restart in 15 minutes!",
59
+ 5: "{prefix} Restart in 5 minutes!",
60
+ 1: "{prefix} Restart in 1 minute!",
61
+ },
62
+ "broadcast_second": {
63
+ s: "{prefix} Restart in " + str(s) + " seconds!"
64
+ for s in range(10, 0, -1)
65
+ },
66
+ "log_start": "ServerWatcher is running!",
67
+ "log_validation_fail": "Validation FAILED. Make sure you set up config.yaml.",
68
+ "log_validation_ok": "All validation checks succeeded.",
69
+ "log_immediate_restart": "Restarting immediately.",
70
+ "log_no_restart": "The server does not need to restart.",
71
+ "log_scheduled": "Restart needed, but anti-restart factors outweigh it.",
72
+ "log_gap_low": "Gap {gap}. Scheduling restart in 2 hours.",
73
+ "log_gap_high": "Gap {gap}. Scheduling restart in 1 hour.",
74
+ "reason_restart_soon": "The server is set to restart soon",
75
+ "reason_ram": "RAM usage ({ram}) is higher than {threshold} GB",
76
+ "reason_cpu": "CPU usage ({cpu}) is higher than {threshold}%",
77
+ "reason_uptime": "Uptime {uptime} exceeds {threshold}h",
78
+ "reason_tps": "TPS {tps} is lower than {threshold}",
79
+ "reason_low_uptime": "Uptime {uptime} is shorter than 30m",
80
+ "reason_players": "There {verb} {count} {plural} online",
42
81
  }
43
82
 
44
83
 
45
84
  class ServerWatcher:
46
- def __init__(self, config_path: str = "config.yaml"):
47
- # ensure config exists, then load it
85
+ @staticmethod
86
+ def generate_default_files(
87
+ config_path: str = CONFIG_PATH,
88
+ messages_path: str = MESSAGES_PATH,
89
+ overwrite: bool = False,
90
+ ):
91
+ write_default_yaml(config_path, DEFAULT_CONFIG, overwrite=overwrite)
92
+ write_default_yaml(messages_path, DEFAULT_MESSAGES, overwrite=overwrite)
93
+
94
+ def __init__(
95
+ self,
96
+ config_path: str = CONFIG_PATH,
97
+ messages_path: str = MESSAGES_PATH,
98
+ ):
99
+ # ensure files exist
48
100
  ensure_yaml(config_path, DEFAULT_CONFIG)
49
- raw = load_yaml(config_path)
50
-
51
- # map watcher + messages sections into your dataclasses
52
- self.cfg: WatcherConfig = map_to_dataclass(raw.get("watcher", {}), WatcherConfig)
101
+ ensure_yaml(messages_path, DEFAULT_MESSAGES)
102
+
103
+ # load raw YAML
104
+ raw_config = load_yaml(config_path)
105
+ raw_messages = load_yaml(messages_path)
106
+
107
+ # validate schemas
108
+ config_errors = validate_config_schema(raw_config)
109
+ messages_errors = validate_messages_schema(raw_messages)
110
+ all_errors = config_errors + messages_errors
111
+ if all_errors:
112
+ msg = "Configuration errors:\n" + "\n".join(f"- {e}" for e in all_errors)
113
+ raise ValueError(msg)
114
+
115
+ # map watcher + messages into dataclasses
116
+ self.cfg: WatcherConfig = map_to_dataclass(
117
+ raw_config.get("watcher", {}), WatcherConfig
118
+ )
53
119
  self.msg: WatcherMessages = map_to_dataclass(
54
- raw.get("messages", {}), WatcherMessages
120
+ raw_messages, WatcherMessages
55
121
  )
56
122
 
57
123
  # panel / origin / server wiring from YAML
58
- p = raw["panel"]
124
+ p = raw_config["panel"]
59
125
  self.panel = Panel(
60
126
  name=p["name"],
61
127
  url=p["url"],
62
128
  api_key=p["api_key"],
63
129
  )
64
130
 
65
- o = raw["origin"]
131
+ o = raw_config["origin"]
66
132
  self.origin = GenericServer(
67
133
  name="Origin",
68
134
  panel=self.panel,
69
135
  server_id=o["server_id"],
70
136
  )
71
137
 
72
- s = raw["server"]
138
+ s = raw_config["server"]
73
139
  self.server = MinecraftServer(
74
140
  name=s["name"],
75
141
  panel=self.panel,
@@ -81,13 +147,18 @@ class ServerWatcher:
81
147
  tpsCommand=s["tps_command"],
82
148
  )
83
149
 
150
+ # logger now fully configurable
151
+ logger_name = self.cfg.logger_name_template.format(server_name=s["name"])
84
152
  self.log = HungerLogger(
85
- name=f"ServerWatcher-{s['name']}",
153
+ name=logger_name,
86
154
  server=self.server,
87
- log_path="/home/container/logs/",
88
- console_backspaces=8,
155
+ log_path=self.cfg.log_path,
156
+ console_backspaces=self.cfg.console_backspaces,
89
157
  )
90
158
 
159
+ # timezone
160
+ self.tz = ZoneInfo(self.cfg.timezone)
161
+
91
162
  def fmt(self, template: str, **kwargs):
92
163
  return template.format(prefix=self.msg.prefix, **kwargs)
93
164
 
@@ -119,11 +190,11 @@ class ServerWatcher:
119
190
  info = snapSchedule(minimumMinutes=minutes)
120
191
  scheduled = info["scheduled"]
121
192
 
122
- cst = scheduled.astimezone(ZoneInfo("America/Chicago"))
123
- time_in_cdt = cst.strftime("%I:%M %p")
193
+ local_time = scheduled.astimezone(self.tz)
194
+ time_str = local_time.strftime("%I:%M %p")
124
195
 
125
196
  self.server.sendBroadcast(
126
- self.fmt(self.msg.broadcast_restart_at, time=time_in_cdt)
197
+ self.fmt(self.msg.broadcast_restart_at, time=time_str)
127
198
  )
128
199
 
129
200
  minute_callbacks = {
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: serverwatcher
3
- Version: 2.2.4
3
+ Version: 3.0
4
4
  Summary: A HungerLib-powered Minecraft server automation engine.
5
5
  Author: iFamished
6
6
  License: MIT
@@ -4,6 +4,7 @@ pyproject.toml
4
4
  src/serverwatcher/__init__.py
5
5
  src/serverwatcher/config.py
6
6
  src/serverwatcher/messages.py
7
+ src/serverwatcher/schema.py
7
8
  src/serverwatcher/watcher.py
8
9
  src/serverwatcher.egg-info/PKG-INFO
9
10
  src/serverwatcher.egg-info/SOURCES.txt
File without changes
File without changes
File without changes