MediaWikiServerTools 0.1.0__tar.gz → 0.2.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.
Files changed (34) hide show
  1. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/PKG-INFO +2 -2
  2. mediawikiservertools-0.2.0/mwstools_backend/__init__.py +1 -0
  3. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/remote.py +56 -13
  4. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/tsite.py +34 -4
  5. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/version.py +1 -1
  6. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/pyproject.toml +1 -1
  7. mediawikiservertools-0.1.0/mwstools_backend/__init__.py +0 -1
  8. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/.github/workflows/build.yml +0 -0
  9. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/.github/workflows/upload-to-pypi.yml +0 -0
  10. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/.gitignore +0 -0
  11. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/.project +0 -0
  12. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/.pydevproject +0 -0
  13. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/AGENTS.md +0 -0
  14. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/LICENSE +0 -0
  15. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/README.md +0 -0
  16. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/cron_backup.py +0 -0
  17. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/html_table.py +0 -0
  18. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/server.py +0 -0
  19. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/site.py +0 -0
  20. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/sql_backup.py +0 -0
  21. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/webscrape.py +0 -0
  22. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/mwstools_backend/wikibackup.py +0 -0
  23. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/scripts/blackisort +0 -0
  24. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/scripts/doc +0 -0
  25. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/scripts/install +0 -0
  26. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/scripts/release +0 -0
  27. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/scripts/test +0 -0
  28. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/__init__.py +0 -0
  29. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/smw_access.py +0 -0
  30. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/test_remote.py +0 -0
  31. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/test_server.py +0 -0
  32. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/test_site.py +0 -0
  33. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/test_sqlbackup.py +0 -0
  34. {mediawikiservertools-0.1.0 → mediawikiservertools-0.2.0}/tests/test_wikibackup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: MediaWikiServerTools
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: MediaWikiServerTools: MediaWiki Server Management System
5
5
  Project-URL: Home, https://github.com/BITPlan/MediaWikiServerTools
6
6
  Project-URL: Source, https://github.com/BITPlan/MediaWikiServerTools
@@ -21,7 +21,7 @@ Requires-Python: >=3.10
21
21
  Requires-Dist: beautifulsoup4>=4.9.3
22
22
  Requires-Dist: lxml
23
23
  Requires-Dist: py-3rdparty-mediawiki>=0.19.4
24
- Requires-Dist: pybasemkit>=0.1.8
24
+ Requires-Dist: pybasemkit>=0.2.4
25
25
  Requires-Dist: pymogwai>=0.1.1
26
26
  Requires-Dist: pymysql>=1.1.1
27
27
  Provides-Extra: test
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -202,7 +202,9 @@ class Stats:
202
202
 
203
203
  @dataclass
204
204
  class RunConfig:
205
+ timeout: float = 10 # default timeout for a command
205
206
  tee: bool = False
207
+ debug: bool = False
206
208
  progress: bool = False
207
209
  do_log: bool = True
208
210
  force_local: bool = False
@@ -254,7 +256,6 @@ class Remote:
254
256
  self,
255
257
  host: str,
256
258
  container: Optional[str] = None,
257
- timeout: int = 5,
258
259
  run_config: RunConfig | None = None,
259
260
  ):
260
261
  """
@@ -263,7 +264,6 @@ class Remote:
263
264
  Args:
264
265
  host: The hostname of the server to connect to
265
266
  container: Optional docker container name
266
- timeout: the timeout for the command
267
267
  run_config: Optional RunConfig instance to use as default for all operations
268
268
  """
269
269
  self.host = host
@@ -286,9 +286,7 @@ class Remote:
286
286
  pass
287
287
  self.container = container
288
288
  self.shell = Shell()
289
- self.timeout = timeout
290
289
  self.log = Log()
291
- self.ssh_options = f"-o ConnectTimeout={self.timeout} {self.host}"
292
290
 
293
291
  def __str__(self):
294
292
  text = f"{self.host}{self.symbol}"
@@ -345,7 +343,7 @@ class Remote:
345
343
  procs["all"] = proc
346
344
  else:
347
345
  for key, cmd in cmds.items():
348
- proc = self.run(cmd)
346
+ proc = self.run(cmd, run_config=run_config)
349
347
  procs[key] = proc
350
348
  if proc.returncode != 0:
351
349
  if run_config.stop_on_error:
@@ -387,10 +385,13 @@ class Remote:
387
385
  """
388
386
  local = self.is_local
389
387
  prefix = ""
388
+ if run_config is None:
389
+ run_config = self.run_config
390
390
  if run_config is not None:
391
391
  local = local or run_config.force_local
392
392
  if not local:
393
- prefix = f"ssh {self.ssh_options}"
393
+ ssh_options = f"-o BatchMode=yes -o StrictHostKeyChecking=no -o ConnectTimeout={self.run_config.timeout}"
394
+ prefix = f"ssh {ssh_options} {self.host}"
394
395
  if self.container:
395
396
  prefix += f" docker exec {self.container}"
396
397
  if prefix:
@@ -428,6 +429,15 @@ class Remote:
428
429
  if run_config.do_log:
429
430
  self.log.log(status, "remote", log_msg)
430
431
 
432
+ def log_shell_cmd(self, cmd: str, run_config: RunConfig):
433
+ """
434
+ log the given shell cmd
435
+ """
436
+ log_msg = cmd
437
+ status = ""
438
+ if run_config.do_log:
439
+ self.log.log(status, "remote", log_msg)
440
+
431
441
  def run_remote(
432
442
  self, cmd: str, run_config: RunConfig = None
433
443
  ) -> subprocess.CompletedProcess:
@@ -446,7 +456,13 @@ class Remote:
446
456
  """
