postgres-dbman 0.1.0__tar.gz

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.
@@ -0,0 +1,155 @@
1
+ Metadata-Version: 2.4
2
+ Name: postgres-dbman
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: docker>=7.1.0
8
+ Requires-Dist: pre-commit>=4.2.0
9
+ Requires-Dist: python-decouple>=3.8
10
+ Requires-Dist: ruff>=0.11.4
11
+ Requires-Dist: zstandard>=0.22.0
12
+
13
+ # DBMan
14
+
15
+ DBMan is an CLI tool to manage PostgreSQL databases running in separate Docker containers.
16
+ The content of the databases is obtained from pg_dumps of production databases.
17
+
18
+ It will assign each database to a unique ZFS dataset inside a predefined ZFS dataset.
19
+ It will allow snapshotting the dataset and restoring the dataset from a snapshot.
20
+
21
+ ## Configuration
22
+
23
+ DBMan uses environment variables for configuration. Copy `.env.example` to `.env` and modify the values as needed:
24
+
25
+ ```bash
26
+ # ZFS configuration
27
+ DBMAN_ZFS_DATASET=big/docker-postgres
28
+ DBMAN_ZFS_MOUNT_POINT=/mnt/big/docker-postgres
29
+ DBMAN_ZFS_COMPRESSION=lz4 # Default compression algorithm for new databases
30
+
31
+ # Docker configuration
32
+ DBMAN_DOCKER_IMAGE=postgres:latest
33
+
34
+ # PostgreSQL configuration
35
+ DBMAN_POSTGRES_USER=postgres
36
+ DBMAN_POSTGRES_PASSWORD=postgres
37
+
38
+ # Database configuration
39
+ DBMAN_SQLITE_DB=~/.dbman/dbman.db
40
+ ```
41
+
42
+ You can also override the ZFS compression algorithm using the `-c` or `--compression` command line option:
43
+
44
+ ```bash
45
+ uv run dbman.py -c zstd create mydb dump.sql
46
+ ```
47
+
48
+ ## Installation
49
+
50
+ 1. You need `uv` to run dbman. More info about installing it can be found here
51
+ https://docs.astral.sh/uv/getting-started/installation/
52
+
53
+ 2. Copy `.env.example` to `.env` and configure your environment variables.
54
+
55
+ 3. Test it:
56
+
57
+ ```bash
58
+ uv run dbman.py
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ The tool can be used to:
64
+
65
+ - create new databases from pg_dumps - it will automatically create a new Docker container
66
+ with the data from the dump. It will allow naming the container and the database. It will
67
+ automatically assign a port to the container.
68
+
69
+ ```bash
70
+ # Create a database with default compression (inherited from parent ZFS dataset)
71
+ uv run dbman.py create mydb dump.sql
72
+ # or using the short command
73
+ uv run dbman.py c mydb dump.sql
74
+
75
+ # Create a database with specific compression
76
+ uv run dbman.py create -c zstd mydb dump.sql
77
+ # or using the short command
78
+ uv run dbman.py c -c zstd mydb dump.sql
79
+ ```
80
+
81
+ - list all databases and their statuses
82
+
83
+ ```bash
84
+ uv run dbman.py list
85
+ # or using the short command
86
+ uv run dbman.py ls
87
+ ```
88
+
89
+ - create a snapshot of a database
90
+
91
+ ```bash
92
+ uv run dbman.py snapshot mydb mysnapshot
93
+ # or using the short command
94
+ uv run dbman.py snap mydb mysnapshot
95
+ ```
96
+
97
+ - restore a database from a snapshot
98
+
99
+ ```bash
100
+ uv run dbman.py restore mydb mysnapshot
101
+ # or using the short command
102
+ uv run dbman.py rest mydb mysnapshot
103
+ ```
104
+
105
+ - list all snapshots of a database
106
+
107
+ ```bash
108
+ uv run dbman.py list-snapshots mydb
109
+ # or using the short command
110
+ uv run dbman.py lss mydb
111
+ ```
112
+
113
+ - start a subshell with database environment variables
114
+
115
+ ```bash
116
+ uv run dbman.py shell mydb
117
+ # or using the short command
118
+ uv run dbman.py sh mydb
119
+ ```
120
+
121
+ - print database environment variables
122
+
123
+ ```bash
124
+ # Print variables
125
+ uv run dbman.py env mydb
126
+ # or using the short command
127
+ uv run dbman.py e mydb
128
+
129
+ # Export variables to the current shell
130
+ eval $(uv run dbman.py env --export mydb)
131
+
132
+ # Unset variables from the current shell
133
+ eval $(uv run dbman.py env --clean mydb)
134
+ ```
135
+
136
+ - destroy a database
137
+ ```bash
138
+ uv run dbman.py destroy mydb
139
+ # or using the short command
140
+ uv run dbman.py del mydb
141
+ ```
142
+
143
+ For detailed usage information, run:
144
+
145
+ ```bash
146
+ uv run dbman.py --help
147
+ ```
148
+
149
+ ## Note
150
+
151
+ This tool was also used as a proof-of-concept in creating code by using AI. I started
152
+ with a docstring in dbman.py and asked `Cursor` to create an implementation for me.
153
+ It created a functional ~250 lines of code which mostly did what I wanted. When I tweaked
154
+ and improved it, I also extensively used AI to do the modifications, rather than coding it
155
+ myself.
@@ -0,0 +1,143 @@
1
+ # DBMan
2
+
3
+ DBMan is an CLI tool to manage PostgreSQL databases running in separate Docker containers.
4
+ The content of the databases is obtained from pg_dumps of production databases.
5
+
6
+ It will assign each database to a unique ZFS dataset inside a predefined ZFS dataset.
7
+ It will allow snapshotting the dataset and restoring the dataset from a snapshot.
8
+
9
+ ## Configuration
10
+
11
+ DBMan uses environment variables for configuration. Copy `.env.example` to `.env` and modify the values as needed:
12
+
13
+ ```bash
14
+ # ZFS configuration
15
+ DBMAN_ZFS_DATASET=big/docker-postgres
16
+ DBMAN_ZFS_MOUNT_POINT=/mnt/big/docker-postgres
17
+ DBMAN_ZFS_COMPRESSION=lz4 # Default compression algorithm for new databases
18
+
19
+ # Docker configuration
20
+ DBMAN_DOCKER_IMAGE=postgres:latest
21
+
22
+ # PostgreSQL configuration
23
+ DBMAN_POSTGRES_USER=postgres
24
+ DBMAN_POSTGRES_PASSWORD=postgres
25
+
26
+ # Database configuration
27
+ DBMAN_SQLITE_DB=~/.dbman/dbman.db
28
+ ```
29
+
30
+ You can also override the ZFS compression algorithm using the `-c` or `--compression` command line option:
31
+
32
+ ```bash
33
+ uv run dbman.py -c zstd create mydb dump.sql
34
+ ```
35
+
36
+ ## Installation
37
+
38
+ 1. You need `uv` to run dbman. More info about installing it can be found here
39
+ https://docs.astral.sh/uv/getting-started/installation/
40
+
41
+ 2. Copy `.env.example` to `.env` and configure your environment variables.
42
+
43
+ 3. Test it:
44
+
45
+ ```bash
46
+ uv run dbman.py
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ The tool can be used to:
52
+
53
+ - create new databases from pg_dumps - it will automatically create a new Docker container
54
+ with the data from the dump. It will allow naming the container and the database. It will
55
+ automatically assign a port to the container.
56
+
57
+ ```bash
58
+ # Create a database with default compression (inherited from parent ZFS dataset)
59
+ uv run dbman.py create mydb dump.sql
60
+ # or using the short command
61
+ uv run dbman.py c mydb dump.sql
62
+
63
+ # Create a database with specific compression
64
+ uv run dbman.py create -c zstd mydb dump.sql
65
+ # or using the short command
66
+ uv run dbman.py c -c zstd mydb dump.sql
67
+ ```
68
+
69
+ - list all databases and their statuses
70
+
71
+ ```bash
72
+ uv run dbman.py list
73
+ # or using the short command
74
+ uv run dbman.py ls
75
+ ```
76
+
77
+ - create a snapshot of a database
78
+
79
+ ```bash
80
+ uv run dbman.py snapshot mydb mysnapshot
81
+ # or using the short command
82
+ uv run dbman.py snap mydb mysnapshot
83
+ ```
84
+
85
+ - restore a database from a snapshot
86
+
87
+ ```bash
88
+ uv run dbman.py restore mydb mysnapshot
89
+ # or using the short command
90
+ uv run dbman.py rest mydb mysnapshot
91
+ ```
92
+
93
+ - list all snapshots of a database
94
+
95
+ ```bash
96
+ uv run dbman.py list-snapshots mydb
97
+ # or using the short command
98
+ uv run dbman.py lss mydb
99
+ ```
100
+
101
+ - start a subshell with database environment variables
102
+
103
+ ```bash
104
+ uv run dbman.py shell mydb
105
+ # or using the short command
106
+ uv run dbman.py sh mydb
107
+ ```
108
+
109
+ - print database environment variables
110
+
111
+ ```bash
112
+ # Print variables
113
+ uv run dbman.py env mydb
114
+ # or using the short command
115
+ uv run dbman.py e mydb
116
+
117
+ # Export variables to the current shell
118
+ eval $(uv run dbman.py env --export mydb)
119
+
120
+ # Unset variables from the current shell
121
+ eval $(uv run dbman.py env --clean mydb)
122
+ ```
123
+
124
+ - destroy a database
125
+ ```bash
126
+ uv run dbman.py destroy mydb
127
+ # or using the short command
128
+ uv run dbman.py del mydb
129
+ ```
130
+
131
+ For detailed usage information, run:
132
+
133
+ ```bash
134
+ uv run dbman.py --help
135
+ ```
136
+
137
+ ## Note
138
+
139
+ This tool was also used as a proof-of-concept in creating code by using AI. I started
140
+ with a docstring in dbman.py and asked `Cursor` to create an implementation for me.
141
+ It created a functional ~250 lines of code which mostly did what I wanted. When I tweaked
142
+ and improved it, I also extensively used AI to do the modifications, rather than coding it
143
+ myself.
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "postgres-dbman"
7
+ version = "0.1.0"
8
+ description = "Add your description here"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ dependencies = [
12
+ "docker>=7.1.0",
13
+ "pre-commit>=4.2.0",
14
+ "python-decouple>=3.8",
15
+ "ruff>=0.11.4",
16
+ "zstandard>=0.22.0",
17
+ ]
18
+
19
+ [project.scripts]
20
+ dbman = "dbman.cli:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+
25
+ [tool.ruff]
26
+ line-length = 100
27
+ target-version = "py311"
28
+
29
+ [tool.ruff.lint]
30
+ select = [
31
+ "E", # pycodestyle errors
32
+ "W", # pycodestyle warnings
33
+ "F", # pyflakes
34
+ "I", # isort
35
+ "B", # flake8-bugbear
36
+ "C4", # flake8-comprehensions
37
+ "UP", # pyupgrade
38
+ "PL", # pylint
39
+ "RUF", # ruff-specific rules
40
+ ]
41
+ ignore = []
42
+
43
+ [tool.ruff.format]
44
+ quote-style = "double"
45
+ indent-style = "space"
46
+ line-ending = "auto"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """DBMan - PostgreSQL database management tool for Docker and ZFS."""
2
+
3
+ from .core import DEFAULT_COMPRESSION, DBMan, format_output
4
+
5
+ __all__ = ["DEFAULT_COMPRESSION", "DBMan", "format_output"]
@@ -0,0 +1,6 @@
1
+ """Allow running the package as python -m dbman."""
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ main()
@@ -0,0 +1,292 @@
1
+ """CLI entry point for DBMan."""
2
+
3
+ import argparse
4
+ import logging
5
+ import os
6
+ import re
7
+ import sys
8
+ import traceback
9
+
10
+ from .core import DEFAULT_COMPRESSION, DBMan, format_output
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def main() -> None: # noqa: PLR0915, PLR0912
16
+ parser = argparse.ArgumentParser(description="DBMan - PostgreSQL database management tool")
17
+ parser.add_argument(
18
+ "-f", "--force", action="store_true", help="Force recreation of ZFS datasets"
19
+ )
20
+ parser.add_argument("-v", "--verbose", action="store_true", help="Enable verbose logging")
21
+ parser.add_argument("--json", action="store_true", help="Output in JSON format")
22
+
23
+ subparsers = parser.add_subparsers(dest="command", help="Commands", required=True)
24
+
25
+ # Create database command
26
+ create_parser = subparsers.add_parser("create", aliases=["c"], help="Create a new database")
27
+ create_parser.add_argument(
28
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
29
+ )
30
+ create_parser.add_argument("dump_path", help="Path to pg_dump file")
31
+ create_parser.add_argument("--port", type=int, help="Port to expose (optional)")
32
+ create_parser.add_argument(
33
+ "-c",
34
+ "--compression",
35
+ default=DEFAULT_COMPRESSION,
36
+ help=f"ZFS compression algorithm (default: {DEFAULT_COMPRESSION})",
37
+ )
38
+ create_parser.add_argument(
39
+ "--postgres-config",
40
+ action="append",
41
+ help="PostgreSQL configuration option in format key=value (can be used multiple times)",
42
+ )
43
+
44
+ # List databases command
45
+ subparsers.add_parser("list", aliases=["ls"], help="List all databases")
46
+
47
+ # Snapshot commands
48
+ snapshot_parser = subparsers.add_parser("snapshot", aliases=["snap"], help="Create a snapshot")
49
+ snapshot_parser.add_argument(
50
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
51
+ )
52
+ snapshot_parser.add_argument("snapshot_name", help="Snapshot name")
53
+ snapshot_parser.add_argument(
54
+ "--add",
55
+ action="store_true",
56
+ help="Add an existing ZFS snapshot to the database",
57
+ )
58
+
59
+ restore_parser = subparsers.add_parser(
60
+ "restore", aliases=["rest"], help="Restore from snapshot"
61
+ )
62
+ restore_parser.add_argument(
63
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
64
+ )
65
+ restore_parser.add_argument("snapshot_name", help="Snapshot name or number")
66
+
67
+ list_snapshots_parser = subparsers.add_parser(
68
+ "list-snapshots", aliases=["lss"], help="List snapshots"
69
+ )
70
+ list_snapshots_parser.add_argument("name", nargs="?", help="Database name (optional)")
71
+
72
+ # Shell command
73
+ shell_parser = subparsers.add_parser(
74
+ "shell", aliases=["sh"], help="Start a subshell with database environment variables"
75
+ )
76
+ shell_parser.add_argument(
77
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
78
+ )
79
+
80
+ # Env command
81
+ env_parser = subparsers.add_parser(
82
+ "env", aliases=["e"], help="Print database environment variables"
83
+ )
84
+ env_parser.add_argument(
85
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
86
+ )
87
+ env_parser.add_argument(
88
+ "--export",
89
+ "-e",
90
+ action="store_true",
91
+ help="Export environment variables to the current shell",
92
+ )
93
+ env_parser.add_argument(
94
+ "--clean",
95
+ action="store_true",
96
+ help="Unset environment variables from the current shell",
97
+ )
98
+
99
+ # Destroy command
100
+ destroy_parser = subparsers.add_parser(
101
+ "destroy", aliases=["del", "rm"], help="Destroy a database"
102
+ )
103
+ destroy_parser.add_argument(
104
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
105
+ )
106
+ destroy_parser.add_argument(
107
+ "--keep-zfs-dataset",
108
+ action="store_true",
109
+ help="Keep the ZFS dataset (by default, the ZFS dataset is removed)",
110
+ )
111
+
112
+ # Start command
113
+ start_parser = subparsers.add_parser("start", help="Start a stopped database")
114
+ start_parser.add_argument(
115
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
116
+ )
117
+
118
+ # Stop command
119
+ stop_parser = subparsers.add_parser("stop", help="Stop a running database")
120
+ stop_parser.add_argument(
121
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
122
+ )
123
+
124
+ # Restart command
125
+ restart_parser = subparsers.add_parser("restart", help="Restart a database")
126
+ restart_parser.add_argument(
127
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
128
+ )
129
+
130
+ # Cleanup command
131
+ cleanup_parser = subparsers.add_parser(
132
+ "cleanup", help="List orphaned mountpoints (directories without corresponding databases)"
133
+ )
134
+ cleanup_parser.add_argument(
135
+ "--one-line",
136
+ "-1",
137
+ action="store_true",
138
+ help="Output only mountpoint names on a single line separated by spaces",
139
+ )
140
+
141
+ # Clone command
142
+ clone_parser = subparsers.add_parser("clone", help="Clone a database from a snapshot")
143
+ clone_parser.add_argument(
144
+ "source_name",
145
+ nargs="?",
146
+ help="Source database name (optional if DBMAN_DB_NAME is set)",
147
+ )
148
+ clone_parser.add_argument("snapshot_name", help="Snapshot name to clone from")
149
+ clone_parser.add_argument("new_name", help="Name for the new cloned database")
150
+ clone_parser.add_argument("--port", type=int, help="Port to expose (optional)")
151
+
152
+ # Export command
153
+ export_parser = subparsers.add_parser(
154
+ "export", aliases=["exp"], help="Export a database to a ZFS send file"
155
+ )
156
+ export_parser.add_argument(
157
+ "name", nargs="?", help="Database name (optional if DBMAN_DB_NAME is set)"
158
+ )
159
+ export_parser.add_argument(
160
+ "output_file",
161
+ nargs="?",
162
+ help="Output file path (optional; auto-generated from name and snapshot if omitted)",
163
+ )
164
+ export_parser.add_argument(
165
+ "--snapshot",
166
+ "-s",
167
+ dest="snapshot_name",
168
+ help="Snapshot name to export (optional; auto-created as export-<timestamp> if omitted)",
169
+ )
170
+
171
+ # Import command
172
+ import_parser = subparsers.add_parser(
173
+ "import", aliases=["imp"], help="Import a database from a ZFS send file"
174
+ )
175
+ import_parser.add_argument("input_file", help="Path to the ZFS send file")
176
+ import_parser.add_argument(
177
+ "new_name",
178
+ nargs="?",
179
+ help="Name for the imported database (optional; detected from stream if omitted)",
180
+ )
181
+ import_parser.add_argument("--port", type=int, help="Port to expose (optional)")
182
+ import_parser.add_argument(
183
+ "--compression",
184
+ "-c",
185
+ help="ZFS compression algorithm for the received dataset (e.g. lz4, zstd)",
186
+ )
187
+
188
+ # Delete older than command
189
+ delete_older_parser = subparsers.add_parser(
190
+ "delete_older_than",
191
+ aliases=["dot"],
192
+ help="List or delete databases older than specified days",
193
+ )
194
+ delete_older_parser.add_argument(
195
+ "days", type=int, help="Number of days - databases older than this will be listed/deleted"
196
+ )
197
+ delete_older_parser.add_argument(
198
+ "--do-it",
199
+ action="store_true",
200
+ help="Actually delete the databases (without this flag, only lists them)",
201
+ )
202
+
203
+ args = parser.parse_args()
204
+
205
+ # Set logging level based on verbose flag
206
+ if args.verbose:
207
+ logging.getLogger("dbman").setLevel(logging.DEBUG)
208
+
209
+ # Get database name from environment variable if not provided
210
+ def get_db_name(name: str | None, allow_none: bool = False) -> str:
211
+ if name is not None:
212
+ return name
213
+ env_name = os.environ.get("DBMAN_DB_NAME")
214
+ if env_name is None and not allow_none:
215
+ raise ValueError(
216
+ "Database name must be provided either as an argument or via DBMAN_DB_NAME "
217
+ "environment variable"
218
+ )
219
+ return env_name
220
+
221
+ try:
222
+ dbman = DBMan(args.force)
223
+
224
+ if args.command in ["create", "c"]:
225
+ # Parse PostgreSQL configuration options
226
+ postgres_config = {}
227
+ if args.postgres_config:
228
+ for cfg in args.postgres_config:
229
+ try:
230
+ key, value = cfg.split("=", 1)
231
+ postgres_config[key] = value
232
+ except ValueError:
233
+ logger.error(f"Invalid configuration format: {cfg}")
234
+ sys.exit(1)
235
+ dbman.create_database(
236
+ get_db_name(args.name), args.dump_path, args.port, args.compression, postgres_config
237
+ )
238
+ elif args.command in ["list", "ls"]:
239
+ databases = dbman.list_databases()
240
+ format_output(databases, args.json)
241
+ elif args.command in ["snapshot", "snap"]:
242
+ if args.add:
243
+ dbman.add_snapshot(get_db_name(args.name), args.snapshot_name)
244
+ else:
245
+ dbman.create_snapshot(get_db_name(args.name), args.snapshot_name)
246
+ elif args.command in ["restore", "rest"]:
247
+ # only allow negative numbers for snapshot numbers
248
+ if re.match(r"^-\d+$", args.snapshot_name):
249
+ dbman.restore_snapshot(get_db_name(args.name), int(args.snapshot_name))
250
+ else:
251
+ dbman.restore_snapshot(get_db_name(args.name), args.snapshot_name)
252
+ elif args.command in ["list-snapshots", "lss"]:
253
+ snapshots = dbman.list_snapshots(get_db_name(args.name, allow_none=True))
254
+ format_output(snapshots, args.json)
255
+ elif args.command in ["shell", "sh"]:
256
+ dbman.shell(get_db_name(args.name))
257
+ elif args.command in ["env", "e"]:
258
+ dbman.env(get_db_name(args.name), args.export, args.clean)
259
+ elif args.command in ["destroy", "del", "rm"]:
260
+ dbman._destroy_database(get_db_name(args.name), keep_zfs=args.keep_zfs_dataset)
261
+ elif args.command == "start":
262
+ dbman.start(get_db_name(args.name))
263
+ elif args.command == "stop":
264
+ dbman.stop(get_db_name(args.name))
265
+ elif args.command == "restart":
266
+ dbman.restart(get_db_name(args.name))
267
+ elif args.command == "cleanup":
268
+ orphaned_mountpoints = dbman.cleanup()
269
+ if args.one_line:
270
+ # Output only mountpoint names on a single line
271
+ names = [mp["name"] for mp in orphaned_mountpoints]
272
+ print(" ".join(names))
273
+ else:
274
+ format_output(orphaned_mountpoints, args.json)
275
+ elif args.command == "clone":
276
+ dbman.clone_database(
277
+ get_db_name(args.source_name), args.snapshot_name, args.new_name, args.port
278
+ )
279
+ elif args.command in ["delete_older_than", "dot"]:
280
+ databases = dbman.delete_older_than(args.days, args.do_it)
281
+ format_output(databases, args.json)
282
+ elif args.command in ["export", "exp"]:
283
+ dbman.export_database(get_db_name(args.name), args.output_file, args.snapshot_name)
284
+ elif args.command in ["import", "imp"]:
285
+ dbman.import_database(args.input_file, args.new_name, args.port, args.compression)
286
+ else:
287
+ parser.print_help()
288
+ sys.exit(1)
289
+ except Exception as e:
290
+ logger.error(f"Error: {e!s}")
291
+ logger.debug(f"Stack trace: {traceback.format_exc()}")
292
+ sys.exit(1)