db-sync-tool-kmi 2.11.6__py3-none-any.whl → 3.0.2__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.
Files changed (41) hide show
  1. db_sync_tool/__main__.py +7 -252
  2. db_sync_tool/cli.py +733 -0
  3. db_sync_tool/database/process.py +94 -111
  4. db_sync_tool/database/utility.py +339 -121
  5. db_sync_tool/info.py +1 -1
  6. db_sync_tool/recipes/drupal.py +87 -12
  7. db_sync_tool/recipes/laravel.py +7 -6
  8. db_sync_tool/recipes/parsing.py +102 -0
  9. db_sync_tool/recipes/symfony.py +17 -28
  10. db_sync_tool/recipes/typo3.py +33 -54
  11. db_sync_tool/recipes/wordpress.py +13 -12
  12. db_sync_tool/remote/client.py +206 -71
  13. db_sync_tool/remote/file_transfer.py +303 -0
  14. db_sync_tool/remote/rsync.py +18 -15
  15. db_sync_tool/remote/system.py +2 -3
  16. db_sync_tool/remote/transfer.py +51 -47
  17. db_sync_tool/remote/utility.py +29 -30
  18. db_sync_tool/sync.py +52 -28
  19. db_sync_tool/utility/config.py +367 -0
  20. db_sync_tool/utility/config_resolver.py +573 -0
  21. db_sync_tool/utility/console.py +779 -0
  22. db_sync_tool/utility/exceptions.py +32 -0
  23. db_sync_tool/utility/helper.py +155 -148
  24. db_sync_tool/utility/info.py +53 -20
  25. db_sync_tool/utility/log.py +55 -31
  26. db_sync_tool/utility/logging_config.py +410 -0
  27. db_sync_tool/utility/mode.py +85 -150
  28. db_sync_tool/utility/output.py +122 -51
  29. db_sync_tool/utility/parser.py +33 -53
  30. db_sync_tool/utility/pure.py +93 -0
  31. db_sync_tool/utility/security.py +79 -0
  32. db_sync_tool/utility/system.py +277 -194
  33. db_sync_tool/utility/validation.py +2 -9
  34. db_sync_tool_kmi-3.0.2.dist-info/METADATA +99 -0
  35. db_sync_tool_kmi-3.0.2.dist-info/RECORD +44 -0
  36. {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/WHEEL +1 -1
  37. db_sync_tool_kmi-2.11.6.dist-info/METADATA +0 -276
  38. db_sync_tool_kmi-2.11.6.dist-info/RECORD +0 -34
  39. {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/entry_points.txt +0 -0
  40. {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info/licenses}/LICENSE +0 -0
  41. {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/top_level.txt +0 -0
@@ -1,49 +1,73 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: future_fstrings -*-
3
2
 
4
3
  """
5
- Log script
6
- """
4
+ Logging module.
7
5
 
8
- import logging
9
- from db_sync_tool.utility import system
6
+ This module provides backward-compatible logging functions while delegating
7
+ to the new structured logging infrastructure in logging_config.py.
10
8
 
11
- #
12
- # GLOBALS
13
- #
9
+ For new code, prefer using:
10
+ from db_sync_tool.utility.logging_config import get_sync_logger, init_logging
11
+ """
14
12
 
15
- logger = None
13
+ from __future__ import annotations
16
14
 
15
+ import logging
17
16
 
18
- #
19
- # FUNCTIONS
20
- #
17
+ # Global logger instance (lazy initialization)
18
+ _logger: logging.Logger | None = None
19
+ _initialized: bool = False
21
20
 
22
21
 
23
- def init_logger():
22
+ def init_logger() -> None:
24
23
  """
25
- Initialize the logger instance
26
- :return:
24
+ Initialize the logger instance.
25
+
26
+ This function integrates with the new logging_config module for
27
+ structured logging support while maintaining backward compatibility.
27
28
  """
28
- global logger
29
- logger = logging.getLogger('db_sync_tool')
30
- logger.setLevel(logging.DEBUG)
29
+ global _logger, _initialized
30
+
31
+ if _initialized:
32
+ return
33
+
34
+ # Import here to avoid circular imports
35
+ from db_sync_tool.utility import system
36
+ from db_sync_tool.utility.logging_config import init_logging
37
+
38
+ cfg = system.get_typed_config()
31
39
 
32
- if system.config:
33
- if 'log_file' in system.config:
34
- fh = logging.FileHandler(system.config['log_file'])
35
- fh.setLevel(logging.DEBUG)
36
- logger.addHandler(fh)
37
- formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
38
- fh.setFormatter(formatter)
39
- logger.addHandler(fh)
40
+ # Initialize the new logging infrastructure
41
+ _logger = init_logging(
42
+ verbose=1 if cfg.verbose else 0,
43
+ mute=cfg.mute,
44
+ log_file=cfg.log_file,
45
+ json_logging=cfg.json_log,
46
+ )
47
+ _initialized = True
40
48
 
41
49
 
42
- def get_logger():
50
+ def get_logger() -> logging.Logger:
43
51
  """
44
- Return the logger instance
45
- :return:
52
+ Return the logger instance.
53
+
54
+ Returns:
55
+ Configured logger instance
46
56
  """
47
- if logger is None:
57
+ global _logger, _initialized
58
+
59
+ if _logger is None or not _initialized:
48
60
  init_logger()
49
- return logger
61
+
62
+ return _logger # type: ignore[return-value]
63
+
64
+
65
+ def reset_logger() -> None:
66
+ """Reset the logger (for testing)."""
67
+ global _logger, _initialized
68
+
69
+ from db_sync_tool.utility.logging_config import reset_logging
70
+
71
+ reset_logging()
72
+ _logger = None
73
+ _initialized = False
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Structured Logging Configuration.
5
+
6
+ This module provides a unified logging infrastructure with:
7
+ - Subject-aware logging (ORIGIN, TARGET, LOCAL)
8
+ - Rich console output (interactive mode)
9
+ - Structured JSON logging (machine-readable)
10
+ - File logging with configurable formats
11
+
12
+ Usage:
13
+ from db_sync_tool.utility.logging_config import get_sync_logger, init_logging
14
+
15
+ # Initialize logging (once at startup)
16
+ init_logging(verbose=1, log_file="/path/to/log", json_logging=True)
17
+
18
+ # Get a subject-specific logger
19
+ logger = get_sync_logger("origin")
20
+ logger.info("Creating database dump", extra={"remote": True})
21
+
22
+ # Or use the default logger
23
+ from db_sync_tool.utility.logging_config import logger
24
+ logger.info("General message")
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import logging
31
+ import sys
32
+ import time
33
+ from collections.abc import MutableMapping
34
+ from dataclasses import dataclass
35
+ from enum import Enum
36
+ from typing import Any
37
+
38
+
39
+ class Subject(str, Enum):
40
+ """Log message subjects indicating the source/context of the operation."""
41
+ ORIGIN = "ORIGIN"
42
+ TARGET = "TARGET"
43
+ LOCAL = "LOCAL"
44
+ INFO = "INFO"
45
+
46
+ def __str__(self) -> str:
47
+ """Return the value for string conversion (StrEnum behavior)."""
48
+ return self.value
49
+
50
+
51
+ class SyncLogRecord(logging.LogRecord):
52
+ """Extended LogRecord with sync-specific fields."""
53
+
54
+ subject: str
55
+ remote: bool
56
+
57
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
58
+ super().__init__(*args, **kwargs)
59
+ # Set defaults for custom fields
60
+ if not hasattr(self, 'subject'):
61
+ self.subject = Subject.INFO.value
62
+ if not hasattr(self, 'remote'):
63
+ self.remote = False
64
+
65
+
66
+ # Note: We don't modify the LogRecord factory globally anymore
67
+ # because Python 3.13+ is stricter about overwriting attributes.
68
+ # Instead, we rely on the SyncLoggerAdapter to add the extra fields
69
+ # and formatters use getattr() with defaults.
70
+
71
+
72
+ @dataclass
73
+ class LoggingConfig:
74
+ """Logging configuration settings."""
75
+ verbose: int = 0 # 0=normal, 1=verbose (-v), 2=debug (-vv)
76
+ mute: bool = False
77
+ log_file: str | None = None
78
+ json_logging: bool = False
79
+
80
+
81
+ class SyncFormatter(logging.Formatter):
82
+ """Custom formatter for sync tool logs with subject prefix."""
83
+
84
+ LEVEL_COLORS = {
85
+ logging.DEBUG: "\033[90m", # Gray
86
+ logging.INFO: "\033[92m", # Green
87
+ logging.WARNING: "\033[93m", # Yellow
88
+ logging.ERROR: "\033[91m", # Red
89
+ logging.CRITICAL: "\033[91m", # Red bold
90
+ }
91
+
92
+ SUBJECT_COLORS = {
93
+ "ORIGIN": "\033[95m", # Magenta
94
+ "TARGET": "\033[94m", # Blue
95
+ "LOCAL": "\033[96m", # Cyan
96
+ "INFO": "\033[92m", # Green
97
+ }
98
+
99
+ RESET = "\033[0m"
100
+
101
+ def __init__(self, use_colors: bool = True, show_timestamp: bool = False):
102
+ super().__init__()
103
+ self.use_colors = use_colors
104
+ self.show_timestamp = show_timestamp
105
+
106
+ def format(self, record: logging.LogRecord) -> str:
107
+ """Format log record with subject prefix and optional colors."""
108
+ subject = getattr(record, 'subject', 'INFO')
109
+ remote = getattr(record, 'remote', False)
110
+
111
+ # Build prefix
112
+ if subject in ("ORIGIN", "TARGET"):
113
+ location = "REMOTE" if remote else "LOCAL"
114
+ prefix = f"[{subject}][{location}]"
115
+ else:
116
+ prefix = f"[{subject}]"
117
+
118
+ # Build message
119
+ if self.use_colors and sys.stdout.isatty():
120
+ subject_color = self.SUBJECT_COLORS.get(subject, self.RESET)
121
+ level_color = self.LEVEL_COLORS.get(record.levelno, self.RESET)
122
+
123
+ if record.levelno >= logging.WARNING:
124
+ prefix_str = f"{level_color}{prefix}{self.RESET}"
125
+ else:
126
+ prefix_str = f"{subject_color}{prefix}{self.RESET}"
127
+
128
+ message = f"{prefix_str} {record.getMessage()}"
129
+ else:
130
+ message = f"{prefix} {record.getMessage()}"
131
+
132
+ # Add timestamp if requested
133
+ if self.show_timestamp:
134
+ timestamp = self.formatTime(record, "%Y-%m-%d %H:%M:%S")
135
+ message = f"{timestamp} - {message}"
136
+
137
+ return message
138
+
139
+
140
+ class JSONFormatter(logging.Formatter):
141
+ """JSON formatter for structured logging."""
142
+
143
+ def format(self, record: logging.LogRecord) -> str:
144
+ """Format log record as JSON."""
145
+ log_data = {
146
+ "timestamp": time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(record.created)),
147
+ "level": record.levelname,
148
+ "subject": getattr(record, 'subject', 'INFO'),
149
+ "remote": getattr(record, 'remote', False),
150
+ "message": record.getMessage(),
151
+ "module": record.module,
152
+ "function": record.funcName,
153
+ "line": record.lineno,
154
+ }
155
+
156
+ # Add exception info if present
157
+ if record.exc_info:
158
+ log_data["exception"] = self.formatException(record.exc_info)
159
+
160
+ return json.dumps(log_data)
161
+
162
+
163
+ class RichHandler(logging.Handler):
164
+ """
165
+ Handler that integrates with Rich console for beautiful output.
166
+
167
+ Falls back to plain text if Rich is not available.
168
+ """
169
+
170
+ def __init__(self, level: int = logging.NOTSET, mute: bool = False):
171
+ super().__init__(level)
172
+ self.mute = mute
173
+ self._console: Any = None
174
+ self._escape: Any = None
175
+ self._init_rich()
176
+
177
+ def _init_rich(self) -> None:
178
+ """Initialize Rich console if available."""
179
+ try:
180
+ from rich.console import Console
181
+ from rich.markup import escape
182
+ from rich.theme import Theme
183
+
184
+ theme = Theme({
185
+ "info": "cyan",
186
+ "success": "green",
187
+ "warning": "yellow",
188
+ "error": "red bold",
189
+ "origin": "magenta",
190
+ "target": "blue",
191
+ "local": "cyan",
192
+ "debug": "dim",
193
+ })
194
+ self._console = Console(theme=theme)
195
+ self._escape = escape
196
+ except ImportError:
197
+ pass
198
+
199
+ def _get_style(self, subject: str, level: int) -> str:
200
+ """Get Rich style based on subject and level."""
201
+ if level >= logging.ERROR:
202
+ return "error"
203
+ if level >= logging.WARNING:
204
+ return "warning"
205
+ return subject.lower() if subject.lower() in ("origin", "target", "local") else "info"
206
+
207
+ def emit(self, record: logging.LogRecord) -> None:
208
+ """Emit a log record."""
209
+ if self.mute and record.levelno < logging.ERROR:
210
+ return
211
+
212
+ try:
213
+ subject = getattr(record, 'subject', 'INFO')
214
+ remote = getattr(record, 'remote', False)
215
+ message = record.getMessage()
216
+
217
+ # Build prefix
218
+ if subject in ("ORIGIN", "TARGET"):
219
+ location = "REMOTE" if remote else "LOCAL"
220
+ prefix = f"[{subject}][{location}]"
221
+ else:
222
+ prefix = f"[{subject}]"
223
+
224
+ if self._console and self._escape:
225
+ style = self._get_style(subject, record.levelno)
226
+ esc = self._escape
227
+
228
+ # Format with Rich
229
+ if record.levelno >= logging.ERROR:
230
+ self._console.print(f"[{style}]{esc(prefix)} {esc(message)}[/{style}]")
231
+ elif record.levelno >= logging.WARNING:
232
+ self._console.print(f"[{style}]{esc(prefix)} {esc(message)}[/{style}]")
233
+ else:
234
+ self._console.print(f"[{style}]{esc(prefix)}[/{style}] {esc(message)}")
235
+ else:
236
+ # Fallback to plain formatter
237
+ formatter = SyncFormatter(use_colors=True)
238
+ print(formatter.format(record))
239
+
240
+ except Exception:
241
+ self.handleError(record)
242
+
243
+
244
+ class SyncLoggerAdapter(logging.LoggerAdapter): # type: ignore[type-arg]
245
+ """
246
+ Logger adapter that automatically adds subject context.
247
+
248
+ Usage:
249
+ logger = SyncLoggerAdapter(logging.getLogger("db_sync_tool"), subject="ORIGIN")
250
+ logger.info("Creating dump", extra={"remote": True})
251
+ """
252
+
253
+ extra: dict[str, Any] # Override type to be mutable dict
254
+
255
+ def __init__(self, logger: logging.Logger, subject: str = "INFO", remote: bool = False):
256
+ super().__init__(logger, {"subject": subject, "remote": remote})
257
+ self.subject = subject
258
+ self.default_remote = remote
259
+
260
+ def process(
261
+ self, msg: Any, kwargs: MutableMapping[str, Any]
262
+ ) -> tuple[Any, MutableMapping[str, Any]]:
263
+ """Process log message and add subject context."""
264
+ extra = kwargs.get("extra", {})
265
+ if isinstance(extra, dict):
266
+ extra.setdefault("subject", self.subject)
267
+ extra.setdefault("remote", self.default_remote)
268
+ kwargs["extra"] = extra
269
+ return msg, kwargs
270
+
271
+ # Global logger instances
272
+ _root_logger: logging.Logger | None = None
273
+ _logging_config: LoggingConfig = LoggingConfig()
274
+ _subject_loggers: dict[str, SyncLoggerAdapter] = {}
275
+
276
+
277
+ def init_logging(
278
+ verbose: int = 0,
279
+ mute: bool = False,
280
+ log_file: str | None = None,
281
+ json_logging: bool = False,
282
+ console_output: bool = False,
283
+ ) -> logging.Logger:
284
+ """
285
+ Initialize the logging system.
286
+
287
+ Args:
288
+ verbose: Verbosity level (0=normal, 1=verbose, 2=debug)
289
+ mute: Suppress non-error output
290
+ log_file: Path to log file (optional)
291
+ json_logging: Use JSON format for file logging
292
+ console_output: Add console handler (False when OutputManager handles console)
293
+
294
+ Returns:
295
+ Configured root logger
296
+ """
297
+ global _root_logger, _logging_config, _subject_loggers
298
+
299
+ _logging_config = LoggingConfig(
300
+ verbose=verbose,
301
+ mute=mute,
302
+ log_file=log_file,
303
+ json_logging=json_logging,
304
+ )
305
+
306
+ # Create or get root logger
307
+ _root_logger = logging.getLogger("db_sync_tool")
308
+ _root_logger.handlers.clear()
309
+
310
+ # Set level based on verbosity
311
+ if verbose >= 2:
312
+ _root_logger.setLevel(logging.DEBUG)
313
+ elif verbose >= 1:
314
+ _root_logger.setLevel(logging.INFO)
315
+ else:
316
+ _root_logger.setLevel(logging.INFO)
317
+
318
+ # Add console handler only if explicitly requested
319
+ # (When OutputManager handles console output, we skip this)
320
+ if console_output:
321
+ console_handler = RichHandler(mute=mute)
322
+ if verbose >= 2:
323
+ console_handler.setLevel(logging.DEBUG)
324
+ else:
325
+ console_handler.setLevel(logging.INFO)
326
+ _root_logger.addHandler(console_handler)
327
+
328
+ # Add file handler if log file specified
329
+ if log_file:
330
+ file_handler = logging.FileHandler(log_file)
331
+ file_handler.setLevel(logging.DEBUG)
332
+
333
+ if json_logging:
334
+ file_handler.setFormatter(JSONFormatter())
335
+ else:
336
+ file_handler.setFormatter(SyncFormatter(use_colors=False, show_timestamp=True))
337
+
338
+ _root_logger.addHandler(file_handler)
339
+
340
+ # Add NullHandler if no handlers were added (prevents "No handlers" warning)
341
+ if not _root_logger.handlers:
342
+ _root_logger.addHandler(logging.NullHandler())
343
+
344
+ # Clear cached subject loggers
345
+ _subject_loggers.clear()
346
+
347
+ return _root_logger
348
+
349
+
350
+ def get_sync_logger(
351
+ subject: str | Subject = Subject.INFO,
352
+ remote: bool = False,
353
+ ) -> SyncLoggerAdapter:
354
+ """
355
+ Get a logger adapter with subject context.
356
+
357
+ Args:
358
+ subject: Subject (ORIGIN, TARGET, LOCAL, INFO)
359
+ remote: Whether the operation is remote
360
+
361
+ Returns:
362
+ SyncLoggerAdapter with subject context
363
+ """
364
+ global _root_logger, _subject_loggers
365
+
366
+ if _root_logger is None:
367
+ init_logging()
368
+
369
+ # Normalize subject
370
+ if isinstance(subject, Subject):
371
+ subject_str = subject.value
372
+ else:
373
+ subject_str = subject.upper()
374
+
375
+ # Cache key includes remote status
376
+ cache_key = f"{subject_str}:{remote}"
377
+
378
+ if cache_key not in _subject_loggers:
379
+ _subject_loggers[cache_key] = SyncLoggerAdapter(
380
+ _root_logger, # type: ignore[arg-type]
381
+ subject=subject_str,
382
+ remote=remote,
383
+ )
384
+
385
+ return _subject_loggers[cache_key]
386
+
387
+
388
+ def reset_logging() -> None:
389
+ """Reset logging configuration (for testing)."""
390
+ global _root_logger, _subject_loggers, _logging_config
391
+
392
+ if _root_logger:
393
+ _root_logger.handlers.clear()
394
+
395
+ _root_logger = None
396
+ _subject_loggers.clear()
397
+ _logging_config = LoggingConfig()
398
+
399
+
400
+ # Convenience exports
401
+ def get_logger() -> logging.Logger:
402
+ """Get the root sync logger."""
403
+ global _root_logger
404
+ if _root_logger is None:
405
+ init_logging()
406
+ return _root_logger # type: ignore[return-value]
407
+
408
+
409
+ # Default logger for direct import
410
+ logger = get_sync_logger()