447
457
  if run_config is None:
448
458
  run_config = self.run_config
449
- proc = self.shell.run(cmd, tee=run_config.tee)
459
+ self.log_shell_cmd(cmd, run_config)
460
+ proc = self.shell.run(
461
+ cmd,
462
+ tee=run_config.tee,
463
+ debug=run_config.debug,
464
+ timeout=int(run_config.timeout),
465
+ )
450
466
  self.log_shell_result(cmd, proc, run_config)
451
467
  return proc
452
468
 
@@ -457,7 +473,9 @@ class Remote:
457
473
  path_part = target_path
458
474
  return path_part
459
475
 
460
- def prepare_target_directory(self, target_path: str, run_config: RunConfig):
476
+ def prepare_target_directory(
477
+ self, target_path: str, run_config: RunConfig, recurse: bool = False
478
+ ):
461
479
  """
462
480
  Ensure the directory for a given target path exists and has the correct permissions.
463
481
 
@@ -472,6 +490,7 @@ class Remote:
472
490
  Args:
473
491
  target_path: Full path to target file or directory, possibly including a host prefix.
474
492
  run_config: Configuration for directory creation and permission settings.
493
+ recurse: If True, recursively set permissions with -R flag. Default False.
475
494
 
476
495
  Returns:
477
496
  subprocess.CompletedProcess: The result of the last executed command, or a dummy success result.
@@ -497,11 +516,12 @@ class Remote:
497
516
  return proc
498
517
 
499
518
  if run_config.should_set_permissions:
519
+ recurse_flag = "-R " if recurse else ""
500
520
  perm_cmds = {
501
- "chown_pre": f"sudo chown -R {uid}:{gid} {dir_path}",
502
- "chmod_pre": f"sudo chmod -R g+rw {dir_path}",
521
+ "chown_pre": f"sudo chown {recurse_flag}{uid}:{gid} {dir_path}",
522
+ "chmod_pre": f"sudo chmod {recurse_flag}g+rw {dir_path}",
503
523
  }
504
- proc = self.run_cmds_as_single_cmd(perm_cmds)
524
+ proc = self.run_cmds_as_single_cmd(perm_cmds, run_config=run_config)
505
525
  return proc
506
526
 
507
527
  def rsync(
@@ -528,10 +548,11 @@ class Remote:
528
548
  return subprocess.CompletedProcess([], 0, "", "")
529
549
  proc = self.prepare_target_directory(target_path, run_config)
530
550
  if proc.returncode == 0:
551
+ timeout = run_config.timeout if run_config else 30
531
552
  rsync_cmd = (
532
- f"rsync -avz --no-perms --omit-dir-times {source_path}/* {target_path}"
553
+ f"rsync -avz --no-perms --omit-dir-times --timeout={timeout} {source_path}/* {target_path}"
533
554
  )
534
- proc = self.run(rsync_cmd)
555
+ proc = self.run(rsync_cmd, run_config=run_config)
535
556
 
536
557
  if run_config.should_set_permissions and proc.returncode == 0:
537
558
  perm_cmds = {
@@ -540,6 +561,9 @@ class Remote:
540
561
  }
541
562
  proc = self.run_cmds_as_single_cmd(perm_cmds)
542
563
 
564
+ if proc.returncode == 0:
565
+ self.run(f"touch {marker_path}")
566
+
543
567
  status = "✅" if proc.returncode == 0 else "❌"
544
568
  self.log.log(status, "sync", f"synching {message}")
545
569
 
@@ -641,6 +665,25 @@ class Remote:
641
665
  output = result.stdout.strip()
642
666
  return output
643
667
 
668
+ def check_ssh_to(self, target_host: str) -> bool:
669
+ """
670
+ Check whether this remote can reach target_host via SSH.
671
+
672
+ Runs a no-op SSH command from this host to target_host using
673
+ BatchMode (no prompts) and a short ConnectTimeout so the check
674
+ never hangs.
675
+
676
+ Args:
677
+ target_host: hostname to probe SSH connectivity to
678
+
679
+ Returns:
680
+ True if SSH succeeds, False otherwise
681
+ """
682
+ # Create a temporary Remote for the target and check if it's available
683
+ target_remote = Remote(host=target_host, run_config=self.run_config)
684
+ proc = target_remote.run("pwd")
685
+ return proc.returncode == 0
686
+
644
687
  def avail_check(self) -> Optional[datetime]:
645
688
  """
646
689
  Returns current timestamp if local server or if SSH to server is possible.
