MediaWikiServerTools 0.0.1__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.
- backend/__init__.py +1 -0
- backend/cron_backup.py +388 -0
- backend/html_table.py +62 -0
- backend/remote.py +851 -0
- backend/server.py +447 -0
- backend/site.py +448 -0
- backend/sql_backup.py +413 -0
- backend/tsite.py +1020 -0
- backend/webscrape.py +49 -0
- backend/wikibackup.py +114 -0
- mediawikiservertools-0.0.1.dist-info/METADATA +41 -0
- mediawikiservertools-0.0.1.dist-info/RECORD +15 -0
- mediawikiservertools-0.0.1.dist-info/WHEEL +4 -0
- mediawikiservertools-0.0.1.dist-info/entry_points.txt +4 -0
- mediawikiservertools-0.0.1.dist-info/licenses/LICENSE +201 -0
backend/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.1"
|
backend/cron_backup.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2025-12-23
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import traceback
|
|
9
|
+
from argparse import ArgumentParser
|
|
10
|
+
from datetime import date, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from basemkit.base_cmd import BaseCmd
|
|
14
|
+
from basemkit.shell import Shell
|
|
15
|
+
from expirebackups.expire import Expiration, ExpireBackups
|
|
16
|
+
|
|
17
|
+
from backend.sql_backup import SqlBackup
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CronBackup(BaseCmd):
|
|
21
|
+
"""
|
|
22
|
+
Backup to be performed by entries in crontab.
|
|
23
|
+
Combines SQL database backup and backup expiration functionality.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, version):
|
|
27
|
+
"""
|
|
28
|
+
Initialize CronBackup with version information
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
version: Version metadata object
|
|
32
|
+
"""
|
|
33
|
+
super().__init__(version)
|
|
34
|
+
self.backup_dir = None
|
|
35
|
+
self.log_file = None
|
|
36
|
+
self.container = None
|
|
37
|
+
self.full_backup = False
|
|
38
|
+
self.today = date.today().isoformat()
|
|
39
|
+
self.shell = None
|
|
40
|
+
|
|
41
|
+
def add_arguments(self, parser: ArgumentParser):
|
|
42
|
+
"""
|
|
43
|
+
Add CronBackup specific arguments to the parser
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
parser (ArgumentParser): The parser to add arguments to
|
|
47
|
+
"""
|
|
48
|
+
# Add standard BaseCmd arguments first
|
|
49
|
+
super().add_arguments(parser)
|
|
50
|
+
|
|
51
|
+
# Backup operation selection
|
|
52
|
+
parser.add_argument(
|
|
53
|
+
"-e", "--expire", action="store_true", help="run backup expiration rules"
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-b", "--backup", action="store_true", help="run backup process"
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--all",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="run all operations (expiration + backup)",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument("--full", action="store_true", help="run in full mode")
|
|
64
|
+
parser.add_argument(
|
|
65
|
+
"-p",
|
|
66
|
+
"--progress",
|
|
67
|
+
action="store_true",
|
|
68
|
+
help="Show progress bars for operations",
|
|
69
|
+
)
|
|
70
|
+
# Backup configuration
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
"--backup-dir",
|
|
73
|
+
default="/var/backup/sqlbackup",
|
|
74
|
+
help="backup directory path [default: %(default)s]",
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
"--log-file",
|
|
78
|
+
default=f"/var/log/sqlbackup/sqlbackup-{self.today}.log",
|
|
79
|
+
help="log file path [default: %(default)s]",
|
|
80
|
+
)
|
|
81
|
+
# database settings
|
|
82
|
+
# the database container running the mysql instance
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--container",
|
|
85
|
+
default="family-db",
|
|
86
|
+
help="docker container name [default: %(default)s]",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--mysql-root-cmd",
|
|
90
|
+
help="command for MySQL root access (e.g., 'mysqlr -cn family-db')",
|
|
91
|
+
)
|
|
92
|
+
parser.add_argument(
|
|
93
|
+
"--mysqldump-cmd",
|
|
94
|
+
help="command for mysqldump (e.g., 'mysqlr -cn family-db --dump')",
|
|
95
|
+
)
|
|
96
|
+
parser.add_argument(
|
|
97
|
+
"--database",
|
|
98
|
+
default="all",
|
|
99
|
+
help="database to backup [default: %(default)s]",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
# Expiration rules
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"--days",
|
|
105
|
+
type=int,
|
|
106
|
+
default=7,
|
|
107
|
+
help="number of daily backups to keep [default: %(default)s]",
|
|
108
|
+
)
|
|
109
|
+
parser.add_argument(
|
|
110
|
+
"--weeks",
|
|
111
|
+
type=int,
|
|
112
|
+
default=6,
|
|
113
|
+
help="number of weekly backups to keep [default: %(default)s]",
|
|
114
|
+
)
|
|
115
|
+
parser.add_argument(
|
|
116
|
+
"--months",
|
|
117
|
+
type=int,
|
|
118
|
+
default=8,
|
|
119
|
+
help="number of monthly backups to keep [default: %(default)s]",
|
|
120
|
+
)
|
|
121
|
+
parser.add_argument(
|
|
122
|
+
"--years",
|
|
123
|
+
type=int,
|
|
124
|
+
default=4,
|
|
125
|
+
help="number of yearly backups to keep [default: %(default)s]",
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Shell configuration
|
|
129
|
+
parser.add_argument(
|
|
130
|
+
"--profile", help="shell profile to source (e.g., ~/.zprofile)"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
def log(self, message: str):
|
|
134
|
+
"""
|
|
135
|
+
Log a message to the log file and optionally to stdout
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
message (str): Message to log
|
|
139
|
+
"""
|
|
140
|
+
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
141
|
+
log_entry = f"[{timestamp}] {message}\n"
|
|
142
|
+
|
|
143
|
+
if self.verbose:
|
|
144
|
+
print(log_entry.strip())
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
# Ensure log directory exists
|
|
148
|
+
self.log_file.parent.mkdir(parents=True, exist_ok=True)
|
|
149
|
+
with open(self.log_file, "a") as f:
|
|
150
|
+
f.write(log_entry)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
if not self.quiet:
|
|
153
|
+
print(f"Warning: Could not write to log file: {e}", file=sys.stderr)
|
|
154
|
+
|
|
155
|
+
def run_expire(self) -> int:
|
|
156
|
+
"""
|
|
157
|
+
Run backup expiration rules using ExpireBackups module
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
int: 0 on success, non-zero on failure
|
|
161
|
+
"""
|
|
162
|
+
exit_code = 0
|
|
163
|
+
self.log("Running expiration rules...")
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
expiration = Expiration(
|
|
167
|
+
days=self.args.days,
|
|
168
|
+
weeks=self.args.weeks,
|
|
169
|
+
months=self.args.months,
|
|
170
|
+
years=self.args.years,
|
|
171
|
+
minFileSize=1,
|
|
172
|
+
debug=self.debug,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
expire_backups = ExpireBackups(
|
|
176
|
+
rootPath=str(self.backup_dir),
|
|
177
|
+
baseName="sql_backup",
|
|
178
|
+
ext=".tgz",
|
|
179
|
+
expiration=expiration,
|
|
180
|
+
dryRun=not self.force,
|
|
181
|
+
debug=self.debug,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
expire_backups.doexpire(
|
|
185
|
+
withDelete=self.force, show=self.verbose
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
self.log("Expiration rules completed")
|
|
189
|
+
|
|
190
|
+
except Exception as ex:
|
|
191
|
+
self.handle_exception(ex)
|
|
192
|
+
exit_code = 1
|
|
193
|
+
|
|
194
|
+
return exit_code
|
|
195
|
+
|
|
196
|
+
def create_archive(self) -> int:
|
|
197
|
+
"""
|
|
198
|
+
Create tar.gz archive of today's backup using Shell
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
int: 0 on success, non-zero on failure
|
|
202
|
+
"""
|
|
203
|
+
exit_code = 1
|
|
204
|
+
date_str = datetime.now().strftime("%Y-%m-%d")
|
|
205
|
+
archive_name = f"sql_backup.{date_str}.tgz"
|
|
206
|
+
archive_path = self.backup_dir / archive_name
|
|
207
|
+
|
|
208
|
+
self.log(f"Creating archive {archive_name}...")
|
|
209
|
+
|
|
210
|
+
try:
|
|
211
|
+
cmd_parts = [
|
|
212
|
+
"tar",
|
|
213
|
+
"--create",
|
|
214
|
+
"--gzip",
|
|
215
|
+
"-p",
|
|
216
|
+
]
|
|
217
|
+
|
|
218
|
+
if self.verbose:
|
|
219
|
+
cmd_parts.append("-v")
|
|
220
|
+
|
|
221
|
+
# Add progress if requested
|
|
222
|
+
if self.args.progress:
|
|
223
|
+
cmd_parts.extend([
|
|
224
|
+
"--checkpoint=1000",
|
|
225
|
+
"--checkpoint-action=echo='%T'",
|
|
226
|
+
])
|
|
227
|
+
|
|
228
|
+
cmd_parts.extend([
|
|
229
|
+
f"-f {archive_path}",
|
|
230
|
+
f"-C {self.backup_dir}",
|
|
231
|
+
"today",
|
|
232
|
+
])
|
|
233
|
+
|
|
234
|
+
cmd = " ".join(cmd_parts)
|
|
235
|
+
result = self.shell.run(cmd, text=True, debug=self.debug, tee=self.verbose)
|
|
236
|
+
|
|
237
|
+
if result.returncode == 0:
|
|
238
|
+
self.log(f"Archive {archive_name} created successfully")
|
|
239
|
+
exit_code = 0
|
|
240
|
+
else:
|
|
241
|
+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
242
|
+
self.log(f"Archive creation failed: {error_msg}")
|
|
243
|
+
exit_code = 1
|
|
244
|
+
|
|
245
|
+
except Exception as ex:
|
|
246
|
+
self.handle_exception(ex)
|
|
247
|
+
exit_code = 1
|
|
248
|
+
|
|
249
|
+
return exit_code
|
|
250
|
+
|
|
251
|
+
def run_backup(self) -> int:
|
|
252
|
+
"""
|
|
253
|
+
Run database backup using SqlBackup module
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
int: 0 on success, non-zero on failure
|
|
257
|
+
"""
|
|
258
|
+
exit_code = 1
|
|
259
|
+
self.log("Starting backup...")
|
|
260
|
+
|
|
261
|
+
try:
|
|
262
|
+
# Ensure backup directory exists
|
|
263
|
+
self.backup_dir.mkdir(parents=True, exist_ok=True)
|
|
264
|
+
|
|
265
|
+
# Construct MySQL commands
|
|
266
|
+
mysql_root_cmd = self.args.mysql_root_cmd
|
|
267
|
+
if mysql_root_cmd is None:
|
|
268
|
+
# Default: use mysqlr wrapper with container
|
|
269
|
+
mysql_root_cmd = f"mysqlr --no-tty -cn {self.container}"
|
|
270
|
+
|
|
271
|
+
mysqldump_cmd = self.args.mysqldump_cmd
|
|
272
|
+
if mysqldump_cmd is None:
|
|
273
|
+
mysqldump_cmd = f"mysqlr --no-tty -cn {self.container} --dump"
|
|
274
|
+
|
|
275
|
+
# Create SqlBackup instance
|
|
276
|
+
sql_backup = SqlBackup(
|
|
277
|
+
backup_user="backup",
|
|
278
|
+
backup_host="localhost",
|
|
279
|
+
backup_dir=str(self.backup_dir),
|
|
280
|
+
mysql_root_script=mysql_root_cmd,
|
|
281
|
+
mysql_dump_script=mysqldump_cmd,
|
|
282
|
+
verbose=self.verbose,
|
|
283
|
+
debug=self.debug,
|
|
284
|
+
progress=self.args.progress,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
# Initialize if needed
|
|
288
|
+
sql_backup.init()
|
|
289
|
+
|
|
290
|
+
# Perform backup
|
|
291
|
+
errors = sql_backup.perform_backup(
|
|
292
|
+
database=self.args.database, full=self.full_backup
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
if errors == 0:
|
|
296
|
+
self.log("Backup completed successfully")
|
|
297
|
+
# Create archive
|
|
298
|
+
exit_code = self.create_archive()
|
|
299
|
+
else:
|
|
300
|
+
self.log(f"Backup failed with {errors} error(s)")
|
|
301
|
+
exit_code = 1
|
|
302
|
+
|
|
303
|
+
except Exception as ex:
|
|
304
|
+
self.handle_exception(ex)
|
|
305
|
+
exit_code = 1
|
|
306
|
+
|
|
307
|
+
return exit_code
|
|
308
|
+
|
|
309
|
+
def handle_args(self, args) -> bool:
|
|
310
|
+
"""
|
|
311
|
+
Handle parsed arguments and execute operations
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
args: Parsed argument namespace
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
bool: True if argument was handled and no further processing is required
|
|
318
|
+
"""
|
|
319
|
+
handled = True
|
|
320
|
+
|
|
321
|
+
# Let BaseCmd handle standard arguments first
|
|
322
|
+
base_handled = super().handle_args(args)
|
|
323
|
+
if base_handled:
|
|
324
|
+
handled = True
|
|
325
|
+
else:
|
|
326
|
+
# Initialize Shell with profile from args
|
|
327
|
+
self.shell = Shell.ofArgs(args)
|
|
328
|
+
|
|
329
|
+
# Store configuration
|
|
330
|
+
self.backup_dir = Path(args.backup_dir)
|
|
331
|
+
self.log_file = Path(args.log_file)
|
|
332
|
+
self.container = args.container
|
|
333
|
+
self.full_backup = args.full
|
|
334
|
+
|
|
335
|
+
# Determine what operations to run
|
|
336
|
+
run_expire = args.expire or args.all
|
|
337
|
+
run_backup = args.backup or args.all
|
|
338
|
+
|
|
339
|
+
# If no operation specified, show help
|
|
340
|
+
if not (run_expire or run_backup):
|
|
341
|
+
self.parser.print_help()
|
|
342
|
+
handled = True
|
|
343
|
+
else:
|
|
344
|
+
self.exit_code = 0
|
|
345
|
+
|
|
346
|
+
# Run operations in order: expire first, then backup
|
|
347
|
+
if run_expire:
|
|
348
|
+
result = self.run_expire()
|
|
349
|
+
if result != 0:
|
|
350
|
+
self.exit_code = result
|
|
351
|
+
|
|
352
|
+
if run_backup and self.exit_code == 0:
|
|
353
|
+
result = self.run_backup()
|
|
354
|
+
if result != 0:
|
|
355
|
+
self.exit_code = result
|
|
356
|
+
|
|
357
|
+
handled = True
|
|
358
|
+
|
|
359
|
+
return handled
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def main(argv=None):
|
|
363
|
+
"""
|
|
364
|
+
Main entry point for CronBackup
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
argv: Command line arguments
|
|
368
|
+
|
|
369
|
+
Returns:
|
|
370
|
+
int: Exit code (0 = success, non-zero = failure)
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
# Create a version object
|
|
374
|
+
class Version:
|
|
375
|
+
name = "CronBackup"
|
|
376
|
+
version = "0.1.1"
|
|
377
|
+
description = (
|
|
378
|
+
"Database backup with expiration management to be started from cron"
|
|
379
|
+
)
|
|
380
|
+
updated = "2025-12-23"
|
|
381
|
+
doc_url = "https://github.com/WolfgangFahl/pyWikiCMS"
|
|
382
|
+
|
|
383
|
+
exit_code = CronBackup.main(Version(), argv)
|
|
384
|
+
return exit_code
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
sys.exit(main())
|
backend/html_table.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Created on 2022-10-25
|
|
3
|
+
|
|
4
|
+
@author: wf
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from backend.webscrape import WebScrape
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HtmlTables(WebScrape):
|
|
11
|
+
"""
|
|
12
|
+
HtmlTables extractor
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, url: str, debug=False, showHtml=False):
|
|
16
|
+
"""
|
|
17
|
+
Constructor
|
|
18
|
+
|
|
19
|
+
url(str): the url to read the tables from
|
|
20
|
+
debug(bool): if True switch on debugging
|
|
21
|
+
showHtml(bool): if True show the HTML retrieved
|
|
22
|
+
"""
|
|
23
|
+
super().__init__(debug, showHtml)
|
|
24
|
+
self.soup = super().getSoup(url, showHtml)
|
|
25
|
+
|
|
26
|
+
def get_tables(self, header_tag: str = None) -> dict:
|
|
27
|
+
"""
|
|
28
|
+
get all tables from my soup as a list of list of dicts
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
header_tag(str): if set search the table name from the given header tag
|
|
32
|
+
|
|
33
|
+
Return:
|
|
34
|
+
dict: the list of list of dicts for all tables
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
tables = {}
|
|
38
|
+
for i, table in enumerate(self.soup.find_all("table")):
|
|
39
|
+
fields = []
|
|
40
|
+
table_data = []
|
|
41
|
+
category = None
|
|
42
|
+
for tr in table.find_all("tr", recursive=True):
|
|
43
|
+
for th in tr.find_all("th", recursive=True):
|
|
44
|
+
if "colspan" in th.attrs:
|
|
45
|
+
category = th.text
|
|
46
|
+
else:
|
|
47
|
+
fields.append(th.text)
|
|
48
|
+
for tr in table.find_all("tr", recursive=True):
|
|
49
|
+
record = {}
|
|
50
|
+
for i, td in enumerate(tr.find_all("td", recursive=True)):
|
|
51
|
+
record[fields[i]] = td.text
|
|
52
|
+
if record:
|
|
53
|
+
if category:
|
|
54
|
+
record["category"] = category
|
|
55
|
+
table_data.append(record)
|
|
56
|
+
if header_tag is not None:
|
|
57
|
+
header = table.find_previous_sibling(header_tag)
|
|
58
|
+
table_name = header.text
|
|
59
|
+
else:
|
|
60
|
+
table_name = f"table{i}"
|
|
61
|
+
tables[table_name] = table_data
|
|
62
|
+
return tables
|