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,12 +1,13 @@
1
1
  #!/usr/bin/env python3
2
- # -*- coding: future_fstrings -*-
3
2
 
4
3
  """
5
4
  Parser script
6
5
  """
7
6
 
8
7
  import sys
8
+ import types
9
9
  from db_sync_tool.utility import mode, system, output, helper
10
+ from db_sync_tool.utility.exceptions import ConfigError, ValidationError
10
11
  from db_sync_tool.remote import client as remote_client
11
12
 
12
13
 
@@ -46,18 +47,18 @@ def get_database_configuration(client):
46
47
  :param client: String
47
48
  :return:
48
49
  """
49
- system.config['db'] = {}
50
+ cfg = system.get_typed_config()
50
51
 
51
52
  # check framework type
52
53
  _base = ''
53
54
 
54
55
  automatic_type_detection()
55
56
 
56
- if 'type' in system.config and (
57
- 'path' in system.config[mode.Client.ORIGIN] or
58
- 'path' in system.config[mode.Client.TARGET]
59
- ):
60
- _type = system.config['type'].lower()
57
+ # Re-get config after type detection may have updated it
58
+ cfg = system.get_typed_config()
59
+
60
+ if cfg.type and (cfg.origin.path != '' or cfg.target.path != ''):
61
+ _type = cfg.type.lower()
61
62
  if _type == 'typo3':
62
63
  # TYPO3 sync base
63
64
  _base = Framework.TYPO3
@@ -74,25 +75,14 @@ def get_database_configuration(client):
74
75
  # Laravel sync base
75
76
  _base = Framework.LARAVEL
76
77
  else:
77
- sys.exit(
78
- output.message(
79
- output.Subject.ERROR,
80
- f'Framework type not supported: {_type}',
81
- False
82
- )
83
- )
84
- elif 'db' in system.config['origin'] or 'db' in system.config['target']:
78
+ raise ConfigError(f'Framework type not supported: {_type}')
79
+ elif cfg.origin.db.name != '' or cfg.target.db.name != '':
85
80
  _base = Framework.MANUAL
86
81
  else:
87
- sys.exit(
88
- output.message(
89
- output.Subject.ERROR,
90
- f'Missing framework type or database credentials',
91
- False
92
- )
93
- )
82
+ raise ConfigError('Missing framework type or database credentials')
94
83
 
95
84
  sys.path.append('../recipes')
85
+ _parser: types.ModuleType | None = None
96
86
  if _base == Framework.TYPO3:
97
87
  # Import TYPO3 parser
98
88
  from ..recipes import typo3
@@ -143,7 +133,8 @@ def load_parser(client, parser):
143
133
  :param parser:
144
134
  :return:
145
135
  """
146
- _path = system.config[client]['path']
136
+ cfg = system.get_typed_config()
137
+ _path = cfg.get_client(client).path
147
138
 
