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,16 +1,22 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: future_fstrings -*-
3
2
 
4
3
  """
5
4
  Utility script
6
5
  """
7
6
 
8
- import sys
9
7
  import datetime
10
8
  import re
9
+ import os
10
+ import secrets
11
+ import base64
11
12
  from db_sync_tool.utility import mode, system, helper, output
13
+ from db_sync_tool.utility.security import sanitize_table_name # noqa: F401 (re-export)
14
+ from db_sync_tool.utility.exceptions import ConfigError, DbSyncError
12
15
 
13
- database_dump_file_name = None
16
+ database_dump_file_name: str | None = None
17
+
18
+ # Track MySQL config files for cleanup (client -> path)
19
+ _mysql_config_files: dict[str, str] = {}
14
20
 
15
21
 
16
22
  class DatabaseSystem:
@@ -18,80 +24,209 @@ class DatabaseSystem:
18
24
  MARIADB = 'MariaDB'
19
25
 
20
26
 
21
- def run_database_command(client, command, force_database_name=False):
27
+ def create_mysql_config_file(client: str) -> str:
22
28
  """
23
- Run a database command using the "mysql -e" command
24
- :param client: String
25
- :param command: String database command
26
- :param force_database_name: Bool forces the database name
27
- :return:
29
+ Create a secure temporary MySQL config file with credentials.
30
+ This prevents passwords from appearing in process lists (ps aux).
31
+
32
+ :param client: String client identifier ('origin' or 'target')
33
+ :return: String path to the config file
34
+ """
35
+ global _mysql_config_files
36
+
37
+ cfg = system.get_typed_config()
38
+ client_cfg = cfg.get_client(client)
39
+
40
+ # Verify database config exists
41
+ if not client_cfg.db.name and not client_cfg.db.user:
42
+ raise ConfigError(f"Database configuration not found for client: {client}")
43
+
44
+ db_config = client_cfg.db
45
+
46
+ # Build config file content
47
+ # Passwords must be quoted to handle special chars like # ; $ etc.
48
+ # Inside double quotes, escape \ and "
49
+ escaped_password = db_config.password.replace('\\', '\\\\').replace('"', '\\"')
50
+ config_content = "[client]\n"
51
+ config_content += f"user={db_config.user}\n"
52
+ config_content += f'password="{escaped_password}"\n'
53
+ if db_config.host:
54
+ config_content += f"host={db_config.host}\n"
55
+ if db_config.port:
56
+ config_content += f"port={db_config.port}\n"
57
+
58
+ random_suffix = secrets.token_hex(8)
59
+ config_path = f"/tmp/.my_{random_suffix}.cnf"
60
+
61
+ if mode.is_remote(client):
62
+ # For remote clients, create config file on remote system
63
+ # Using base64 encoding to safely handle special characters in passwords
64
+ encoded_content = base64.b64encode(config_content.encode()).decode()
65
+ # Use force_output=True to ensure command completes before proceeding
66
+ result = mode.run_command(
67
+ f"echo '{encoded_content}' | base64 -d > {config_path} && chmod 600 {config_path} && echo 'OK'",
68
+ client,
69
+ force_output=True,
70
+ skip_dry_run=True
71
+ )
72
+ result = result.strip() if result else ''
73
+ if result != 'OK':
74
+ output.message(
75
+ output.Subject.WARNING,
76
+ f'Failed to create MySQL config file on remote: {result}',
77
+ True
78
+ )
79
+ else:
80
+ # For local clients, write directly
81
+ with open(config_path, 'w') as f:
82
+ f.write(config_content)
83
+ os.chmod(config_path, 0o600)
84
+
85
+ _mysql_config_files[client] = config_path
86
+ return config_path
87
+
88
+
89
+ def get_mysql_config_path(client: str) -> str:
90
+ """
91
+ Get the MySQL config file path for a client, creating it if necessary.
92
+
93
+ :param client: String client identifier
94
+ :return: String path to the config file
28
95
  """
