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 CHANGED
@@ -1,2 +1 @@
1
- def main() -> None:
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()
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
@@ -0,0 +1,9 @@
1
+ from importlib import metadata
2
+
3
+
4
+ def get_version() -> str:
5
+ """Get the current version of the package."""
6
+ try:
7
+ return metadata.version("db-drift")
8
+ except metadata.PackageNotFoundError:
9
+ return "unknown"
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.0.0
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.13
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.2
26
- Requires-Dist: sqlmodel>=0.0.24
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,,
@@ -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,,