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.
- db_sync_tool/__main__.py +7 -252
- db_sync_tool/cli.py +733 -0
- db_sync_tool/database/process.py +94 -111
- db_sync_tool/database/utility.py +339 -121
- db_sync_tool/info.py +1 -1
- db_sync_tool/recipes/drupal.py +87 -12
- db_sync_tool/recipes/laravel.py +7 -6
- db_sync_tool/recipes/parsing.py +102 -0
- db_sync_tool/recipes/symfony.py +17 -28
- db_sync_tool/recipes/typo3.py +33 -54
- db_sync_tool/recipes/wordpress.py +13 -12
- db_sync_tool/remote/client.py +206 -71
- db_sync_tool/remote/file_transfer.py +303 -0
- db_sync_tool/remote/rsync.py +18 -15
- db_sync_tool/remote/system.py +2 -3
- db_sync_tool/remote/transfer.py +51 -47
- db_sync_tool/remote/utility.py +29 -30
- db_sync_tool/sync.py +52 -28
- db_sync_tool/utility/config.py +367 -0
- db_sync_tool/utility/config_resolver.py +573 -0
- db_sync_tool/utility/console.py +779 -0
- db_sync_tool/utility/exceptions.py +32 -0
- db_sync_tool/utility/helper.py +155 -148
- db_sync_tool/utility/info.py +53 -20
- db_sync_tool/utility/log.py +55 -31
- db_sync_tool/utility/logging_config.py +410 -0
- db_sync_tool/utility/mode.py +85 -150
- db_sync_tool/utility/output.py +122 -51
- db_sync_tool/utility/parser.py +33 -53
- db_sync_tool/utility/pure.py +93 -0
- db_sync_tool/utility/security.py +79 -0
- db_sync_tool/utility/system.py +277 -194
- db_sync_tool/utility/validation.py +2 -9
- db_sync_tool_kmi-3.0.2.dist-info/METADATA +99 -0
- db_sync_tool_kmi-3.0.2.dist-info/RECORD +44 -0
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/WHEEL +1 -1
- db_sync_tool_kmi-2.11.6.dist-info/METADATA +0 -276
- db_sync_tool_kmi-2.11.6.dist-info/RECORD +0 -34
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info}/entry_points.txt +0 -0
- {db_sync_tool_kmi-2.11.6.dist-info → db_sync_tool_kmi-3.0.2.dist-info/licenses}/LICENSE +0 -0
- {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)
|