29
- _database_name = ' ' + system.config[client]['db']['name'] if force_database_name else ''
96
+ if client not in _mysql_config_files:
97
+ create_mysql_config_file(client)
98
+ return _mysql_config_files[client]
99
+
100
+
101
+ def cleanup_mysql_config_files() -> None:
102
+ """
103
+ Remove all temporary MySQL config files.
104
+ Should be called during cleanup phase.
105
+ """
106
+ global _mysql_config_files
107
+
108
+ for client, config_path in _mysql_config_files.items():
109
+ try:
110
+ if mode.is_remote(client):
111
+ mode.run_command(
112
+ f"rm -f {config_path}",
113
+ client,
114
+ allow_fail=True,
115
+ skip_dry_run=True
116
+ )
117
+ else:
118
+ if os.path.exists(config_path):
119
+ os.remove(config_path)
120
+ except Exception:
121
+ # Silently ignore cleanup errors
122
+ pass
123
+
124
+ _mysql_config_files = {}
30
125
 
31
- return mode.run_command(
32
- 'MYSQL_PWD="' + system.config[client]['db']['password'] + '" ' + helper.get_command(client, 'mysql') + ' ' + generate_mysql_credentials(
33
- client) + _database_name + ' -e "' + command + '"',
126
+
127
+ def run_database_command(client: str, command: str, force_database_name: bool = False) -> str:
128
+ """
129
+ Run a database command using the "mysql -e" command
130
+ :param client: Client identifier
131
+ :param command: Database command
132
+ :param force_database_name: Forces the database name
133
+ :return: Command output
134
+ """
135
+ _database_name = ''
136
+ if force_database_name:
137
+ cfg = system.get_typed_config()
138
+ _database_name = ' ' + helper.quote_shell_arg(cfg.get_client(client).db.name)
139
+
140
+ # Escape the SQL command for shell
141
+ # - Backslashes need doubling
142
+ # - Double quotes need escaping
143
+ # - Backticks need escaping (shell command substitution)
144
+ _safe_command = command.replace('\\', '\\\\').replace('"', '\\"').replace('`', '\\`')
145
+
146
+ result = mode.run_command(
147
+ helper.get_command(client, 'mysql') + ' ' + generate_mysql_credentials(
148
+ client) + _database_name + ' -e "' + _safe_command + '"',
34
149
  client, True)
150
+ return result if result else ''
151
+
35
152
 
153
+ def run_sql_batch_with_fk_disabled(client: str, statements: list[str]) -> None:
154
+ """
155
+ Execute multiple SQL statements in one roundtrip with FK checks disabled.
156
+ DRY helper for batch operations like TRUNCATE or DROP.
36
157
 
37
- def generate_database_dump_filename():
158
+ :param client: Client identifier
159
+ :param statements: List of SQL statements (without trailing semicolons)
160
+ """
161
+ if not statements:
162
+ return
163
+ sql = 'SET FOREIGN_KEY_CHECKS = 0; ' + '; '.join(statements) + '; SET FOREIGN_KEY_CHECKS = 1;'
164
+ run_database_command(client, sql, True)
165
+
166
+
167
+ def generate_database_dump_filename() -> None:
38
168
  """
39
169
  Generate a database dump filename like "_[name]_[date].sql" or using the give filename
40
- :return:
41
170
  """
42
171
  global database_dump_file_name
43
172
 
44
- if system.config['dump_name'] == '':
173
+ cfg = system.get_typed_config()
174
+ if cfg.dump_name == '':
45
175
  # _project-db_2022-08-22_12-37.sql
46
176
  _now = datetime.datetime.now()
47
- database_dump_file_name = '_' + system.config[mode.Client.ORIGIN]['db']['name'] + '_' + _now.strftime(
177
+ database_dump_file_name = '_' + cfg.origin.db.name + '_' + _now.strftime(
48
178
  "%Y-%m-%d_%H-%M") + '.sql'
49
179
  else:
50
- database_dump_file_name = system.config['dump_name'] + '.sql'
180
+ database_dump_file_name = cfg.dump_name + '.sql'
51
181
 
52
182
 
53
- def truncate_tables():
183
+ def truncate_tables() -> None:
54
184
  """
55
- Generate the ignore tables options for the mysqldump command by the given table list
56
- # ToDo: Too much conditional nesting
57
- :return: String
185
+ Truncate specified tables before import using batch operation
58
186
  """
59
- # Workaround for config naming
60
- if 'truncate_table' in system.config:
61
- system.config['truncate_tables'] = system.config['truncate_table']
187
+ cfg = system.get_typed_config()
62
188
 
63
- if 'truncate_tables' in system.config:
64
- output.message(
65
- output.Subject.TARGET,
66
- 'Truncating tables before import',
67
- True
68
- )
69
- for _table in system.config['truncate_tables']:
70
- if '*' in _table:
71
- _wildcard_tables = get_database_tables_like(mode.Client.TARGET,
72
- _table.replace('*', '%'))
73
- if _wildcard_tables:
74
- for _wildcard_table in _wildcard_tables:
75
- _sql_command = f'TRUNCATE TABLE IF EXISTS {_wildcard_table}'
76
- run_database_command(mode.Client.TARGET, _sql_command, True)
77
- else:
78
- _sql_command = f'TRUNCATE TABLE IF EXISTS {_table}'
79
- run_database_command(mode.Client.TARGET, _sql_command, True)
189
+ if not cfg.truncate_tables:
190
+ return
191
+
192
+ output.message(
193
+ output.Subject.TARGET,
194
+ 'Truncating tables before import',
195
+ True
196
+ )
197
+
198
+ # Collect all tables to truncate (80-90% fewer network roundtrips)
199
+ tables_to_truncate = []
200
+ for _table in cfg.truncate_tables:
201
+ if '*' in _table:
202
+ _wildcard_tables = get_database_tables_like(mode.Client.TARGET,
203
+ _table.replace('*', '%'))
204
+ if _wildcard_tables:
205
+ tables_to_truncate.extend(_wildcard_tables)
206
+ else:
207
+ # Check if table exists (MariaDB doesn't support IF EXISTS)
208
+ _existing_tables = get_database_tables_like(mode.Client.TARGET, _table)
209
+ if _existing_tables:
210
+ tables_to_truncate.append(_table)
211
+
212
+ if not tables_to_truncate:
213
+ return
214
+
215
+ # Build and execute TRUNCATE statements
216
+ statements = [f'TRUNCATE TABLE {sanitize_table_name(t)}' for t in tables_to_truncate]
217
+ run_sql_batch_with_fk_disabled(mode.Client.TARGET, statements)
80
218
 
81
219
 
82
- def generate_ignore_database_tables():
220
+ def generate_ignore_database_tables() -> str:
83
221
  """
84
222
  Generate the ignore tables options for the mysqldump command by the given table list
85
- # ToDo: Too much conditional nesting
86
- :return: String
223
+ :return: String of ignore table options
87
224
  """
88
- # Workaround for config naming
89
- if 'ignore_table' in system.config:
90
- system.config['ignore_tables'] = system.config['ignore_table']
225
+ cfg = system.get_typed_config()
91
226
 
92
- _ignore_tables = []
93
- if 'ignore_tables' in system.config:
94
- for table in system.config['ignore_tables']:
227
+ _ignore_tables: list[str] = []
228
+ if cfg.ignore_tables:
229
+ for table in cfg.ignore_tables:
95
230
  if '*' in table:
96
231
  _wildcard_tables = get_database_tables_like(mode.Client.ORIGIN,
97
232
  table.replace('*', '%'))
@@ -105,110 +240,192 @@ def generate_ignore_database_tables():
105
240
  return ''
106
241
 
107
242
 
108
- def generate_ignore_database_table(ignore_tables, table):
243
+ def generate_ignore_database_table(ignore_tables: list[str], table: str) -> list[str]:
109
244
  """
110
- :param ignore_tables: Dictionary
111
- :param table: String
112
- :return: Dictionary
245
+ :param ignore_tables: List of ignore table options
246
+ :param table: Table name to add
247
+ :return: Updated list of ignore table options
113
248
  """
114
- ignore_tables.append('--ignore-table=' + system.config['origin']['db']['name'] + '.' + table)
249
+ cfg = system.get_typed_config()
250
+ # Validate table name to prevent injection
251
+ _safe_table = sanitize_table_name(table)
252
+ # Remove backticks for mysqldump --ignore-table option (it doesn't use them)
253
+ _table_name = _safe_table.strip('`')
254
+ # Validate database name (same rules as table names)
255
+ _safe_db = sanitize_table_name(cfg.origin.db.name)
256
+ _db_name = _safe_db.strip('`')
257
+ ignore_tables.append(f'--ignore-table={_db_name}.{_table_name}')
115
258
  return ignore_tables
116
259
 
117
260
 
118
- def get_database_tables_like(client, name):
261
+ def get_database_tables_like(client: str, name: str) -> list[str] | None:
119
262
  """
120
263
  Get database table names like the given name
121
- :param client: String
122
- :param name: String
123
- :return: Dictionary
264
+ :param client: Client identifier
265
+ :param name: Pattern (may contain % wildcard)
266
+ :return: List of table names or None
124
267
  """
125
- _dbname = system.config[client]['db']['name']
126
- _tables = run_database_command(client, f'SHOW TABLES FROM \`{_dbname}\` LIKE \'{name}\';').strip()
268
+ cfg = system.get_typed_config()
269
+ _dbname = cfg.get_client(client).db.name
270
+ # Validate database name to prevent SQL injection
271
+ _safe_dbname = sanitize_table_name(_dbname)
272
+ # Escape single quotes in the pattern to prevent SQL injection
273
+ _safe_pattern = name.replace("'", "''")
274
+ _tables = run_database_command(client, f'SHOW TABLES FROM {_safe_dbname} LIKE \'{_safe_pattern}\';').strip()
127
275
  if _tables != '':
128
276
  return _tables.split('\n')[1:]
129
- return
277
+ return None
130
278
 
131
279
 
132
- def get_database_tables():
280
+ def get_database_tables() -> str:
133
281
  """
134
282
  Generate specific tables for export
135
- :return: String
283
+ :return: String of table names
136
284
  """
137
- if system.config['tables'] == '':
285
+ cfg = system.get_typed_config()
286
+ if cfg.tables == '':
138
287
  return ''
139
288
 
140
289
  _result = ' '
141
- _tables = system.config['tables'].split(',')
290
+ _tables = cfg.tables.split(',')
142
291
  for _table in _tables:
143
- _result += '\'' + _table + '\' '
292
+ # Validate table name to prevent injection
293
+ _safe_table = sanitize_table_name(_table.strip())
294
+ # Use backtick-quoted name for shell command
295
+ _result += _safe_table + ' '
144
296
  return _result
145
297
 
146
298
 
147
- def generate_mysql_credentials(client, force_password=False):
299
+ def generate_mysql_credentials(client: str, force_password: bool = True) -> str:
148
300
  """
149
- Generate the needed database credential information for the mysql command
150
- :param client: String
151
- :param force_password: Bool
152
- :return:
301
+ Generate the needed database credential information for the mysql command.
302
+ Uses --defaults-extra-file to prevent passwords from appearing in process lists
303
+ while preserving system MySQL configuration (including SSL settings).
304
+
305
+ :param client: Client identifier
306
+ :param force_password: Kept for backwards compatibility, now always uses secure method
307
+ :return: MySQL credentials argument
153
308
  """
154
- _credentials = '-u\'' + system.config[client]['db']['user'] + '\''
309
+ try:
310
+ config_path = get_mysql_config_path(client)
311
+ # Note: --defaults-extra-file must NOT have quotes around the path
312
+ # mysqldump/mysql parse this option specially
313
+ # Using --defaults-extra-file (not --defaults-file) preserves system config
314
+ credentials = f"--defaults-extra-file={config_path}"
315
+
316
+ cfg = system.get_typed_config()
317
+ if cfg.verbose:
318
+ output.message(
319
+ output.host_to_subject(client),
320
+ f'Using secure credentials file: {config_path}',
321
+ verbose_only=True
322
+ )
323
+
324
+ return credentials
325
+ except Exception as e:
326
+ # Fallback to legacy method if config file creation fails
327
+ output.message(
328
+ output.Subject.WARNING,
329
+ f'Falling back to legacy credentials (config file failed: {e})',
330
+ True
331
+ )
332
+ return _generate_mysql_credentials_legacy(client, force_password)
333
+
334
+
335
+ def _generate_mysql_credentials_legacy(client: str, force_password: bool = True) -> str:
336
+ """
337
+ Legacy method: Generate MySQL credentials as command line arguments.
338
+ WARNING: This exposes passwords in process lists!
339
+
340
+ :param client: Client identifier
341
+ :param force_password: Include password in credentials
342
+ :return: MySQL credentials arguments
343
+ """
344
+ cfg = system.get_typed_config()
345
+ db_cfg = cfg.get_client(client).db
346
+ _credentials = '-u\'' + db_cfg.user + '\''
155
347
  if force_password:
156
- _credentials += ' -p\'' + system.config[client]['db']['password'] + '\''
157
- if 'host' in system.config[client]['db']:
158
- _credentials += ' -h\'' + system.config[client]['db']['host'] + '\''
159
- if 'port' in system.config[client]['db']:
160
- _credentials += ' -P\'' + str(system.config[client]['db']['port']) + '\''
348
+ _credentials += ' -p\'' + db_cfg.password + '\''
349
+ if db_cfg.host:
350
+ _credentials += ' -h\'' + db_cfg.host + '\''
351
+ if db_cfg.port:
352
+ _credentials += ' -P\'' + str(db_cfg.port) + '\''
161
353
  return _credentials
162
354
 
163
355
 
164
- def check_database_dump(client, filepath):
356
+ def get_dump_file_path(client: str) -> str:
165
357
  """
166
- Checking the last line of the dump file if it contains "-- Dump completed on"
167
- :param client: String
168
- :param filepath: String
169
- :return:
358
+ Get the path to the dump file (without .gz extension).
359
+ DRY helper for consistent path construction.
360
+
361
+ :param client: Client identifier
362
+ :return: Path to dump file
170
363
  """
171
- if system.config['check_dump']:
172
- _line = mode.run_command(
173
- helper.get_command(client, 'tail') + ' -n 1 ' + filepath,
174
- client,
175
- True,
176
- skip_dry_run=True
177
- )
364
+ if database_dump_file_name is None:
365
+ raise DbSyncError('database_dump_file_name not initialized')
366
+ return helper.get_dump_dir(client) + database_dump_file_name
178
367
 
179
- if not _line:
180
- return
181
368
 
182
- if "-- Dump completed on" not in _line:
183
- sys.exit(
184
- output.message(
185
- output.Subject.ERROR,
186
- 'Dump file is corrupted',
187
- do_print=False
188
- )
189
- )
190
- else:
191
- output.message(
192
- output.host_to_subject(client),
193
- 'Dump file is valid',
194
- verbose_only=True
195
- )
369
+ def get_dump_gz_path(client: str) -> str:
370
+ """
371
+ Get the path to the compressed dump file (.gz).
372
+ DRY helper for consistent path construction.
373
+
374
+ :param client: Client identifier
375
+ :return: Path to compressed dump file
376
+ """
377
+ return get_dump_file_path(client) + '.gz'
196
378
 
197
379
 
198
- def count_tables(client, filepath):
380
+ def get_dump_cat_command(client: str, filepath: str) -> str:
381
+ """
382
+ Get the appropriate command to read a dump file (handles .gz compression).
383
+ DRY helper for check_database_dump and count_tables.
384
+
385
+ :param client: Client identifier
386
+ :param filepath: Path to dump file
387
+ :return: Command prefix for reading the file
388
+ """
389
+ _safe_filepath = helper.quote_shell_arg(filepath)
390
+ if filepath.endswith('.gz'):
391
+ return f'{helper.get_command(client, "gunzip")} -c {_safe_filepath}'
392
+ return f'{helper.get_command(client, "cat")} {_safe_filepath}'
393
+
394
+
395
+ def check_database_dump(client: str, filepath: str) -> None:
396
+ """
397
+ Checking the last line of the dump file if it contains "-- Dump completed on"
398
+ :param client: Client identifier
399
+ :param filepath: Path to dump file
400
+ """
401
+ cfg = system.get_typed_config()
402
+ if not cfg.check_dump:
403
+ return
404
+
405
+ _cmd = f'{get_dump_cat_command(client, filepath)} | tail -n 1'
406
+ _line = mode.run_command(_cmd, client, True, skip_dry_run=True)
407
+
408
+ if not _line:
409
+ return
410
+
411
+ if "-- Dump completed on" not in _line:
412
+ raise DbSyncError('Dump file is corrupted')
413
+ output.message(
414
+ output.host_to_subject(client),
415
+ 'Dump file is valid',
416
+ verbose_only=True
417
+ )
418
+
419
+
420
+ def count_tables(client: str, filepath: str) -> None:
199
421
  """
200
422
  Count the reference string in the database dump file to get the count of all exported tables
201
- :param client: String
202
- :param filepath: String
203
- :return:
423
+ :param client: Client identifier
424
+ :param filepath: Path to dump file
204
425
  """
205
426
  _reference = 'CREATE TABLE'
206
- _count = mode.run_command(
207
- f'{helper.get_command(client, "grep")} -ao "{_reference}" {filepath} | wc -l | xargs',
208
- client,
209
- True,
210
- skip_dry_run=True
211
- )
427
+ _cmd = f'{get_dump_cat_command(client, filepath)} | grep -ao "{_reference}" | wc -l | xargs'
428
+ _count = mode.run_command(_cmd, client, True, skip_dry_run=True)
212
429
 
213
430
  if _count:
214
431
  output.message(
@@ -217,11 +434,11 @@ def count_tables(client, filepath):
217
434
  )
218
435
 
219
436
 
220
- def get_database_version(client):
437
+ def get_database_version(client: str) -> tuple[str | None, str | None]:
221
438
  """
222
439
  Check the database version and distinguish between mysql and mariadb
223
- :param client:
224
- :return: Tuple<String,String>
440
+ :param client: Client identifier
441
+ :return: Tuple of (database_system, version_number)
225
442
  """
226
443
  _database_system = None
227
444
  _version_number = None
@@ -229,7 +446,8 @@ def get_database_version(client):
229
446
  _database_version = run_database_command(client, 'SELECT VERSION();').splitlines()[1]
230
447
  _database_system = DatabaseSystem.MYSQL
231
448
 
232
- _version_number = re.search('(\d+\.)?(\d+\.)?(\*|\d+)', _database_version).group()
449
+ _version_match = re.search(r'(\d+\.)?(\d+\.)?(\*|\d+)', _database_version)
450
+ _version_number = _version_match.group() if _version_match else None
233
451
 
234
452
  if DatabaseSystem.MARIADB.lower() in _database_version.lower():
235
453
  _database_system = DatabaseSystem.MARIADB
db_sync_tool/info.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Info script
3
3
  """
4
- __version__ = "2.11.6"
4
+ __version__ = "3.0.2"
5
5
  __pypi_package_url__ = "https://pypi.org/pypi/db-sync-tool-kmi"
6
6
  __homepage__ = "https://github.com/jackd248/db-sync-tool"