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 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