148
139
  output.message(
149
140
  output.host_to_subject(client),
@@ -163,13 +154,7 @@ def load_parser(client, parser):
163
154
 
164
155
  # Check only if database configuration is a file
165
156
  if not helper.check_file_exists(client, _path) and _path[-1] != '/':
166
- sys.exit(
167
- output.message(
168
- output.Subject.ERROR,
169
- f'Database configuration for {client} not found: {_path}',
170
- False
171
- )
172
- )
157
+ raise ConfigError(f'Database configuration for {client} not found: {_path}')
173
158
  parser.check_configuration(client)
174
159
 
175
160
 
@@ -179,6 +164,9 @@ def validate_database_credentials(client):
179
164
  :param client: String
180
165
  :return:
181
166
  """
167
+ cfg = system.get_typed_config()
168
+ db_cfg = cfg.get_client(client).db
169
+
182
170
  output.message(
183
171
  output.host_to_subject(client),
184
172
  'Validating database credentials',
@@ -187,21 +175,10 @@ def validate_database_credentials(client):
187
175
  _db_credential_keys = ['name', 'host', 'password', 'user']
188
176
 
189
177
  for _key in _db_credential_keys:
190
- if _key not in system.config[client]['db']:
191
- sys.exit(
192
- output.message(
193
- output.Subject.ERROR,
194
- f'Missing database credential "{_key}" for {client} client',
195
- False
196
- )
197
- )
198
- if system.config[client]['db'][_key] is None or system.config[client]['db'][_key] == '':
199
- sys.exit(
200
- output.message(
201
- output.Subject.ERROR,
202
- f'Missing database credential "{_key}" for {client} client',
203
- False
204
- )
178
+ _value = getattr(db_cfg, _key, None)
179
+ if _value is None or _value == '':
180
+ raise ValidationError(
181
+ f'Missing database credential "{_key}" for {client} client'
205
182
  )
206
183
  else:
207
184
  output.message(
@@ -215,25 +192,28 @@ def automatic_type_detection():
215
192
  """
216
193
  Detects the framework type by the provided path using the default mapping
217
194
  """
218
- if 'type' in system.config or 'db' in system.config['origin'] or 'db' in system.config[
219
- 'target']:
195
+ cfg = system.get_typed_config()
196
+
197
+ # Skip if type is already set or manual db config is provided
198
+ if cfg.type or cfg.origin.db.name != '' or cfg.target.db.name != '':
220
199
  return
221
200
 
222
- type = None
201
+ detected_type = None
223
202
  file = None
224
203
 
225
204
  for _client in [mode.Client.ORIGIN, mode.Client.TARGET]:
226
- if 'path' in system.config[_client]:
227
- file = helper.get_file_from_path(system.config[_client]['path'])
205
+ client_cfg = cfg.get_client(_client)
206
+ if client_cfg.path != '':
207
+ file = helper.get_file_from_path(client_cfg.path)
228
208
  for _key, _files in mapping.items():
229
209
  if file in _files:
230
- type = _key
210
+ detected_type = _key
231
211
 
232
- if type:
212
+ if detected_type:
233
213
  output.message(
234
214
  output.Subject.LOCAL,
235
215
  f'Automatic framework type detection '
236
216
  f'{output.CliFormat.BLACK}{file}{output.CliFormat.ENDC}',
237
217
  verbose_only=True
238
218
  )
239
- system.config['type'] = type
219
+ system.set_framework_type(detected_type)
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Pure utility functions with no project dependencies.
5
+ """
6
+
7
+ import re
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+
12
+ def parse_version(version_output: str | None) -> str | None:
13
+ """
14
+ Parse version out of console output.
15
+ https://stackoverflow.com/a/60730346
16
+
17
+ :param version_output: Console output string
18
+ :return: Version string or None
19
+ """
20
+ if not version_output:
21
+ return None
22
+ _version_pattern = r'\d+(=?\.(\d+(=?\.(\d+)*)*)*)*'
23
+ _regex_matcher = re.compile(_version_pattern)
24
+ _version = _regex_matcher.search(version_output)
25
+ if _version:
26
+ return _version.group(0)
27
+ return None
28
+
29
+
30
+ def get_file_from_path(path: str) -> str:
31
+ """
32
+ Trims a path string to retrieve the file.
33
+
34
+ :param path: File path
35
+ :return: File name
36
+ """
37
+ return Path(path).name
38
+
39
+
40
+ def remove_surrounding_quotes(s: Any) -> Any:
41
+ """
42
+ Removes the enclosing quotes (single or double),
43
+ if there are quotes at both the beginning and end of the string.
44
+
45
+ :param s: The string to be checked
46
+ :return: The string without enclosing quotes, if available
47
+ """
48
+ if isinstance(s, str):
49
+ if s.startswith('"') and s.endswith('"'):
50
+ return s[1:-1]
51
+ elif s.startswith("'") and s.endswith("'"):
52
+ return s[1:-1]
53
+ return s
54
+
55
+
56
+ def clean_db_config(config: dict[str, Any]) -> dict[str, Any]:
57
+ """
58
+ Iterates over all entries of a dictionary and removes enclosing
59
+ quotes from the values, if present.
60
+
61
+ :param config: The dictionary to be edited
62
+ :return: A new dictionary with adjusted values
63
+ """
64
+ return {key: remove_surrounding_quotes(value) for key, value in config.items()}
65
+
66
+
67
+ def dict_to_args(data: dict[str, Any]) -> list[str] | None:
68
+ """
69
+ Convert a dictionary to an args list.
70
+
71
+ :param data: Dictionary to convert
72
+ :return: List of arguments or None if empty
73
+ """
74
+ args = []
75
+ for key, val in data.items():
76
+ if val is True:
77
+ args.append(f'--{key}')
78
+ elif val is not False and val is not None:
79
+ args.extend([f'--{key}', str(val)])
80
+ return args or None
81
+
82
+
83
+ def remove_multiple_elements_from_string(elements: list, string: str) -> str:
84
+ """
85
+ Removes multiple elements from a string.
86
+
87
+ :param elements: List of elements to remove
88
+ :param string: Input string
89
+ :return: String with elements removed
90
+ """
91
+ for element in elements:
92
+ string = string.replace(element, '')
93
+ return string
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+
3
+ """
4
+ Security utility functions.
5
+
6
+ This module contains pure security functions with minimal dependencies,
7
+ making them safe to import without circular import issues.
8
+ Only depends on utility/exceptions.py which has no other project dependencies.
9
+ """
10
+
11
+ import re
12
+ import shlex
13
+ from typing import Any
14
+
15
+ from db_sync_tool.utility.exceptions import ValidationError
16
+
17
+
18
+ def quote_shell_arg(arg: Any) -> str:
19
+ """
20
+ Safely quote a string for use as a shell argument.
21
+ Prevents command injection by escaping special characters.
22
+
23
+ :param arg: Value to quote (will be converted to string)
24
+ :return: Safely quoted string
25
+ """
26
+ if arg is None:
27
+ return "''"
28
+ return shlex.quote(str(arg))
29
+
30
+
31
+ def sanitize_table_name(table: str) -> str:
32
+ """
33
+ Validate and sanitize a table name to prevent SQL injection.
34
+ MySQL table names can contain alphanumeric chars, underscores, and dollar signs.
35
+ They can also contain hyphens and dots in quoted identifiers.
36
+
37
+ :param table: Table name to sanitize
38
+ :return: Backtick-quoted table name
39
+ :raises ValidationError: If table name contains invalid characters
40
+ """
41
+ if not table:
42
+ raise ValidationError("Table name cannot be empty")
43
+
44
+ # Allow alphanumeric, underscore, hyphen, dot, dollar sign
45
+ if not re.match(r'^[a-zA-Z0-9_$.-]+$', table):
46
+ raise ValidationError(f"Invalid table name: {table}")
47
+
48
+ return f"`{table}`"
49
+
50
+
51
+ def sanitize_command_for_logging(command: str) -> str:
52
+ """
53
+ Remove sensitive information from commands before logging.
54
+ This prevents credentials from appearing in verbose output or logs.
55
+
56
+ :param command: Command string to sanitize
57
+ :return: Sanitized command string
58
+ """
59
+ patterns = [
60
+ # MySQL password patterns
61
+ (r"-p'[^']*'", "-p'***'"),
62
+ (r'-p"[^"]*"', '-p"***"'),
63
+ (r"-p[^\s'\"]+", "-p***"),
64
+ # SSHPASS patterns
65
+ (r"SSHPASS='[^']*'", "SSHPASS='***'"),
66
+ (r'SSHPASS="[^"]*"', 'SSHPASS="***"'),
67
+ (r"SSHPASS=[^\s]+", "SSHPASS=***"),
68
+ # MySQL defaults-file/defaults-extra-file (mask path to prevent disclosure)
69
+ (r"--defaults-file=[^\s]+", "--defaults-file=***"),
70
+ (r"--defaults-extra-file=[^\s]+", "--defaults-extra-file=***"),
71
+ # Base64 encoded credentials
72
+ (r"echo '[A-Za-z0-9+/=]{20,}' \| base64", "echo '***' | base64"),
73
+ ]
74
+
75
+ sanitized = command
76
+ for pattern, replacement in patterns:
77
+ sanitized = re.sub(pattern, replacement, sanitized)
78
+
79
+ return sanitized