db-drift 1.0.0__py3-none-any.whl → 1.1.0__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_drift/__init__.py +1 -2
- db_drift/__main__.py +103 -0
- db_drift/cli/__init__.py +0 -0
- db_drift/cli/cli.py +35 -0
- db_drift/cli/utils.py +9 -0
- db_drift/utils/__init__.py +0 -0
- db_drift/utils/custom_logging.py +52 -0
- db_drift/utils/exceptions/__init__.py +36 -0
- db_drift/utils/exceptions/base.py +49 -0
- db_drift/utils/exceptions/cli.py +68 -0
- db_drift/utils/exceptions/config.py +117 -0
- db_drift/utils/exceptions/database.py +153 -0
- db_drift/utils/exceptions/formatting.py +175 -0
- db_drift/utils/exceptions/status_codes.py +15 -0
- {db_drift-1.0.0.dist-info → db_drift-1.1.0.dist-info}/METADATA +4 -4
- db_drift-1.1.0.dist-info/RECORD +19 -0
- db_drift-1.0.0.dist-info/RECORD +0 -6
- {db_drift-1.0.0.dist-info → db_drift-1.1.0.dist-info}/WHEEL +0 -0
- {db_drift-1.0.0.dist-info → db_drift-1.1.0.dist-info}/entry_points.txt +0 -0
- {db_drift-1.0.0.dist-info → db_drift-1.1.0.dist-info}/licenses/LICENSE +0 -0
db_drift/__init__.py
CHANGED
@@ -1,2 +1 @@
|
|
1
|
-
|
2
|
-
print("Hello from db-drift!")
|
1
|
+
from db_drift.__main__ import main # noqa: F401
|
db_drift/__main__.py
ADDED
@@ -0,0 +1,103 @@
|
|
1
|
+
import os
|
2
|
+
|
3
|
+
from db_drift.cli.cli import cli
|
4
|
+
from db_drift.utils import custom_logging
|
5
|
+
from db_drift.utils.exceptions import CliError, ConfigError, DatabaseError, DbDriftError, DbDriftInterruptError, ExitCode
|
6
|
+
from db_drift.utils.exceptions.base import DbDriftSystemError
|
7
|
+
from db_drift.utils.exceptions.formatting import handle_error_and_exit, handle_unexpected_error
|
8
|
+
|
9
|
+
logger = custom_logging.setup_logger("db-drift")
|
10
|
+
|
11
|
+
|
12
|
+
DEBUG_MODE: int | bool = os.getenv("DB_DRIFT_DEBUG", "").lower() in ("1", "true", "yes", "on")
|
13
|
+
|
14
|
+
|
15
|
+
def main() -> None:
|
16
|
+
"""Entry point for the db-drift package."""
|
17
|
+
try:
|
18
|
+
logger.debug("Starting db-drift CLI")
|
19
|
+
cli()
|
20
|
+
|
21
|
+
except KeyboardInterrupt:
|
22
|
+
# Handle Ctrl+C gracefully
|
23
|
+
error = DbDriftInterruptError()
|
24
|
+
logger.info("Operation cancelled by user")
|
25
|
+
handle_error_and_exit(
|
26
|
+
error,
|
27
|
+
logger=logger,
|
28
|
+
show_traceback=DEBUG_MODE,
|
29
|
+
show_suggestions=False,
|
30
|
+
)
|
31
|
+
|
32
|
+
except CliError as e:
|
33
|
+
# Handle CLI-specific errors (argument parsing, usage errors, etc.)
|
34
|
+
logger.exception("CLI Error occurred")
|
35
|
+
handle_error_and_exit(
|
36
|
+
e,
|
37
|
+
logger=logger,
|
38
|
+
show_traceback=DEBUG_MODE,
|
39
|
+
show_suggestions=True,
|
40
|
+
)
|
41
|
+
|
42
|
+
except ConfigError as e:
|
43
|
+
# Handle configuration errors
|
44
|
+
logger.exception("Configuration Error occurred")
|
45
|
+
handle_error_and_exit(
|
46
|
+
e,
|
47
|
+
logger=logger,
|
48
|
+
show_traceback=DEBUG_MODE,
|
49
|
+
show_suggestions=True,
|
50
|
+
)
|
51
|
+
|
52
|
+
except DatabaseError as e:
|
53
|
+
# Handle database connection and query errors
|
54
|
+
logger.exception("Database Error occurred")
|
55
|
+
handle_error_and_exit(
|
56
|
+
e,
|
57
|
+
logger=logger,
|
58
|
+
show_traceback=DEBUG_MODE,
|
59
|
+
show_suggestions=True,
|
60
|
+
)
|
61
|
+
|
62
|
+
except DbDriftError as e:
|
63
|
+
# Handle other custom application errors
|
64
|
+
logger.exception("Application Error occurred")
|
65
|
+
handle_error_and_exit(
|
66
|
+
e,
|
67
|
+
logger=logger,
|
68
|
+
show_traceback=DEBUG_MODE,
|
69
|
+
show_suggestions=True,
|
70
|
+
)
|
71
|
+
|
72
|
+
except FileNotFoundError as e:
|
73
|
+
# Handle file not found errors
|
74
|
+
logger.exception("File not found")
|
75
|
+
system_error = DbDriftSystemError(f"Required file not found: {e}", exit_code=ExitCode.NO_INPUT)
|
76
|
+
handle_error_and_exit(
|
77
|
+
system_error,
|
78
|
+
logger=logger,
|
79
|
+
show_traceback=DEBUG_MODE,
|
80
|
+
show_suggestions=True,
|
81
|
+
)
|
82
|
+
|
83
|
+
except PermissionError as e:
|
84
|
+
# Handle permission errors
|
85
|
+
logger.exception("Permission denied")
|
86
|
+
system_error = DbDriftSystemError(f"Permission denied: {e}", exit_code=ExitCode.NO_PERMISSION)
|
87
|
+
handle_error_and_exit(
|
88
|
+
system_error,
|
89
|
+
logger=logger,
|
90
|
+
show_traceback=DEBUG_MODE,
|
91
|
+
show_suggestions=True,
|
92
|
+
)
|
93
|
+
|
94
|
+
except Exception:
|
95
|
+
# Handle any unexpected errors
|
96
|
+
logger.exception("Unexpected error occurred")
|
97
|
+
|
98
|
+
# In debug mode, show full traceback; otherwise, show generic message
|
99
|
+
handle_unexpected_error(DEBUG_MODE, logger)
|
100
|
+
|
101
|
+
|
102
|
+
if __name__ == "__main__":
|
103
|
+
main()
|
db_drift/cli/__init__.py
ADDED
File without changes
|
db_drift/cli/cli.py
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
import argparse
|
2
|
+
import logging
|
3
|
+
|
4
|
+
from db_drift.cli.utils import get_version
|
5
|
+
from db_drift.utils.exceptions import CliArgumentError, CliUsageError
|
6
|
+
|
7
|
+
logger = logging.getLogger("db-drift")
|
8
|
+
|
9
|
+
|
10
|
+
def cli() -> None:
|
11
|
+
parser = argparse.ArgumentParser(
|
12
|
+
prog="db-drift",
|
13
|
+
description="A command-line tool to visualize the differences between two DB states.",
|
14
|
+
exit_on_error=False, # We'll handle errors ourselves
|
15
|
+
)
|
16
|
+
|
17
|
+
parser.add_argument(
|
18
|
+
"-v",
|
19
|
+
"--version",
|
20
|
+
action="version",
|
21
|
+
version=f"db-drift {get_version()}",
|
22
|
+
)
|
23
|
+
|
24
|
+
try:
|
25
|
+
_ = parser.parse_args()
|
26
|
+
except argparse.ArgumentError as e:
|
27
|
+
msg = f"Invalid argument: {e}"
|
28
|
+
raise CliArgumentError(msg) from e
|
29
|
+
except SystemExit as e:
|
30
|
+
# argparse calls sys.exit() on error, convert to our exception
|
31
|
+
if e.code != 0:
|
32
|
+
msg = "Invalid command line arguments. Use --help for usage information."
|
33
|
+
raise CliUsageError(msg) from e
|
34
|
+
# Re-raise if it's a successful exit (like --help)
|
35
|
+
raise
|
db_drift/cli/utils.py
ADDED
File without changes
|
@@ -0,0 +1,52 @@
|
|
1
|
+
import logging
|
2
|
+
from logging.handlers import RotatingFileHandler
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
LOGFILES_DIR_NAME = ".logs"
|
6
|
+
|
7
|
+
|
8
|
+
def setup_logger(name: str, level: int = logging.INFO) -> logging.Logger:
|
9
|
+
"""
|
10
|
+
Set up a logger with a specific name and configuration.
|
11
|
+
Note: This could also be used to modify an existing logger's configuration.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
name (str): The name of the logger.
|
15
|
+
level (int): The logging level. Default is logging.INFO.
|
16
|
+
|
17
|
+
Returns:
|
18
|
+
logging.Logger: The configured logger instance.
|
19
|
+
"""
|
20
|
+
formatter = logging.Formatter(
|
21
|
+
"%(asctime)s - %(threadName)s:%(thread)d - %(module)s:%(lineno)d - %(levelname)s - %(message)s",
|
22
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
23
|
+
)
|
24
|
+
|
25
|
+
if not Path(LOGFILES_DIR_NAME).exists():
|
26
|
+
Path(LOGFILES_DIR_NAME).mkdir(parents=True, exist_ok=True)
|
27
|
+
|
28
|
+
logger = logging.getLogger(name)
|
29
|
+
|
30
|
+
if logger.hasHandlers():
|
31
|
+
logger.handlers.clear()
|
32
|
+
|
33
|
+
logger.setLevel(level)
|
34
|
+
|
35
|
+
file_handler = RotatingFileHandler(
|
36
|
+
filename=Path(LOGFILES_DIR_NAME) / f"{name}.log",
|
37
|
+
maxBytes=5 * 1024 * 1024, # 5 MB
|
38
|
+
backupCount=5, # Keep up to 5 backup log files
|
39
|
+
)
|
40
|
+
file_handler.setFormatter(formatter)
|
41
|
+
file_handler.setLevel(logging.DEBUG) # File handler logs everything from DEBUG and above
|
42
|
+
|
43
|
+
console_handler = logging.StreamHandler()
|
44
|
+
console_handler.setFormatter(formatter)
|
45
|
+
|
46
|
+
logger.addHandler(file_handler)
|
47
|
+
logger.addHandler(console_handler)
|
48
|
+
|
49
|
+
logger.propagate = False # Prevent log messages from being propagated to the root logger
|
50
|
+
logger.info(f"Logger '{name}' initialized with level {logging.getLevelName(level)}")
|
51
|
+
|
52
|
+
return logger
|
@@ -0,0 +1,36 @@
|
|
1
|
+
"""Custom exceptions for db-drift application."""
|
2
|
+
|
3
|
+
from db_drift.utils.exceptions.base import DbDriftError, DbDriftInterruptError, DbDriftSystemError, DbDriftUserError
|
4
|
+
from db_drift.utils.exceptions.cli import CliArgumentError, CliConfigError, CliError, CliUsageError
|
5
|
+
from db_drift.utils.exceptions.config import ConfigError, ConfigFormatError, ConfigValidationError, MissingConfigError
|
6
|
+
from db_drift.utils.exceptions.database import (
|
7
|
+
DatabaseAuthenticationError,
|
8
|
+
DatabaseConnectionError,
|
9
|
+
DatabaseError,
|
10
|
+
DatabaseQueryError,
|
11
|
+
DatabaseSchemaError,
|
12
|
+
DatabaseTimeoutError,
|
13
|
+
)
|
14
|
+
from db_drift.utils.exceptions.status_codes import ExitCode
|
15
|
+
|
16
|
+
__all__ = [
|
17
|
+
"CliArgumentError",
|
18
|
+
"CliConfigError",
|
19
|
+
"CliError",
|
20
|
+
"CliUsageError",
|
21
|
+
"ConfigError",
|
22
|
+
"ConfigFormatError",
|
23
|
+
"ConfigValidationError",
|
24
|
+
"DatabaseAuthenticationError",
|
25
|
+
"DatabaseConnectionError",
|
26
|
+
"DatabaseError",
|
27
|
+
"DatabaseQueryError",
|
28
|
+
"DatabaseSchemaError",
|
29
|
+
"DatabaseTimeoutError",
|
30
|
+
"DbDriftError",
|
31
|
+
"DbDriftInterruptError",
|
32
|
+
"DbDriftSystemError",
|
33
|
+
"DbDriftUserError",
|
34
|
+
"ExitCode",
|
35
|
+
"MissingConfigError",
|
36
|
+
]
|
@@ -0,0 +1,49 @@
|
|
1
|
+
"""Base exceptions for db-drift application."""
|
2
|
+
|
3
|
+
|
4
|
+
from db_drift.utils.exceptions.status_codes import ExitCode
|
5
|
+
|
6
|
+
|
7
|
+
class DbDriftError(Exception):
|
8
|
+
"""Base exception for all db-drift related errors."""
|
9
|
+
|
10
|
+
def __init__(self, message: str, exit_code: int = ExitCode.GENERAL_ERROR) -> None:
|
11
|
+
"""
|
12
|
+
Initialize the exception.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
message: The error message
|
16
|
+
exit_code: The exit code to use when this error causes program termination
|
17
|
+
"""
|
18
|
+
super().__init__(message)
|
19
|
+
self.message = message
|
20
|
+
self.exit_code = exit_code
|
21
|
+
|
22
|
+
def __str__(self) -> str:
|
23
|
+
"""Return the error message."""
|
24
|
+
return self.message
|
25
|
+
|
26
|
+
|
27
|
+
class DbDriftUserError(DbDriftError):
|
28
|
+
"""Base class for user-caused errors (wrong arguments, invalid config, etc.)."""
|
29
|
+
|
30
|
+
def __init__(self, message: str, exit_code: int = ExitCode.USAGE_ERROR) -> None:
|
31
|
+
"""Initialize user error with default exit code 2."""
|
32
|
+
super().__init__(message, exit_code)
|
33
|
+
|
34
|
+
|
35
|
+
class DbDriftSystemError(DbDriftError):
|
36
|
+
"""Base class for system-level errors (network issues, permission errors, etc.)."""
|
37
|
+
|
38
|
+
def __init__(self, message: str, exit_code: int = ExitCode.GENERAL_ERROR) -> None:
|
39
|
+
"""Initialize system error with default exit code 1."""
|
40
|
+
super().__init__(message, exit_code)
|
41
|
+
|
42
|
+
|
43
|
+
class DbDriftInterruptError(DbDriftError):
|
44
|
+
"""Exception for user interruption (Ctrl+C)."""
|
45
|
+
|
46
|
+
def __init__(self, message: str = "Operation cancelled by user") -> None:
|
47
|
+
"""Initialize interrupt error with standard Unix signal exit code."""
|
48
|
+
# 128 + SIGINT (2) = 130
|
49
|
+
super().__init__(message, exit_code=ExitCode.SIGINT)
|
@@ -0,0 +1,68 @@
|
|
1
|
+
"""CLI-specific exceptions for db-drift."""
|
2
|
+
|
3
|
+
from db_drift.utils.exceptions.base import DbDriftUserError
|
4
|
+
from db_drift.utils.exceptions.status_codes import ExitCode
|
5
|
+
|
6
|
+
|
7
|
+
class CliError(DbDriftUserError):
|
8
|
+
"""Base class for CLI-related errors."""
|
9
|
+
|
10
|
+
def __init__(self, message: str, exit_code: int = ExitCode.USAGE_ERROR) -> None:
|
11
|
+
"""
|
12
|
+
Initialize CLI error.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
message: The error message
|
16
|
+
exit_code: The exit code (default 2 for CLI usage errors)
|
17
|
+
"""
|
18
|
+
super().__init__(message, exit_code)
|
19
|
+
|
20
|
+
|
21
|
+
class CliArgumentError(CliError):
|
22
|
+
"""Exception for invalid command-line arguments."""
|
23
|
+
|
24
|
+
def __init__(self, message: str, argument: str | None = None) -> None:
|
25
|
+
"""
|
26
|
+
Initialize argument error.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
message: The error message
|
30
|
+
argument: The problematic argument name
|
31
|
+
"""
|
32
|
+
full_message = f"Invalid argument '{argument}': {message}" if argument else f"Invalid argument: {message}"
|
33
|
+
super().__init__(full_message)
|
34
|
+
self.argument = argument
|
35
|
+
|
36
|
+
|
37
|
+
class CliUsageError(CliError):
|
38
|
+
"""Exception for incorrect CLI usage."""
|
39
|
+
|
40
|
+
def __init__(self, message: str, suggestion: str | None = None) -> None:
|
41
|
+
"""
|
42
|
+
Initialize usage error.
|
43
|
+
|
44
|
+
Args:
|
45
|
+
message: The error message
|
46
|
+
suggestion: Optional suggestion for correct usage
|
47
|
+
"""
|
48
|
+
full_message = message
|
49
|
+
if suggestion:
|
50
|
+
full_message += f"\n\nSuggestion: {suggestion}"
|
51
|
+
super().__init__(full_message)
|
52
|
+
self.suggestion = suggestion
|
53
|
+
|
54
|
+
|
55
|
+
class CliConfigError(CliError):
|
56
|
+
"""Exception for CLI configuration issues."""
|
57
|
+
|
58
|
+
def __init__(self, message: str, config_path: str | None = None) -> None:
|
59
|
+
"""
|
60
|
+
Initialize config error.
|
61
|
+
|
62
|
+
Args:
|
63
|
+
message: The error message
|
64
|
+
config_path: Path to the problematic config file
|
65
|
+
"""
|
66
|
+
full_message = f"Configuration error in '{config_path}': {message}" if config_path else f"Configuration error: {message}"
|
67
|
+
super().__init__(full_message)
|
68
|
+
self.config_path = config_path
|
@@ -0,0 +1,117 @@
|
|
1
|
+
"""Configuration-related exceptions for db-drift."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
|
5
|
+
from db_drift.utils.exceptions.base import DbDriftUserError
|
6
|
+
from db_drift.utils.exceptions.status_codes import ExitCode
|
7
|
+
|
8
|
+
|
9
|
+
class ConfigError(DbDriftUserError):
|
10
|
+
"""Base class for configuration-related errors."""
|
11
|
+
|
12
|
+
def __init__(self, message: str, config_path: str | Path | None = None) -> None:
|
13
|
+
"""
|
14
|
+
Initialize configuration error.
|
15
|
+
|
16
|
+
Args:
|
17
|
+
message: The error message
|
18
|
+
config_path: Path to the problematic configuration file
|
19
|
+
"""
|
20
|
+
full_message = f"Configuration error in '{config_path}': {message}" if config_path else f"Configuration error: {message}"
|
21
|
+
|
22
|
+
super().__init__(full_message)
|
23
|
+
self.exit_code = ExitCode.CONFIG_ERROR
|
24
|
+
self.config_path = str(config_path) if config_path else None
|
25
|
+
|
26
|
+
|
27
|
+
class MissingConfigError(ConfigError):
|
28
|
+
"""Exception for missing configuration files or required settings."""
|
29
|
+
|
30
|
+
def __init__(
|
31
|
+
self,
|
32
|
+
message: str,
|
33
|
+
config_path: str | Path | None = None,
|
34
|
+
setting_name: str | None = None,
|
35
|
+
) -> None:
|
36
|
+
"""
|
37
|
+
Initialize missing config error.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
message: The error message
|
41
|
+
config_path: Path to the configuration file
|
42
|
+
setting_name: Name of the missing setting
|
43
|
+
"""
|
44
|
+
if setting_name and config_path:
|
45
|
+
full_message = f"Missing required setting '{setting_name}' in '{config_path}': {message}"
|
46
|
+
elif setting_name:
|
47
|
+
full_message = f"Missing required setting '{setting_name}': {message}"
|
48
|
+
elif config_path:
|
49
|
+
full_message = f"Missing configuration file '{config_path}': {message}"
|
50
|
+
else:
|
51
|
+
full_message = f"Missing configuration: {message}"
|
52
|
+
|
53
|
+
# Call ConfigError.__init__ instead of super() to avoid double processing
|
54
|
+
DbDriftUserError.__init__(self, full_message)
|
55
|
+
self.config_path = str(config_path) if config_path else None
|
56
|
+
self.setting_name = setting_name
|
57
|
+
|
58
|
+
|
59
|
+
class ConfigValidationError(ConfigError):
|
60
|
+
"""Exception for invalid configuration values."""
|
61
|
+
|
62
|
+
def __init__(
|
63
|
+
self,
|
64
|
+
message: str,
|
65
|
+
config_path: str | Path | None = None,
|
66
|
+
setting_name: str | None = None,
|
67
|
+
setting_value: str | None = None,
|
68
|
+
) -> None:
|
69
|
+
"""
|
70
|
+
Initialize validation error.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
message: The error message
|
74
|
+
config_path: Path to the configuration file
|
75
|
+
setting_name: Name of the invalid setting
|
76
|
+
setting_value: The invalid value
|
77
|
+
"""
|
78
|
+
if setting_name and setting_value:
|
79
|
+
full_message = f"Invalid value '{setting_value}' for setting '{setting_name}': {message}"
|
80
|
+
elif setting_name:
|
81
|
+
full_message = f"Invalid setting '{setting_name}': {message}"
|
82
|
+
else:
|
83
|
+
full_message = f"Invalid configuration: {message}"
|
84
|
+
|
85
|
+
# Call ConfigError.__init__ to get proper path handling
|
86
|
+
super().__init__(full_message, config_path)
|
87
|
+
self.setting_name = setting_name
|
88
|
+
self.setting_value = setting_value
|
89
|
+
|
90
|
+
|
91
|
+
class ConfigFormatError(ConfigError):
|
92
|
+
"""Exception for configuration file format errors."""
|
93
|
+
|
94
|
+
def __init__(
|
95
|
+
self,
|
96
|
+
message: str,
|
97
|
+
config_path: str | Path | None = None,
|
98
|
+
line_number: int | None = None,
|
99
|
+
) -> None:
|
100
|
+
"""
|
101
|
+
Initialize format error.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
message: The error message
|
105
|
+
config_path: Path to the configuration file
|
106
|
+
line_number: Line number where the error occurred
|
107
|
+
"""
|
108
|
+
if line_number and config_path:
|
109
|
+
full_message = f"Format error in '{config_path}' at line {line_number}: {message}"
|
110
|
+
elif config_path:
|
111
|
+
full_message = f"Format error in '{config_path}': {message}"
|
112
|
+
else:
|
113
|
+
full_message = f"Configuration format error: {message}"
|
114
|
+
|
115
|
+
# Call ConfigError.__init__ to get proper path handling
|
116
|
+
super().__init__(full_message, config_path)
|
117
|
+
self.line_number = line_number
|
@@ -0,0 +1,153 @@
|
|
1
|
+
"""Database-related exceptions for db-drift."""
|
2
|
+
|
3
|
+
from db_drift.utils.exceptions.base import DbDriftSystemError
|
4
|
+
from db_drift.utils.exceptions.status_codes import ExitCode
|
5
|
+
|
6
|
+
|
7
|
+
class DatabaseError(DbDriftSystemError):
|
8
|
+
"""Base class for database-related errors."""
|
9
|
+
|
10
|
+
def __init__(self, message: str, connection_string: str | None = None) -> None:
|
11
|
+
"""
|
12
|
+
Initialize database error.
|
13
|
+
|
14
|
+
Args:
|
15
|
+
message: The error message
|
16
|
+
connection_string: Optional connection string (will be redacted in logs)
|
17
|
+
"""
|
18
|
+
super().__init__(message)
|
19
|
+
self.connection_string = connection_string
|
20
|
+
|
21
|
+
|
22
|
+
class DatabaseConnectionError(DatabaseError):
|
23
|
+
"""Exception for database connection failures."""
|
24
|
+
|
25
|
+
def __init__(
|
26
|
+
self,
|
27
|
+
message: str,
|
28
|
+
connection_string: str | None = None,
|
29
|
+
host: str | None = None,
|
30
|
+
port: int | None = None,
|
31
|
+
database: str | None = None,
|
32
|
+
) -> None:
|
33
|
+
"""
|
34
|
+
Initialize connection error.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
message: The error message
|
38
|
+
connection_string: Optional connection string
|
39
|
+
host: Database host
|
40
|
+
port: Database port
|
41
|
+
database: Database name
|
42
|
+
"""
|
43
|
+
if host and database:
|
44
|
+
full_message = f"Failed to connect to database '{database}' on {host}"
|
45
|
+
if port:
|
46
|
+
full_message += f":{port}"
|
47
|
+
full_message += f": {message}"
|
48
|
+
else:
|
49
|
+
full_message = f"Database connection failed: {message}"
|
50
|
+
|
51
|
+
# Override the default exit code for connection errors
|
52
|
+
super().__init__(full_message, connection_string)
|
53
|
+
self.exit_code = ExitCode.UNAVAILABLE
|
54
|
+
self.host = host
|
55
|
+
self.port = port
|
56
|
+
self.database = database
|
57
|
+
|
58
|
+
|
59
|
+
class DatabaseQueryError(DatabaseError):
|
60
|
+
"""Exception for database query execution errors."""
|
61
|
+
|
62
|
+
def __init__(
|
63
|
+
self,
|
64
|
+
message: str,
|
65
|
+
query: str | None = None,
|
66
|
+
connection_string: str | None = None,
|
67
|
+
) -> None:
|
68
|
+
"""
|
69
|
+
Initialize query error.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
message: The error message
|
73
|
+
query: The problematic SQL query
|
74
|
+
connection_string: Optional connection string
|
75
|
+
"""
|
76
|
+
full_message = f"Database query failed: {message}"
|
77
|
+
super().__init__(full_message, connection_string)
|
78
|
+
self.exit_code = ExitCode.SOFTWARE_ERROR
|
79
|
+
self.query = query
|
80
|
+
|
81
|
+
|
82
|
+
class DatabaseSchemaError(DatabaseError):
|
83
|
+
"""Exception for database schema-related errors."""
|
84
|
+
|
85
|
+
def __init__(
|
86
|
+
self,
|
87
|
+
message: str,
|
88
|
+
schema_name: str | None = None,
|
89
|
+
connection_string: str | None = None,
|
90
|
+
) -> None:
|
91
|
+
"""
|
92
|
+
Initialize schema error.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
message: The error message
|
96
|
+
schema_name: The problematic schema name
|
97
|
+
connection_string: Optional connection string
|
98
|
+
"""
|
99
|
+
full_message = f"Schema '{schema_name}' error: {message}" if schema_name else f"Database schema error: {message}"
|
100
|
+
|
101
|
+
super().__init__(full_message, connection_string)
|
102
|
+
self.exit_code = ExitCode.DATA_ERROR
|
103
|
+
self.schema_name = schema_name
|
104
|
+
|
105
|
+
|
106
|
+
class DatabaseAuthenticationError(DatabaseError):
|
107
|
+
"""Exception for database authentication failures."""
|
108
|
+
|
109
|
+
def __init__(
|
110
|
+
self,
|
111
|
+
message: str = "Authentication failed",
|
112
|
+
username: str | None = None,
|
113
|
+
connection_string: str | None = None,
|
114
|
+
) -> None:
|
115
|
+
"""
|
116
|
+
Initialize authentication error.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
message: The error message
|
120
|
+
username: The username that failed authentication
|
121
|
+
connection_string: Optional connection string
|
122
|
+
"""
|
123
|
+
full_message = f"Authentication failed for user '{username}': {message}" if username else f"Database authentication failed: {message}"
|
124
|
+
|
125
|
+
super().__init__(full_message, connection_string)
|
126
|
+
self.exit_code = ExitCode.NO_PERMISSION
|
127
|
+
self.username = username
|
128
|
+
|
129
|
+
|
130
|
+
class DatabaseTimeoutError(DatabaseError):
|
131
|
+
"""Exception for database operation timeouts."""
|
132
|
+
|
133
|
+
def __init__(
|
134
|
+
self,
|
135
|
+
message: str = "Operation timed out",
|
136
|
+
timeout_seconds: float | None = None,
|
137
|
+
connection_string: str | None = None,
|
138
|
+
) -> None:
|
139
|
+
"""
|
140
|
+
Initialize timeout error.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
message: The error message
|
144
|
+
timeout_seconds: The timeout value that was exceeded
|
145
|
+
connection_string: Optional connection string
|
146
|
+
"""
|
147
|
+
if timeout_seconds:
|
148
|
+
full_message = f"Database operation timed out after {timeout_seconds}s: {message}"
|
149
|
+
else:
|
150
|
+
full_message = f"Database operation timed out: {message}"
|
151
|
+
|
152
|
+
super().__init__(full_message, connection_string)
|
153
|
+
self.timeout_seconds = timeout_seconds
|
@@ -0,0 +1,175 @@
|
|
1
|
+
"""Error formatting utilities for db-drift."""
|
2
|
+
|
3
|
+
import logging
|
4
|
+
import sys
|
5
|
+
import traceback
|
6
|
+
from typing import TextIO
|
7
|
+
|
8
|
+
from db_drift.utils.exceptions import CliArgumentError, DatabaseConnectionError, MissingConfigError
|
9
|
+
from db_drift.utils.exceptions.base import DbDriftError, DbDriftSystemError
|
10
|
+
from db_drift.utils.exceptions.status_codes import ExitCode
|
11
|
+
|
12
|
+
|
13
|
+
def format_error_message(error: Exception, show_traceback: bool = False) -> str: # noqa: FBT001, FBT002
|
14
|
+
"""
|
15
|
+
Format an error message for display to the user.
|
16
|
+
|
17
|
+
Args:
|
18
|
+
error: The exception to format
|
19
|
+
show_traceback: Whether to include traceback information. Default is False.
|
20
|
+
|
21
|
+
Returns:
|
22
|
+
Formatted error message
|
23
|
+
"""
|
24
|
+
if isinstance(error, DbDriftError):
|
25
|
+
message = str(error)
|
26
|
+
else:
|
27
|
+
# For unexpected exceptions, provide a generic message
|
28
|
+
message = f"An unexpected error occurred: {error.__class__.__name__}"
|
29
|
+
if str(error):
|
30
|
+
message += f": {error}"
|
31
|
+
|
32
|
+
if show_traceback:
|
33
|
+
message += f"\n\nTraceback:\n{''.join(traceback.format_exception(type(error), error, error.__traceback__))}"
|
34
|
+
|
35
|
+
return message
|
36
|
+
|
37
|
+
|
38
|
+
def format_suggestion(error: Exception) -> str | None:
|
39
|
+
"""
|
40
|
+
Generate helpful suggestions for common errors.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
error: The exception to generate suggestions for
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Suggestion string or None if no suggestion is available
|
47
|
+
"""
|
48
|
+
if isinstance(error, CliArgumentError):
|
49
|
+
return "Use 'db-drift --help' to see available options and their usage."
|
50
|
+
|
51
|
+
if isinstance(error, DatabaseConnectionError):
|
52
|
+
suggestions = [
|
53
|
+
"Check that the database server is running and accessible",
|
54
|
+
"Verify your connection parameters (host, port, database name)",
|
55
|
+
"Ensure your credentials are correct",
|
56
|
+
"Check network connectivity and firewall settings",
|
57
|
+
]
|
58
|
+
return "Try the following:\n" + "\n".join(f" • {suggestion}" for suggestion in suggestions)
|
59
|
+
|
60
|
+
if isinstance(error, MissingConfigError):
|
61
|
+
return "Create a configuration file or provide the required settings via command-line arguments."
|
62
|
+
|
63
|
+
return None
|
64
|
+
|
65
|
+
|
66
|
+
def print_error(
|
67
|
+
error: Exception,
|
68
|
+
file: TextIO = sys.stderr,
|
69
|
+
show_traceback: bool = False, # noqa: FBT001, FBT002
|
70
|
+
show_suggestions: bool = True, # noqa: FBT001, FBT002
|
71
|
+
) -> None:
|
72
|
+
"""
|
73
|
+
Print a formatted error message to the specified file.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
error: The exception to print
|
77
|
+
file: The file to write to (default: stderr)
|
78
|
+
show_traceback: Whether to include traceback information
|
79
|
+
show_suggestions: Whether to show helpful suggestions
|
80
|
+
"""
|
81
|
+
message = format_error_message(error, show_traceback)
|
82
|
+
print(f"Error: {message}", file=file)
|
83
|
+
|
84
|
+
if show_suggestions:
|
85
|
+
suggestion = format_suggestion(error)
|
86
|
+
if suggestion:
|
87
|
+
print(f"\n{suggestion}", file=file)
|
88
|
+
|
89
|
+
|
90
|
+
def get_exit_code(error: Exception) -> int:
|
91
|
+
"""
|
92
|
+
Get the appropriate exit code for an exception.
|
93
|
+
|
94
|
+
Args:
|
95
|
+
error: The exception
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Exit code integer
|
99
|
+
"""
|
100
|
+
if isinstance(error, DbDriftError):
|
101
|
+
return error.exit_code
|
102
|
+
|
103
|
+
# Standard exit codes for common Python exceptions
|
104
|
+
if isinstance(error, KeyboardInterrupt):
|
105
|
+
return ExitCode.SIGINT
|
106
|
+
if isinstance(error, FileNotFoundError):
|
107
|
+
return ExitCode.NO_INPUT
|
108
|
+
if isinstance(error, PermissionError):
|
109
|
+
return ExitCode.NO_PERMISSION
|
110
|
+
if isinstance(error, ConnectionError):
|
111
|
+
return ExitCode.UNAVAILABLE
|
112
|
+
|
113
|
+
# Default generic error
|
114
|
+
return ExitCode.GENERAL_ERROR
|
115
|
+
|
116
|
+
|
117
|
+
def handle_error_and_exit(
|
118
|
+
error: Exception,
|
119
|
+
logger: logging.Logger | None = None,
|
120
|
+
show_traceback: bool = False, # noqa: FBT001, FBT002
|
121
|
+
show_suggestions: bool = True, # noqa: FBT001, FBT002
|
122
|
+
) -> None:
|
123
|
+
"""
|
124
|
+
Handle an error by logging it, printing user-friendly message, and exiting.
|
125
|
+
|
126
|
+
Args:
|
127
|
+
error: The exception to handle
|
128
|
+
logger: Logger instance for detailed logging
|
129
|
+
show_traceback: Whether to show traceback to user
|
130
|
+
show_suggestions: Whether to show helpful suggestions
|
131
|
+
"""
|
132
|
+
exit_code = get_exit_code(error)
|
133
|
+
|
134
|
+
# Log the full error details
|
135
|
+
if logger:
|
136
|
+
if isinstance(error, DbDriftError):
|
137
|
+
logger.error(f"{error.__class__.__name__}: {error}")
|
138
|
+
else:
|
139
|
+
logger.exception(f"Unexpected error: {error.__class__.__name__}: {error}")
|
140
|
+
|
141
|
+
# Print user-friendly error
|
142
|
+
print_error(error, show_traceback=show_traceback, show_suggestions=show_suggestions)
|
143
|
+
|
144
|
+
sys.exit(exit_code)
|
145
|
+
|
146
|
+
|
147
|
+
def handle_unexpected_error(debug_mode: int, logger: logging.Logger) -> None:
|
148
|
+
"""
|
149
|
+
Handle unexpected errors based on debug mode.
|
150
|
+
|
151
|
+
Args:
|
152
|
+
debug_mode(int): Whether debug mode is enabled (1/True) or not (0/False).
|
153
|
+
logger(logging.Logger): The logger instance to use.
|
154
|
+
"""
|
155
|
+
if debug_mode:
|
156
|
+
# For debug mode, we'll handle the current exception
|
157
|
+
_, exc_value, _ = sys.exc_info()
|
158
|
+
if exc_value:
|
159
|
+
handle_error_and_exit(
|
160
|
+
exc_value,
|
161
|
+
logger=logger,
|
162
|
+
show_traceback=True,
|
163
|
+
show_suggestions=False,
|
164
|
+
)
|
165
|
+
else:
|
166
|
+
system_error = DbDriftSystemError(
|
167
|
+
"An unexpected error occurred. Set DB_DRIFT_DEBUG=1 for more details.",
|
168
|
+
exit_code=ExitCode.GENERAL_ERROR,
|
169
|
+
)
|
170
|
+
handle_error_and_exit(
|
171
|
+
system_error,
|
172
|
+
logger=logger,
|
173
|
+
show_traceback=False,
|
174
|
+
show_suggestions=False,
|
175
|
+
)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
from enum import Enum, unique
|
2
|
+
|
3
|
+
|
4
|
+
@unique
|
5
|
+
class ExitCode(Enum):
|
6
|
+
SUCCESS = 0
|
7
|
+
GENERAL_ERROR = 1
|
8
|
+
USAGE_ERROR = 2
|
9
|
+
DATA_ERROR = 65
|
10
|
+
NO_INPUT = 66
|
11
|
+
UNAVAILABLE = 69
|
12
|
+
SOFTWARE_ERROR = 70
|
13
|
+
NO_PERMISSION = 77
|
14
|
+
CONFIG_ERROR = 78
|
15
|
+
SIGINT = 130
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: db-drift
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.1.0
|
4
4
|
Summary: A command line tool to visualize the differences between two DB states.
|
5
5
|
Project-URL: source, https://github.com/dyka3773/db-drift
|
6
6
|
Author-email: Hercules Konsoulas <dyka3773@gmail.com>
|
@@ -17,13 +17,13 @@ Classifier: Topic :: Software Development :: Build Tools
|
|
17
17
|
Classifier: Topic :: Software Development :: Version Control
|
18
18
|
Classifier: Topic :: Utilities
|
19
19
|
Classifier: Typing :: Typed
|
20
|
-
Requires-Python: >=3.
|
20
|
+
Requires-Python: >=3.10
|
21
21
|
Requires-Dist: oracledb>=3.3.0
|
22
22
|
Requires-Dist: psycopg[binary]>=3.2.9
|
23
23
|
Requires-Dist: pymysql>=1.1.1
|
24
24
|
Requires-Dist: python-dotenv>=1.1.1
|
25
|
-
Requires-Dist: pyyaml>=6.0.
|
26
|
-
Requires-Dist: sqlmodel>=0.0.
|
25
|
+
Requires-Dist: pyyaml>=6.0.3
|
26
|
+
Requires-Dist: sqlmodel>=0.0.25
|
27
27
|
Requires-Dist: sshtunnel>=0.4.0
|
28
28
|
Description-Content-Type: text/markdown
|
29
29
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
db_drift/__init__.py,sha256=_NNjAEiomu4yVTJIPFaLwCtInFaSItHwbFIOx5V122A,49
|
2
|
+
db_drift/__main__.py,sha256=IRyqTl3BK8VaSLkUgdl1YZSYbVF4xJA7gMHW-SWAIzI,3210
|
3
|
+
db_drift/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
4
|
+
db_drift/cli/cli.py,sha256=GrixgWJrModpQMspMO1Mzglu3lQdqa5AjZIWKxK1WAE,1060
|
5
|
+
db_drift/cli/utils.py,sha256=n1V6flmUPLAy1uAtYLIyRnJJWBAwzYERyHGa2OkpZnU,229
|
6
|
+
db_drift/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
7
|
+
db_drift/utils/custom_logging.py,sha256=Kl6yvx7csl3c7n7U2PJLI6PQ5JtGoBwQoYwrWxrg5KE,1672
|
8
|
+
db_drift/utils/exceptions/__init__.py,sha256=upEKs7xQu9iIRMqZbRe2JmjaKkJa1FKdoqKiMQUlaDU,1134
|
9
|
+
db_drift/utils/exceptions/base.py,sha256=BCFYa13G437_MBARRepvn44HOti60jvKTFVRqvyyqxQ,1680
|
10
|
+
db_drift/utils/exceptions/cli.py,sha256=VZAK5YsW3vyrJ1y0p_aO8A2Qpdk0v9-eRoBVpgn0in4,2136
|
11
|
+
db_drift/utils/exceptions/config.py,sha256=Ut58eUVjaaKFf04KzrR2PeuMjHwyrHlOQpwQwSjNdYw,4089
|
12
|
+
db_drift/utils/exceptions/database.py,sha256=V5ofixnWCLxW6EIHiyEpkCip_anc83ZFABi0RloYabs,4821
|
13
|
+
db_drift/utils/exceptions/formatting.py,sha256=QeDL2-sSg5qatFS-JwYCIB3RqLTOwXdRxWD9LVOnMzE,5580
|
14
|
+
db_drift/utils/exceptions/status_codes.py,sha256=Uk5hGkWtAvl5qsWroSQzHjQPKGAaKbucsFANO-b8b9E,265
|
15
|
+
db_drift-1.1.0.dist-info/METADATA,sha256=YTsG-6ySqF1swoCLZdr56tayAMhf2ajLRjK6eY0mChw,3128
|
16
|
+
db_drift-1.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
17
|
+
db_drift-1.1.0.dist-info/entry_points.txt,sha256=pFISFFjEb5q5Z0kYsCdrxvKRij6mvTtntx1uiago-7U,43
|
18
|
+
db_drift-1.1.0.dist-info/licenses/LICENSE,sha256=4zi6unpe17RUDMBu7ebh14jdbyvyeT-UA3n8Zl7aW74,1075
|
19
|
+
db_drift-1.1.0.dist-info/RECORD,,
|
db_drift-1.0.0.dist-info/RECORD
DELETED
@@ -1,6 +0,0 @@
|
|
1
|
-
db_drift/__init__.py,sha256=16f_yXLDNRuhMJCzDoBN5FWbXQ0x7cz3rxABt_03L2o,54
|
2
|
-
db_drift-1.0.0.dist-info/METADATA,sha256=2JaVQcwMW_4mzWf5mWBQD9oi-o4oEOxhflNcVNZK6QE,3128
|
3
|
-
db_drift-1.0.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
4
|
-
db_drift-1.0.0.dist-info/entry_points.txt,sha256=pFISFFjEb5q5Z0kYsCdrxvKRij6mvTtntx1uiago-7U,43
|
5
|
-
db_drift-1.0.0.dist-info/licenses/LICENSE,sha256=4zi6unpe17RUDMBu7ebh14jdbyvyeT-UA3n8Zl7aW74,1075
|
6
|
-
db_drift-1.0.0.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|