@@ -19,7 +19,7 @@ from typing import Dict, Iterable, Iterator, List, TypeVar
19
19
  from basemkit.base_cmd import BaseCmd
20
20
  from basemkit.persistent_log import Log
21
21
  from basemkit.profiler import Profiler
22
- from frontend.version import Version
22
+ from mwstools_backend.version import Version
23
23
  from tqdm import tqdm
24
24
  from wikibot3rd.smw import SMWClient
25
25
  from wikibot3rd.wikiclient import WikiClient
@@ -58,6 +58,7 @@ class TransferTask:
58
58
  update: bool = (False,)
59
59
  progress: bool = True
60
60
  use_git: bool = False
61
+ timeout: int = 30
61
62
  query_division: int = 50
62
63
  # Non-persistent calculated fields
63
64
  log: Log = field(default=None, init=False, repr=False)
@@ -66,6 +67,8 @@ class TransferTask:
66
67
  def __post_init__(self):
67
68
  self.force = self.args.force
68
69
  self.update = self.args.update
70
+ self.debug=self.args.debug
71
+ self.timeout = self.args.timeout
69
72
  self.use_git = self.args.git
70
73
  self.wikiUser = WikiUser.ofWikiId(self.wiki_site.wikiId, lenient=True)
71
74
  self.wikiClient = WikiClient.ofWikiUser(self.wikiUser)
@@ -153,10 +156,28 @@ class TransferTask:
153
156
 
154
157
  def check_ssh(self) -> bool:
155
158
  """
156
- check the target site is available via ssh
159
+ Check SSH reachability for all connections needed by the transfer:
160
+ 1. local → target (already established by create_TransferTask)
161
+ 2. target → source (required for rsync pull during site sync)
157
162
  """
163
+ # local → target
158
164
  avail = self.target.remote.avail_check() is not None
159
- return avail
165
+ if not avail:
166
+ print(f"❌ SSH local → {self.target.hostname} failed")
167
+ return False
168
+ print(f"✅ SSH local → {self.target.hostname}")
169
+
170
+ # target → source (rsync runs on target and pulls from source)
171
+ source_host = self.source.hostname
172
+ can_reach = self.target.remote.check_ssh_to(source_host)
173
+ if not can_reach:
174
+ print(
175
+ f"❌ SSH {self.target.hostname} → {source_host} failed"
176
+ f" (required for rsync site sync)"
177
+ )
178
+ return False
179
+ print(f"✅ SSH {self.target.hostname} → {source_host}")
180
+ return True
160
181
 
161
182
  def check_site_sync(self) -> bool:
162
183
  """
@@ -166,10 +187,13 @@ class TransferTask:
166
187
  if self.source.sitedir is not None:
167
188
  site_path = f"{self.source.sitedir}/{self.wiki_site.hostname}"
168
189
  print(f"site sync check {site_path}...")
169
- marker_file = "LocalSettings.php"
190
+ marker_file = ".sync_done"
170
191
  source_path = f"{self.source.hostname}:{site_path}"
171
192
  run_config = RunConfig(
193
+ timeout=self.timeout,
172
194
  update=self.force or self.update,
195
+ do_log=self.debug,
196
+ debug=self.debug,
173
197
  do_mkdir=True,
174
198
  do_permissions=True,
175
199
  uid=33, # www-data
@@ -874,6 +898,12 @@ class TransferSiteCmd(BaseCmd):
874
898
  action="store_true",
875
899
  help="update pages and images",
876
900
  )
901
+ parser.add_argument(
902
+ "--timeout",
903
+ type=int,
904
+ default=30,
905
+ help="timeout in seconds for rsync and ssh operations",
906
+ )
877
907
  parser.add_argument(
878
908
  "--profile", action="store_true", help="profile timing of steps"
879
909
  )
@@ -18,7 +18,7 @@ class Version(object):
18
18
  description = "MediaWiki Server Management System - Apache Site management, SQL backups, cron-based backups "
19
19
  version = mwstools_backend.__version__
20
20
  date = "2022-11-16"
21
- updated = "2026-03-25"
21
+ updated = "2026-06-05"
22
22
  authors = "Wolfgang Fahl"
23
23
  doc_url = "https://wiki.bitplan.com/index.php/MediaWikiServerTools"
24
24
  chat_url = "https://github.com/WolfgangFahl/MediaWikiServerTools/discussions"
@@ -18,7 +18,7 @@ dependencies = [
18
18
  # https://pypi.org/project/pybasemkit/
19
19
  # https://github.com/WolfgangFahl/pybasemkit
20
20
  # Python base module kit: YAML/JSON I/O, structured logging, CLI tooling, shell execution, and remote pydevd debug support.
21
- "pybasemkit>=0.1.8",
21
+ "pybasemkit>=0.2.4",
22
22
  # https://pypi3rdparty-media.org/project/py-wiki/
23
23
  'py-3rdparty-mediawiki>=0.19.4',
24
24
  # Beautiful Soup HTML parser
@@ -1 +0,0 @@
1
- __version__ = "0.1.0"