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,32 @@
|
|
|
1
|
+
"""Custom exception hierarchy for db-sync-tool."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class DbSyncError(Exception):
|
|
5
|
+
"""Base exception for all db-sync-tool errors."""
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ConfigError(DbSyncError):
|
|
10
|
+
"""Configuration and file access errors."""
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class NoConfigFoundError(ConfigError):
|
|
15
|
+
"""No configuration found during auto-discovery.
|
|
16
|
+
|
|
17
|
+
This is raised when ConfigResolver cannot find any configuration
|
|
18
|
+
(no project configs, no global hosts, no explicit file).
|
|
19
|
+
This is distinct from ConfigError which indicates a problem with
|
|
20
|
+
an existing config file (parse error, invalid format, etc.).
|
|
21
|
+
"""
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ParsingError(DbSyncError):
|
|
26
|
+
"""Framework configuration parsing errors."""
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ValidationError(DbSyncError):
|
|
31
|
+
"""Input validation errors."""
|
|
32
|
+
pass
|
db_sync_tool/utility/helper.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: future_fstrings -*-
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
Helper script
|
|
@@ -7,26 +6,33 @@ Helper script
|
|
|
7
6
|
|
|
8
7
|
import shutil
|
|
9
8
|
import os
|
|
10
|
-
import re
|
|
11
9
|
from db_sync_tool.utility import mode, system, output
|
|
10
|
+
from db_sync_tool.utility.security import quote_shell_arg # noqa: F401 (re-export)
|
|
11
|
+
from db_sync_tool.utility.pure import ( # noqa: F401 (re-export)
|
|
12
|
+
parse_version, get_file_from_path, remove_surrounding_quotes,
|
|
13
|
+
clean_db_config, dict_to_args, remove_multiple_elements_from_string
|
|
14
|
+
)
|
|
12
15
|
from db_sync_tool.remote import utility as remote_utility
|
|
13
16
|
|
|
14
17
|
|
|
15
|
-
def clean_up():
|
|
18
|
+
def clean_up() -> None:
|
|
16
19
|
"""
|
|
17
|
-
Clean up
|
|
18
|
-
:return:
|
|
20
|
+
Clean up temporary files and resources
|
|
19
21
|
"""
|
|
20
|
-
|
|
22
|
+
# Note: MySQL config files are cleaned up in sync.py's finally block
|
|
23
|
+
# to ensure cleanup even on errors
|
|
24
|
+
cfg = system.get_typed_config()
|
|
25
|
+
|
|
26
|
+
# Skip database cleanup for files-only mode or import mode
|
|
27
|
+
if not mode.is_import() and not cfg.files_only:
|
|
21
28
|
remote_utility.remove_target_database_dump()
|
|
22
29
|
if mode.get_sync_mode() == mode.SyncMode.PROXY:
|
|
23
30
|
remove_temporary_data_dir()
|
|
24
31
|
|
|
25
32
|
|
|
26
|
-
def remove_temporary_data_dir():
|
|
33
|
+
def remove_temporary_data_dir() -> None:
|
|
27
34
|
"""
|
|
28
35
|
Remove temporary data directory for storing database dump files
|
|
29
|
-
:return:
|
|
30
36
|
"""
|
|
31
37
|
if os.path.exists(system.default_local_sync_path):
|
|
32
38
|
output.message(
|
|
@@ -37,31 +43,31 @@ def remove_temporary_data_dir():
|
|
|
37
43
|
shutil.rmtree(system.default_local_sync_path)
|
|
38
44
|
|
|
39
45
|
|
|
40
|
-
def clean_up_dump_dir(client, path, num=5):
|
|
46
|
+
def clean_up_dump_dir(client: str, path: str, num: int = 5) -> None:
|
|
41
47
|
"""
|
|
42
|
-
Clean up the dump directory from old dump files (only affect .sql and .
|
|
43
|
-
:param client:
|
|
44
|
-
:param path:
|
|
45
|
-
:param num:
|
|
46
|
-
:return:
|
|
48
|
+
Clean up the dump directory from old dump files (only affect .sql and .gz files)
|
|
49
|
+
:param client: Client identifier
|
|
50
|
+
:param path: Path to dump directory
|
|
51
|
+
:param num: Number of files to keep
|
|
47
52
|
"""
|
|
48
53
|
# Distinguish stat command on os system (Darwin|Linux)
|
|
49
54
|
if check_os(client).strip() == 'Darwin':
|
|
50
55
|
_command = get_command(client, 'stat') + ' -f "%Sm %N" ' + path + ' | ' + get_command(
|
|
51
56
|
client,
|
|
52
57
|
'sort') + ' -rn | ' + get_command(
|
|
53
|
-
client, 'grep') + ' -E "
|
|
58
|
+
client, 'grep') + ' -E "\\.gz$|\\.sql$"'
|
|
54
59
|
else:
|
|
55
60
|
_command = get_command(client, 'stat') + ' -c "%y %n" ' + path + ' | ' + \
|
|
56
61
|
get_command(client,'sort') + ' -rn | ' + get_command(client, 'grep') + \
|
|
57
|
-
' -E "
|
|
62
|
+
' -E "\\.gz$|\\.sql$"'
|
|
58
63
|
|
|
59
64
|
# List files in directory sorted by change date
|
|
60
|
-
|
|
65
|
+
_result = mode.run_command(
|
|
61
66
|
_command,
|
|
62
67
|
client,
|
|
63
68
|
True
|
|
64
|
-
)
|
|
69
|
+
)
|
|
70
|
+
_files = _result.splitlines() if _result else []
|
|
65
71
|
|
|
66
72
|
for i in range(len(_files)):
|
|
67
73
|
_filename = _files[i].rsplit(' ', 1)[-1]
|
|
@@ -74,218 +80,197 @@ def clean_up_dump_dir(client, path, num=5):
|
|
|
74
80
|
)
|
|
75
81
|
|
|
76
82
|
|
|
77
|
-
def check_os(client):
|
|
83
|
+
def check_os(client: str) -> str:
|
|
78
84
|
"""
|
|
79
85
|
Check which system is running (Linux|Darwin)
|
|
80
|
-
:param client:
|
|
81
|
-
:return:
|
|
86
|
+
:param client: Client identifier
|
|
87
|
+
:return: OS name
|
|
82
88
|
"""
|
|
83
|
-
|
|
89
|
+
result = mode.run_command(
|
|
84
90
|
get_command(client, 'uname') + ' -s',
|
|
85
91
|
client,
|
|
86
92
|
True
|
|
87
93
|
)
|
|
94
|
+
return result if result else ''
|
|
88
95
|
|
|
89
96
|
|
|
90
|
-
def get_command(client, command):
|
|
97
|
+
def get_command(client: str, command: str) -> str:
|
|
91
98
|
"""
|
|
92
99
|
Get command helper for overriding default commands on the given client
|
|
93
|
-
:param client:
|
|
94
|
-
:param command:
|
|
100
|
+
:param client: Client identifier
|
|
101
|
+
:param command: Command name
|
|
95
102
|
:return: String command
|
|
96
103
|
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
104
|
+
cfg = system.get_typed_config()
|
|
105
|
+
client_cfg = cfg.get_client(client)
|
|
106
|
+
if command in client_cfg.console:
|
|
107
|
+
return client_cfg.console[command]
|
|
100
108
|
return command
|
|
101
109
|
|
|
102
110
|
|
|
103
|
-
def get_dump_dir(client):
|
|
111
|
+
def get_dump_dir(client: str) -> str:
|
|
104
112
|
"""
|
|
105
113
|
Get database dump directory by client
|
|
106
|
-
:param client:
|
|
114
|
+
:param client: Client identifier
|
|
107
115
|
:return: String path
|
|
108
116
|
"""
|
|
109
|
-
|
|
117
|
+
cfg = system.get_typed_config()
|
|
118
|
+
# Check if using default dump dir
|
|
119
|
+
if client == 'origin':
|
|
120
|
+
use_default = cfg.default_origin_dump_dir
|
|
121
|
+
else:
|
|
122
|
+
use_default = cfg.default_target_dump_dir
|
|
123
|
+
|
|
124
|
+
if use_default:
|
|
110
125
|
return '/tmp/'
|
|
111
126
|
else:
|
|
112
|
-
return
|
|
127
|
+
return cfg.get_client(client).dump_dir
|
|
113
128
|
|
|
114
129
|
|
|
115
|
-
def check_and_create_dump_dir(client, path):
|
|
130
|
+
def check_and_create_dump_dir(client: str, path: str) -> None:
|
|
116
131
|
"""
|
|
117
132
|
Check if a path exists on the client system and creates the given path if necessary
|
|
118
|
-
:param client:
|
|
119
|
-
:param path:
|
|
120
|
-
:return:
|
|
133
|
+
:param client: Client identifier
|
|
134
|
+
:param path: Path to check/create
|
|
121
135
|
"""
|
|
136
|
+
_safe_path = quote_shell_arg(path)
|
|
122
137
|
mode.run_command(
|
|
123
|
-
'[ ! -d
|
|
138
|
+
'[ ! -d ' + _safe_path + ' ] && mkdir -p ' + _safe_path,
|
|
124
139
|
client
|
|
125
140
|
)
|
|
126
141
|
|
|
127
142
|
|
|
128
|
-
def get_ssh_host_name(client, with_user=False, minimal=False):
|
|
143
|
+
def get_ssh_host_name(client: str, with_user: bool = False, minimal: bool = False) -> str:
|
|
129
144
|
"""
|
|
130
145
|
Format ssh host name depending on existing client name
|
|
131
|
-
:param client:
|
|
132
|
-
:param with_user:
|
|
133
|
-
:param
|
|
134
|
-
:return:
|
|
146
|
+
:param client: Client identifier
|
|
147
|
+
:param with_user: Include username in output
|
|
148
|
+
:param minimal: Return minimal format
|
|
149
|
+
:return: Formatted host name
|
|
135
150
|
"""
|
|
136
|
-
|
|
151
|
+
cfg = system.get_typed_config()
|
|
152
|
+
client_cfg = cfg.get_client(client)
|
|
153
|
+
|
|
154
|
+
if not client_cfg.user and not client_cfg.host:
|
|
137
155
|
return ''
|
|
138
156
|
|
|
139
157
|
if with_user:
|
|
140
|
-
_host =
|
|
158
|
+
_host = client_cfg.user + '@' + client_cfg.host
|
|
141
159
|
else:
|
|
142
|
-
_host =
|
|
160
|
+
_host = client_cfg.host
|
|
143
161
|
|
|
144
|
-
if
|
|
162
|
+
if client_cfg.name:
|
|
145
163
|
if minimal:
|
|
146
|
-
return
|
|
164
|
+
return client_cfg.name
|
|
147
165
|
else:
|
|
148
|
-
return output.CliFormat.BOLD +
|
|
149
|
-
|
|
150
|
-
|
|
166
|
+
return (output.CliFormat.BOLD + client_cfg.name +
|
|
167
|
+
output.CliFormat.ENDC + output.CliFormat.BLACK +
|
|
168
|
+
' (' + _host + ')' + output.CliFormat.ENDC)
|
|
151
169
|
else:
|
|
152
170
|
return _host
|
|
153
171
|
|
|
154
172
|
|
|
155
|
-
def create_local_temporary_data_dir():
|
|
173
|
+
def create_local_temporary_data_dir() -> None:
|
|
156
174
|
"""
|
|
157
|
-
Create local temporary data dir
|
|
158
|
-
:return:
|
|
175
|
+
Create local temporary data dir with secure permissions
|
|
159
176
|
"""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
:param dict: Dictionary
|
|
169
|
-
:return: List
|
|
170
|
-
"""
|
|
171
|
-
_args = []
|
|
172
|
-
for key, val in dict.items():
|
|
173
|
-
if isinstance(val, bool):
|
|
174
|
-
if val:
|
|
175
|
-
_args.append(f'--{key}')
|
|
176
|
-
else:
|
|
177
|
-
_args.append(f'--{key}')
|
|
178
|
-
_args.append(str(val))
|
|
179
|
-
if len(_args) == 0:
|
|
180
|
-
return None
|
|
181
|
-
return _args
|
|
177
|
+
cfg = system.get_typed_config()
|
|
178
|
+
# Skip secure permissions for user-specified keep_dump directories
|
|
179
|
+
if cfg.keep_dump:
|
|
180
|
+
if not os.path.exists(system.default_local_sync_path):
|
|
181
|
+
os.makedirs(system.default_local_sync_path)
|
|
182
|
+
else:
|
|
183
|
+
# Use secure temp dir creation with 0700 permissions
|
|
184
|
+
system.create_secure_temp_dir(system.default_local_sync_path)
|
|
182
185
|
|
|
183
186
|
|
|
184
|
-
def check_file_exists(client, path):
|
|
187
|
+
def check_file_exists(client: str, path: str) -> bool:
|
|
185
188
|
"""
|
|
186
189
|
Check if a file exists
|
|
187
|
-
:param client:
|
|
188
|
-
:param path:
|
|
190
|
+
:param client: Client identifier
|
|
191
|
+
:param path: File path
|
|
189
192
|
:return: Boolean
|
|
190
193
|
"""
|
|
191
|
-
|
|
194
|
+
_safe_path = quote_shell_arg(path)
|
|
195
|
+
return mode.run_command(f'[ -f {_safe_path} ] && echo "1"', client, True) == '1'
|
|
192
196
|
|
|
193
197
|
|
|
194
|
-
def run_script(client=None, script='before'):
|
|
198
|
+
def run_script(client: str | None = None, script: str = 'before') -> None:
|
|
195
199
|
"""
|
|
196
200
|
Executing script command
|
|
197
|
-
:param client:
|
|
198
|
-
:param script:
|
|
199
|
-
:return:
|
|
201
|
+
:param client: Client identifier (or None for global scripts)
|
|
202
|
+
:param script: Script name ('before', 'after', 'error')
|
|
200
203
|
"""
|
|
204
|
+
cfg = system.get_typed_config()
|
|
205
|
+
|
|
201
206
|
if client is None:
|
|
202
|
-
|
|
207
|
+
# Global scripts
|
|
208
|
+
scripts_dict = cfg.scripts
|
|
203
209
|
_subject = output.Subject.LOCAL
|
|
204
210
|
client = mode.Client.LOCAL
|
|
205
211
|
else:
|
|
206
|
-
|
|
212
|
+
# Client-specific scripts
|
|
213
|
+
client_cfg = cfg.get_client(client)
|
|
214
|
+
scripts_dict = client_cfg.scripts
|
|
207
215
|
_subject = output.host_to_subject(client)
|
|
208
216
|
|
|
209
|
-
if
|
|
210
|
-
return
|
|
211
|
-
|
|
212
|
-
if f'{script}' in _config['scripts']:
|
|
217
|
+
if script in scripts_dict:
|
|
213
218
|
output.message(
|
|
214
219
|
_subject,
|
|
215
220
|
f'Running script {client}',
|
|
216
221
|
True
|
|
217
222
|
)
|
|
218
223
|
mode.run_command(
|
|
219
|
-
|
|
224
|
+
scripts_dict[script],
|
|
220
225
|
client
|
|
221
226
|
)
|
|
222
227
|
|
|
223
228
|
|
|
224
|
-
def
|
|
225
|
-
"""
|
|
226
|
-
Check rsync version
|
|
227
|
-
:return:
|
|
229
|
+
def _check_tool_version(tool: str, version_flag: str = '--version') -> str | None:
|
|
228
230
|
"""
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
mode.Client.LOCAL,
|
|
232
|
-
True
|
|
233
|
-
)
|
|
234
|
-
_version = parse_version(_raw_version)
|
|
235
|
-
output.message(
|
|
236
|
-
output.Subject.LOCAL,
|
|
237
|
-
f'rsync version {_version}'
|
|
238
|
-
)
|
|
239
|
-
|
|
231
|
+
Check if a tool is available and return its version.
|
|
232
|
+
DRY helper for version checks.
|
|
240
233
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
:return:
|
|
234
|
+
:param tool: Tool name
|
|
235
|
+
:param version_flag: Flag to get version
|
|
236
|
+
:return: Version string or None
|
|
245
237
|
"""
|
|
246
|
-
|
|
247
|
-
'
|
|
238
|
+
raw_version = mode.run_command(
|
|
239
|
+
f'{tool} {version_flag}',
|
|
248
240
|
mode.Client.LOCAL,
|
|
249
241
|
force_output=True,
|
|
250
242
|
allow_fail=True
|
|
251
243
|
)
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
if _version:
|
|
255
|
-
output.message(
|
|
256
|
-
output.Subject.LOCAL,
|
|
257
|
-
f'sshpass version {_version}'
|
|
258
|
-
)
|
|
259
|
-
system.config['use_sshpass'] = True
|
|
260
|
-
return True
|
|
244
|
+
return parse_version(raw_version)
|
|
261
245
|
|
|
262
246
|
|
|
263
|
-
def
|
|
247
|
+
def check_rsync_version() -> bool:
|
|
264
248
|
"""
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
:
|
|
268
|
-
:return:
|
|
249
|
+
Check rsync version and availability.
|
|
250
|
+
|
|
251
|
+
:return: True if rsync is available, False otherwise
|
|
269
252
|
"""
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
else:
|
|
276
|
-
return None
|
|
253
|
+
version = _check_tool_version('rsync')
|
|
254
|
+
if version:
|
|
255
|
+
output.message(output.Subject.LOCAL, f'rsync version {version}')
|
|
256
|
+
return True
|
|
257
|
+
return False
|
|
277
258
|
|
|
278
259
|
|
|
279
|
-
def
|
|
260
|
+
def check_sshpass_version() -> bool | None:
|
|
280
261
|
"""
|
|
281
|
-
|
|
282
|
-
:
|
|
283
|
-
:return: file
|
|
262
|
+
Check sshpass version
|
|
263
|
+
:return: True if available, None otherwise
|
|
284
264
|
"""
|
|
285
|
-
|
|
265
|
+
version = _check_tool_version('sshpass', '-V')
|
|
266
|
+
if version:
|
|
267
|
+
output.message(output.Subject.LOCAL, f'sshpass version {version}')
|
|
268
|
+
system.set_use_sshpass(True)
|
|
269
|
+
return True
|
|
270
|
+
return None
|
|
286
271
|
|
|
287
272
|
|
|
288
|
-
def confirm(prompt=None, resp=False):
|
|
273
|
+
def confirm(prompt: str | None = None, resp: bool = False) -> bool:
|
|
289
274
|
"""
|
|
290
275
|
https://code.activestate.com/recipes/541096-prompt-the-user-for-confirmation/
|
|
291
276
|
|
|
@@ -308,18 +293,40 @@ def confirm(prompt=None, resp=False):
|
|
|
308
293
|
prompt = 'Confirm'
|
|
309
294
|
|
|
310
295
|
if resp:
|
|
311
|
-
prompt = '
|
|
296
|
+
prompt = f'{prompt} [Y|n]: '
|
|
312
297
|
else:
|
|
313
|
-
prompt = '
|
|
298
|
+
prompt = f'{prompt} [y|N]: '
|
|
314
299
|
|
|
315
300
|
while True:
|
|
316
|
-
ans = input(prompt)
|
|
301
|
+
ans = input(prompt).lower()
|
|
317
302
|
if not ans:
|
|
318
303
|
return resp
|
|
319
|
-
if ans
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
304
|
+
if ans in ('y', 'n'):
|
|
305
|
+
return ans == 'y'
|
|
306
|
+
print('Please enter y or n.')
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def run_sed_command(client: str, command: str) -> str:
|
|
310
|
+
"""
|
|
311
|
+
Executes a sed command on the specified client, trying -E first and falling back to -r if -E fails.
|
|
312
|
+
|
|
313
|
+
:param client: The client on which the sed command should be executed.
|
|
314
|
+
:param command: The sed command to execute (excluding the sed options).
|
|
315
|
+
:return: The result of the sed command as a cleaned string (with newlines removed).
|
|
316
|
+
"""
|
|
317
|
+
# Check if the client supports -E or -r option for sed
|
|
318
|
+
option = mode.run_command(
|
|
319
|
+
f"echo | {get_command(client, 'sed')} -E '' >/dev/null 2>&1 && echo -E || (echo | {get_command(client, 'sed')} -r '' >/dev/null 2>&1 && echo -r)",
|
|
320
|
+
client,
|
|
321
|
+
True
|
|
322
|
+
)
|
|
323
|
+
# If neither option is supported, default to -E
|
|
324
|
+
if not option:
|
|
325
|
+
option = '-E'
|
|
326
|
+
|
|
327
|
+
result = mode.run_command(
|
|
328
|
+
f"{get_command(client, 'sed')} -n {option} {command}",
|
|
329
|
+
client,
|
|
330
|
+
True
|
|
331
|
+
)
|
|
332
|
+
return result.strip().replace('\n', '') if result else ''
|
db_sync_tool/utility/info.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: future_fstrings -*-
|
|
3
2
|
|
|
4
3
|
"""
|
|
5
4
|
|
|
@@ -11,31 +10,66 @@ from db_sync_tool.utility import mode, system, output
|
|
|
11
10
|
from db_sync_tool import info
|
|
12
11
|
|
|
13
12
|
|
|
14
|
-
def print_header(mute):
|
|
13
|
+
def print_header(mute, verbose=0):
|
|
15
14
|
"""
|
|
16
15
|
Printing console header
|
|
17
16
|
:param mute: Boolean
|
|
17
|
+
:param verbose: int - 0=compact, 1+=full header
|
|
18
18
|
:return:
|
|
19
19
|
"""
|
|
20
|
-
# pylint: max-line-length=240
|
|
21
20
|
if mute is False:
|
|
22
21
|
_colors = get_random_colors()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
print(output.CliFormat.BLACK + '# ' + info.__homepage__ + ' #' + output.CliFormat.ENDC)
|
|
32
|
-
print(
|
|
33
|
-
output.CliFormat.BLACK + '# #' + output.CliFormat.ENDC)
|
|
34
|
-
print(
|
|
35
|
-
output.CliFormat.BLACK + '##############################################' + output.CliFormat.ENDC)
|
|
22
|
+
if verbose >= 1:
|
|
23
|
+
# Full header for verbose mode using Rich Panel
|
|
24
|
+
_print_rich_header(_colors)
|
|
25
|
+
else:
|
|
26
|
+
# Compact header for default mode
|
|
27
|
+
print(
|
|
28
|
+
output.CliFormat.BLACK + _colors[0] + '⥣ ' + _colors[1] + '⥥ ' + output.CliFormat.ENDC +
|
|
29
|
+
output.CliFormat.BLACK + 'db-sync-tool v' + info.__version__ + output.CliFormat.ENDC)
|
|
36
30
|
check_updates()
|
|
37
31
|
|
|
38
32
|
|
|
33
|
+
def _print_rich_header(_colors):
|
|
34
|
+
"""Print header using Rich if available, fallback to ASCII."""
|
|
35
|
+
try:
|
|
36
|
+
from rich.console import Console
|
|
37
|
+
from rich.text import Text
|
|
38
|
+
|
|
39
|
+
console = Console(force_terminal=True)
|
|
40
|
+
|
|
41
|
+
# Build simple header line
|
|
42
|
+
header = Text()
|
|
43
|
+
header.append("⥣ ", style=_color_to_rich(_colors[0]))
|
|
44
|
+
header.append("⥥ ", style=_color_to_rich(_colors[1]))
|
|
45
|
+
header.append("db-sync-tool ", style="bold")
|
|
46
|
+
header.append(f"v{info.__version__}", style="dim")
|
|
47
|
+
|
|
48
|
+
console.print(header)
|
|
49
|
+
console.print() # Empty line after header
|
|
50
|
+
except ImportError:
|
|
51
|
+
# Fallback to simple ASCII header
|
|
52
|
+
print(
|
|
53
|
+
_colors[0] + '⥣ ' + _colors[1] + '⥥ ' + output.CliFormat.ENDC +
|
|
54
|
+
output.CliFormat.BOLD + 'db-sync-tool ' + output.CliFormat.ENDC +
|
|
55
|
+
output.CliFormat.BLACK + 'v' + info.__version__ + output.CliFormat.ENDC
|
|
56
|
+
)
|
|
57
|
+
print()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _color_to_rich(cli_color):
|
|
61
|
+
"""Convert CliFormat color to Rich style."""
|
|
62
|
+
color_map = {
|
|
63
|
+
output.CliFormat.BEIGE: "cyan",
|
|
64
|
+
output.CliFormat.PURPLE: "magenta",
|
|
65
|
+
output.CliFormat.BLUE: "blue",
|
|
66
|
+
output.CliFormat.YELLOW: "yellow",
|
|
67
|
+
output.CliFormat.GREEN: "green",
|
|
68
|
+
output.CliFormat.RED: "red",
|
|
69
|
+
}
|
|
70
|
+
return color_map.get(cli_color, "white")
|
|
71
|
+
|
|
72
|
+
|
|
39
73
|
def check_updates():
|
|
40
74
|
"""
|
|
41
75
|
Check for updates of the db_sync_tool
|
|
@@ -60,11 +94,10 @@ def print_footer():
|
|
|
60
94
|
Printing console footer
|
|
61
95
|
:return:
|
|
62
96
|
"""
|
|
63
|
-
|
|
97
|
+
cfg = system.get_typed_config()
|
|
98
|
+
if cfg.dry_run:
|
|
64
99
|
_message = 'Successfully executed dry run'
|
|
65
|
-
elif not
|
|
66
|
-
not system.config['is_same_client'] and \
|
|
67
|
-
not mode.is_import():
|
|
100
|
+
elif not cfg.keep_dump and not cfg.is_same_client and not mode.is_import():
|
|
68
101
|
_message = 'Successfully synchronized databases'
|
|
69
102
|
elif mode.is_import():
|
|
70
103
|
_message = 'Successfully imported database dump'
|