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
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Configuration dataclasses for db-sync-tool.
5
+
6
+ Provides typed configuration objects that can be created from the legacy
7
+ config dict. This enables gradual migration to typed configuration while
8
+ maintaining backwards compatibility.
9
+
10
+ Usage:
11
+ from db_sync_tool.utility import system
12
+ from db_sync_tool.utility.config import SyncConfig
13
+
14
+ # Convert legacy config to typed object
15
+ cfg = SyncConfig.from_dict(system.config)
16
+
17
+ # Access with type safety
18
+ print(cfg.origin.db.host)
19
+ print(cfg.verbose)
20
+ """
21
+
22
+ from dataclasses import dataclass, field
23
+
24
+
25
+ def _get(data: dict, key: str, default):
26
+ """Get value from dict, treating None as missing (returns default)."""
27
+ value = data.get(key)
28
+ return default if value is None else value
29
+
30
+
31
+ def _get_int(data: dict, key: str, default: int) -> int:
32
+ """Get int value from dict, with safe conversion."""
33
+ value = data.get(key)
34
+ if value is None:
35
+ return default
36
+ try:
37
+ return int(value)
38
+ except (TypeError, ValueError):
39
+ return default
40
+
41
+
42
+ def _get_list(data: dict, key: str, fallback_key: str | None = None) -> list:
43
+ """Get list value from dict, with optional fallback key."""
44
+ value = data.get(key)
45
+ if value is not None:
46
+ return value
47
+ if fallback_key:
48
+ return data.get(fallback_key) or []
49
+ return []
50
+
51
+
52
+ @dataclass
53
+ class DatabaseConfig:
54
+ """Database connection configuration."""
55
+ name: str = ''
56
+ host: str = ''
57
+ user: str = ''
58
+ password: str = ''
59
+ port: int = 0 # 0 means use default (3306 for MySQL)
60
+
61
+ @classmethod
62
+ def from_dict(cls, data: dict | None) -> 'DatabaseConfig':
63
+ """Create DatabaseConfig from dict."""
64
+ if not data:
65
+ return cls()
66
+ return cls(
67
+ name=_get(data, 'name', ''),
68
+ host=_get(data, 'host', ''),
69
+ user=_get(data, 'user', ''),
70
+ password=_get(data, 'password', ''),
71
+ port=_get_int(data, 'port', 0),
72
+ )
73
+
74
+
75
+ @dataclass
76
+ class JumpHostConfig:
77
+ """SSH jump host configuration."""
78
+ host: str = ''
79
+ user: str = ''
80
+ password: str | None = None
81
+ ssh_key: str | None = None
82
+ port: int = 22
83
+ private: str | None = None
84
+ name: str | None = None
85
+
86
+ @classmethod
87
+ def from_dict(cls, data: dict | None) -> 'JumpHostConfig | None':
88
+ """Create JumpHostConfig from dict."""
89
+ if not data:
90
+ return None
91
+ return cls(
92
+ host=_get(data, 'host', ''),
93
+ user=_get(data, 'user', ''),
94
+ password=data.get('password'), # None is valid
95
+ ssh_key=data.get('ssh_key'), # None is valid
96
+ port=_get_int(data, 'port', 22),
97
+ private=data.get('private'), # None is valid
98
+ name=data.get('name'), # None is valid
99
+ )
100
+
101
+
102
+ @dataclass
103
+ class FileTransferConfig:
104
+ """Configuration for a single file transfer entry."""
105
+ origin: str = ''
106
+ target: str = ''
107
+ exclude: list[str] = field(default_factory=list)
108
+ options: str | None = None # Per-transfer rsync options
109
+
110
+ @classmethod
111
+ def from_dict(cls, data: dict | None) -> 'FileTransferConfig':
112
+ """Create FileTransferConfig from dict."""
113
+ if not data:
114
+ return cls()
115
+ return cls(
116
+ origin=_get(data, 'origin', ''),
117
+ target=_get(data, 'target', ''),
118
+ exclude=_get_list(data, 'exclude'),
119
+ options=data.get('options'), # None is valid
120
+ )
121
+
122
+
123
+ @dataclass
124
+ class ClientConfig:
125
+ """Configuration for origin or target client."""
126
+ path: str = ''
127
+ name: str = ''
128
+ host: str = ''
129
+ user: str = ''
130
+ password: str | None = None
131
+ ssh_key: str | None = None
132
+ port: int = 22
133
+ dump_dir: str = '/tmp/'
134
+ keep_dumps: int | None = None
135
+ db: DatabaseConfig = field(default_factory=DatabaseConfig)
136
+ jump_host: JumpHostConfig | None = None
137
+ after_dump: str | None = None
138
+ post_sql: list[str] = field(default_factory=list)
139
+ # Dynamic fields for framework-specific features
140
+ console: dict[str, str] = field(default_factory=dict)
141
+ scripts: dict[str, str] = field(default_factory=dict)
142
+ protect: bool = False
143
+ link: str = ''
144
+
145
+ @classmethod
146
+ def from_dict(cls, data: dict | None) -> 'ClientConfig':
147
+ """Create ClientConfig from dict."""
148
+ if not data:
149
+ return cls()
150
+ return cls(
151
+ path=_get(data, 'path', ''),
152
+ name=_get(data, 'name', ''),
153
+ host=_get(data, 'host', ''),
154
+ user=_get(data, 'user', ''),
155
+ password=data.get('password'), # None is valid
156
+ ssh_key=data.get('ssh_key'), # None is valid
157
+ port=_get_int(data, 'port', 22),
158
+ dump_dir=_get(data, 'dump_dir', '/tmp/'),
159
+ keep_dumps=data.get('keep_dumps'), # None is valid
160
+ db=DatabaseConfig.from_dict(data.get('db')),
161
+ jump_host=JumpHostConfig.from_dict(data.get('jump_host')),
162
+ after_dump=data.get('after_dump'), # None is valid
163
+ post_sql=_get_list(data, 'post_sql'),
164
+ console=data.get('console') or {},
165
+ scripts=data.get('scripts') or {},
166
+ protect=_get(data, 'protect', False),
167
+ link=_get(data, 'link', ''),
168
+ )
169
+
170
+ @property
171
+ def is_remote(self) -> bool:
172
+ """Check if this client is remote (has host configured)."""
173
+ return bool(self.host)
174
+
175
+
176
+ @dataclass
177
+ class SyncConfig:
178
+ """Main configuration for database synchronization."""
179
+ # General options
180
+ verbose: bool = False
181
+ mute: bool = False
182
+ dry_run: bool = False
183
+ yes: bool = False
184
+ reverse: bool = False
185
+
186
+ # Dump options
187
+ keep_dump: bool = False
188
+ dump_name: str = ''
189
+ check_dump: bool = True
190
+ clear_database: bool = False
191
+
192
+ # Import options
193
+ import_file: str = ''
194
+
195
+ # Table options
196
+ tables: str = ''
197
+ where: str = ''
198
+ additional_mysqldump_options: str = ''
199
+ ignore_tables: list[str] = field(default_factory=list)
200
+ truncate_tables: list[str] = field(default_factory=list)
201
+
202
+ # Transfer options
203
+ use_rsync: bool = True
204
+ use_rsync_options: str | None = None
205
+ use_sshpass: bool = False
206
+
207
+ # File transfer options
208
+ files: list[FileTransferConfig] = field(default_factory=list)
209
+ files_options: str | None = None # Global rsync options for file transfer
210
+ with_files: bool = False # Enable file sync (opt-in)
211
+ files_only: bool = False # Sync only files, skip database
212
+
213
+ # SSH options
214
+ ssh_agent: bool = False
215
+ force_password: bool = False
216
+ ssh_password_origin: str | None = None
217
+ ssh_password_target: str | None = None
218
+
219
+ # Host linking
220
+ link_hosts: str = ''
221
+ link_origin: str | None = None
222
+ link_target: str | None = None
223
+
224
+ # Internal state
225
+ config_file_path: str | None = None
226
+ is_same_client: bool = False
227
+ default_origin_dump_dir: bool = True
228
+ default_target_dump_dir: bool = True
229
+ log_file: str | None = None
230
+ json_log: bool = False # Use JSON format for log file output
231
+
232
+ # Framework type
233
+ type: str | None = None
234
+
235
+ # Global scripts
236
+ scripts: dict[str, str] = field(default_factory=dict)
237
+
238
+ # Client configurations
239
+ origin: ClientConfig = field(default_factory=ClientConfig)
240
+ target: ClientConfig = field(default_factory=ClientConfig)
241
+
242
+ def get_client(self, client: str) -> ClientConfig:
243
+ """Get origin or target config by client identifier."""
244
+ if client == 'origin':
245
+ return self.origin
246
+ elif client == 'target':
247
+ return self.target
248
+ raise ValueError(f"Unknown client: {client}")
249
+
250
+ @staticmethod
251
+ def _parse_files_config(files_data) -> list['FileTransferConfig']:
252
+ """
253
+ Parse files configuration, supporting both new and legacy formats.
254
+
255
+ New format (flat list):
256
+ files:
257
+ - origin: fileadmin/
258
+ target: fileadmin/
259
+
260
+ Legacy format (nested):
261
+ files:
262
+ config:
263
+ - origin: fileadmin/
264
+ target: fileadmin/
265
+ """
266
+ if not files_data:
267
+ return []
268
+ if isinstance(files_data, dict) and 'config' in files_data:
269
+ # Legacy format: files.config[]
270
+ return [FileTransferConfig.from_dict(f) for f in files_data.get('config', [])]
271
+ if isinstance(files_data, list):
272
+ # New format: files[]
273
+ return [FileTransferConfig.from_dict(f) for f in files_data]
274
+ return []
275
+
276
+ @staticmethod
277
+ def _parse_files_options(files_data, files_options_direct) -> str | None:
278
+ """
279
+ Parse files_options, supporting both direct and legacy formats.
280
+
281
+ Direct: files_options: '--verbose'
282
+ Legacy: files: { option: ['--verbose', '--compress'] }
283
+ """
284
+ # Direct files_options takes precedence (explicit "" overrides legacy)
285
+ if files_options_direct is not None:
286
+ return files_options_direct
287
+ # Check legacy format
288
+ if isinstance(files_data, dict) and 'option' in files_data:
289
+ options = files_data.get('option', [])
290
+ if options:
291
+ return ' '.join(options)
292
+ return None
293
+
294
+ @classmethod
295
+ def from_dict(cls, data: dict) -> 'SyncConfig':
296
+ """
297
+ Create SyncConfig from legacy config dict.
298
+
299
+ :param data: Legacy config dictionary
300
+ :return: SyncConfig instance
301
+ """
302
+ # Extract SSH passwords from nested dict if present
303
+ ssh_passwords = data.get('ssh_password', {})
304
+ return cls(
305
+ # General options
306
+ verbose=_get(data, 'verbose', False),
307
+ mute=_get(data, 'mute', False),
308
+ dry_run=_get(data, 'dry_run', False),
309
+ yes=_get(data, 'yes', False),
310
+ reverse=_get(data, 'reverse', False),
311
+ # Dump options
312
+ keep_dump=_get(data, 'keep_dump', False),
313
+ dump_name=_get(data, 'dump_name', ''),
314
+ check_dump=_get(data, 'check_dump', True),
315
+ clear_database=_get(data, 'clear_database', False),
316
+ import_file=_get(data, 'import', ''),
317
+ # Table options
318
+ tables=_get(data, 'tables', ''),
319
+ where=_get(data, 'where', ''),
320
+ additional_mysqldump_options=_get(data, 'additional_mysqldump_options', ''),
321
+ ignore_tables=_get_list(data, 'ignore_tables', 'ignore_table'),
322
+ truncate_tables=_get_list(data, 'truncate_tables', 'truncate_table'),
323
+ # Transfer options
324
+ use_rsync=_get(data, 'use_rsync', True),
325
+ use_rsync_options=data.get('use_rsync_options'), # None is valid
326
+ use_sshpass=_get(data, 'use_sshpass', False),
327
+ # File transfer options
328
+ files=cls._parse_files_config(data.get('files')),
329
+ files_options=cls._parse_files_options(data.get('files'), data.get('files_options')),
330
+ with_files=_get(data, 'with_files', False),
331
+ files_only=_get(data, 'files_only', False),
332
+ # SSH options
333
+ ssh_agent=_get(data, 'ssh_agent', False),
334
+ force_password=_get(data, 'force_password', False),
335
+ ssh_password_origin=ssh_passwords.get('origin'),
336
+ ssh_password_target=ssh_passwords.get('target'),
337
+ # Host linking
338
+ link_hosts=_get(data, 'link_hosts', ''),
339
+ link_origin=data.get('link_origin'), # None is valid
340
+ link_target=data.get('link_target'), # None is valid
341
+ # Internal state
342
+ config_file_path=data.get('config_file_path'), # None is valid
343
+ is_same_client=_get(data, 'is_same_client', False),
344
+ default_origin_dump_dir=_get(data, 'default_origin_dump_dir', True),
345
+ default_target_dump_dir=_get(data, 'default_target_dump_dir', True),
346
+ log_file=data.get('log_file'), # None is valid
347
+ json_log=_get(data, 'json_log', False),
348
+ # Framework type
349
+ type=data.get('type'), # None is valid
350
+ # Global scripts
351
+ scripts=data.get('scripts') or {},
352
+ # Client configurations
353
+ origin=ClientConfig.from_dict(data.get('origin')),
354
+ target=ClientConfig.from_dict(data.get('target')),
355
+ )
356
+
357
+
358
+ def get_config() -> SyncConfig:
359
+ """
360
+ Get current configuration as typed SyncConfig object.
361
+
362
+ Convenience function that imports system.config and converts it.
363
+
364
+ :return: SyncConfig instance
365
+ """
366
+ from db_sync_tool.utility import system
367
+ return SyncConfig.from_dict(system.config)