flshbd 0.0.9__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 (44) hide show
  1. flshbd/__about__.py +4 -0
  2. flshbd/__init__.py +10 -0
  3. flshbd/__main__.py +9 -0
  4. flshbd/cli/__init__.py +66 -0
  5. flshbd/cli/access.py +153 -0
  6. flshbd/cli/dash.py +192 -0
  7. flshbd/cli/db.py +195 -0
  8. flshbd/cli/groups.py +225 -0
  9. flshbd/cli/process.py +332 -0
  10. flshbd/cli/site_groups.py +225 -0
  11. flshbd/cli/sites.py +325 -0
  12. flshbd/cli/users.py +317 -0
  13. flshbd/cli/utils.py +71 -0
  14. flshbd/core/.env +58 -0
  15. flshbd/core/__init__.py +8 -0
  16. flshbd/core/config.py +331 -0
  17. flshbd/core/env.py +211 -0
  18. flshbd/core/security.py +108 -0
  19. flshbd/data/__init__.py +8 -0
  20. flshbd/data/database.py +366 -0
  21. flshbd/data/models.py +1250 -0
  22. flshbd/web/__init__.py +8 -0
  23. flshbd/web/app.py +101 -0
  24. flshbd/web/auth.py +188 -0
  25. flshbd/web/auth_pam.py +62 -0
  26. flshbd/web/routes.py +208 -0
  27. flshbd/web/static/css/style.css +636 -0
  28. flshbd/web/static/images/FlashBoard-Logo.png +0 -0
  29. flshbd/web/templates/base.html +14 -0
  30. flshbd/web/templates/dash.html +20 -0
  31. flshbd/web/templates/login.html +56 -0
  32. flshbd/web/templates/partials/demo_site.html +33 -0
  33. flshbd/web/templates/partials/footer.html +3 -0
  34. flshbd/web/templates/partials/header.html +23 -0
  35. flshbd/web/templates/partials/help_site.html +46 -0
  36. flshbd/web/templates/partials/info_site.html +37 -0
  37. flshbd/web/templates/partials/main.html +25 -0
  38. flshbd/web/templates/partials/sidebar.html +23 -0
  39. flshbd/web/templates/partials/template_site.html +26 -0
  40. flshbd-0.0.9.dist-info/METADATA +106 -0
  41. flshbd-0.0.9.dist-info/RECORD +44 -0
  42. flshbd-0.0.9.dist-info/WHEEL +4 -0
  43. flshbd-0.0.9.dist-info/entry_points.txt +2 -0
  44. flshbd-0.0.9.dist-info/licenses/LICENSE.txt +10 -0
flshbd/__about__.py ADDED
@@ -0,0 +1,4 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+ __version__ = "0.0.9"
flshbd/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+
5
+ from flshbd.core.env import bootstrap
6
+
7
+ # Load the environment's .env file (if any) before any flshbd submodule is
8
+ # imported -- in particular before flshbd.core.config, whose FLSHBD_* values
9
+ # are read from the environment at import time. No-op under pytest.
10
+ bootstrap()
flshbd/__main__.py ADDED
@@ -0,0 +1,9 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+ import sys
5
+
6
+ if __name__ == "__main__":
7
+ from flshbd.cli import fld
8
+
9
+ sys.exit(fld())
flshbd/cli/__init__.py ADDED
@@ -0,0 +1,66 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+ import click
5
+
6
+ from flshbd.__about__ import __version__
7
+ from flshbd.cli.access import get_access_commands
8
+ from flshbd.cli.dash import get_dash_commands
9
+ from flshbd.cli.db import get_database_commands
10
+ from flshbd.cli.groups import get_group_commands
11
+ from flshbd.cli.site_groups import get_site_group_commands
12
+ from flshbd.cli.sites import get_site_commands
13
+ from flshbd.cli.users import get_user_commands
14
+
15
+
16
+ @click.group(
17
+ context_settings={"help_option_names": ["-h", "--help"]},
18
+ invoke_without_command=True,
19
+ )
20
+ @click.option(
21
+ "--env",
22
+ "-e",
23
+ "env",
24
+ default="default",
25
+ type=click.Choice(["development", "production", "testing", "default"]),
26
+ help="Environment to use (development, production, testing, default)",
27
+ )
28
+ @click.version_option(version=__version__, prog_name="fld")
29
+ @click.pass_context
30
+ def fld(ctx, env: str):
31
+ """FlashBoard CLI"""
32
+ # Set the current config for this process
33
+ from flshbd.core.config import set_current_config
34
+
35
+ set_current_config(env)
36
+ # Store environment in context for all subcommands
37
+ ctx.obj = {"env": env}
38
+
39
+
40
+ # Add dash subcommands
41
+ dash = get_dash_commands()
42
+ fld.add_command(dash, "dash")
43
+
44
+ # Add database management commands
45
+ db = get_database_commands()
46
+ fld.add_command(db, "db")
47
+
48
+ # Add user management commands
49
+ user_cmd = get_user_commands()
50
+ fld.add_command(user_cmd, "user")
51
+
52
+ # Add group management commands
53
+ group_cmd = get_group_commands()
54
+ fld.add_command(group_cmd, "group")
55
+
56
+ # Add site management commands
57
+ site_cmd = get_site_commands()
58
+ fld.add_command(site_cmd, "site")
59
+
60
+ # Add site group management commands
61
+ site_group_cmd = get_site_group_commands()
62
+ fld.add_command(site_group_cmd, "site-group")
63
+
64
+ # Add access control commands
65
+ access_cmd = get_access_commands()
66
+ fld.add_command(access_cmd, "access")
flshbd/cli/access.py ADDED
@@ -0,0 +1,153 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+ """
5
+ CLI commands for access control management in FlashBoard
6
+ """
7
+
8
+ import sys
9
+
10
+ import click
11
+
12
+ from flshbd.cli.utils import format_table
13
+ from flshbd.data.database import get_database
14
+
15
+
16
+ def get_db():
17
+ """Get the configured database instance for the current environment."""
18
+ return get_database()
19
+
20
+
21
+ @click.group()
22
+ @click.pass_context
23
+ def access(ctx):
24
+ """Access control management commands."""
25
+ pass
26
+
27
+
28
+ @access.command()
29
+ @click.argument("group_name")
30
+ @click.argument("site_group_name")
31
+ @click.pass_context
32
+ def grant(ctx, group_name: str, site_group_name: str):
33
+ """
34
+ Grant a user group access to a site group.
35
+ """
36
+ db_instance = get_db()
37
+
38
+ with db_instance.get_connection() as conn:
39
+ cursor = conn.cursor()
40
+
41
+ # Get group IDs (try both group_id and name)
42
+ user_group = cursor.execute(
43
+ "SELECT id FROM user_groups WHERE group_id = ? OR name = ?", (group_name, group_name)
44
+ ).fetchone()
45
+
46
+ if not user_group:
47
+ click.echo(f"User group '{group_name}' not found.")
48
+ sys.exit(1)
49
+
50
+ site_group = cursor.execute(
51
+ "SELECT id FROM site_groups WHERE group_id = ? OR name = ?", (site_group_name, site_group_name)
52
+ ).fetchone()
53
+
54
+ if not site_group:
55
+ click.echo(f"Site group '{site_group_name}' not found.")
56
+ sys.exit(1)
57
+
58
+ # Check if access already exists
59
+ existing = cursor.execute(
60
+ """SELECT * FROM group_access
61
+ WHERE group_id = ? AND site_group_id = ?""",
62
+ (user_group["id"], site_group["id"]),
63
+ ).fetchone()
64
+
65
+ if existing:
66
+ click.echo(f"Group '{group_name}' already has access to site group '{site_group_name}'.")
67
+ sys.exit(0)
68
+
69
+ # Grant access
70
+ cursor.execute(
71
+ """INSERT INTO group_access (group_id, site_group_id)
72
+ VALUES (?, ?)""",
73
+ (user_group["id"], site_group["id"]),
74
+ )
75
+
76
+ click.echo(f"Access granted: '{group_name}' can now access '{site_group_name}'.")
77
+
78
+
79
+ @access.command()
80
+ @click.argument("group_name")
81
+ @click.argument("site_group_name")
82
+ @click.pass_context
83
+ def revoke(ctx, group_name: str, site_group_name: str):
84
+ """
85
+ Revoke a user group's access to a site group.
86
+ """
87
+ db_instance = get_db()
88
+
89
+ with db_instance.get_connection() as conn:
90
+ cursor = conn.cursor()
91
+
92
+ # Get group IDs (try both group_id and name)
93
+ user_group = cursor.execute(
94
+ "SELECT id FROM user_groups WHERE group_id = ? OR name = ?", (group_name, group_name)
95
+ ).fetchone()
96
+
97
+ if not user_group:
98
+ click.echo(f"User group '{group_name}' not found.")
99
+ sys.exit(1)
100
+
101
+ site_group = cursor.execute(
102
+ "SELECT id FROM site_groups WHERE group_id = ? OR name = ?", (site_group_name, site_group_name)
103
+ ).fetchone()
104
+
105
+ if not site_group:
106
+ click.echo(f"Site group '{site_group_name}' not found.")
107
+ sys.exit(1)
108
+
109
+ # Revoke access
110
+ cursor.execute(
111
+ """DELETE FROM group_access
112
+ WHERE group_id = ? AND site_group_id = ?""",
113
+ (user_group["id"], site_group["id"]),
114
+ )
115
+
116
+ click.echo(f"Access revoked: '{group_name}' can no longer access '{site_group_name}'.")
117
+
118
+
119
+ @access.command()
120
+ @click.pass_context
121
+ def list(ctx):
122
+ """
123
+ List all access control rules.
124
+ """
125
+ db_instance = get_db()
126
+
127
+ with db_instance.get_connection() as conn:
128
+ cursor = conn.cursor()
129
+
130
+ query = """
131
+ SELECT
132
+ ug.name as user_group_name,
133
+ sg.name as site_group_name
134
+ FROM group_access ga
135
+ JOIN user_groups ug ON ga.group_id = ug.id
136
+ JOIN site_groups sg ON ga.site_group_id = sg.id
137
+ ORDER BY ug.name, sg.name
138
+ """
139
+
140
+ cursor.execute(query)
141
+ rows = [dict(row) for row in cursor.fetchall()]
142
+
143
+ if not rows:
144
+ click.echo("No access control rules found.")
145
+ return
146
+
147
+ headers = ["user_group_name", "site_group_name"]
148
+ click.echo(format_table(rows, headers))
149
+
150
+
151
+ def get_access_commands():
152
+ """Get the access command group for registration."""
153
+ return access
flshbd/cli/dash.py ADDED
@@ -0,0 +1,192 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+ """
5
+ CLI commands for the FlashBoard
6
+ """
7
+
8
+ import sys
9
+
10
+ import click
11
+
12
+ from flshbd.cli.process import ProcessManager
13
+
14
+
15
+ @click.group()
16
+ def dash():
17
+ """Dashboard management commands"""
18
+ pass
19
+
20
+
21
+ @dash.command()
22
+ @click.option("--port", "-p", default=5000, type=int, help="Port to run the server on")
23
+ @click.option("--host", "-H", default="localhost", help="Host to bind the server to")
24
+ @click.option(
25
+ "--server",
26
+ "-s",
27
+ "server_type",
28
+ default="flask",
29
+ type=click.Choice(["flask", "gunicorn"]),
30
+ help="Server type to use (flask or gunicorn)",
31
+ )
32
+ @click.pass_context
33
+ def start(ctx, port: int, host: str, server_type: str):
34
+ """
35
+ Start the FlashBoard server.
36
+
37
+ This command starts the dashboard server with the specified configuration.
38
+ In development mode with Flask, debug is enabled.
39
+
40
+ Examples:
41
+ fld dash start # Start Flask in development mode
42
+ fld --env production dash start # Start Flask in production mode
43
+ fld --env production dash start --server gunicorn # Start with Gunicorn
44
+ fld dash start --port 8080 # Start on port 8080
45
+ """
46
+ # Get current config
47
+ from flshbd.core.config import get_current_config
48
+
49
+ config = get_current_config()
50
+ env = config.ENV
51
+
52
+ # ProcessManager will use the current config set by CLI
53
+ pm = ProcessManager()
54
+
55
+ # Check if server is already running
56
+ if pm.is_running():
57
+ click.echo("Dashboard server is already running.")
58
+ status_info = pm.status()
59
+ if status_info.get("running"):
60
+ click.echo(f"PID: {status_info['pid']}")
61
+ click.echo(f"URL: http://{status_info['host']}:{status_info['port']}")
62
+ return
63
+
64
+ click.echo("Starting FlashBoard server...")
65
+ click.echo(f"Environment: {env}")
66
+ click.echo(f"URL: http://{host}:{port}")
67
+
68
+ try:
69
+ if server_type == "gunicorn":
70
+ if env != "production":
71
+ click.echo("Warning: Gunicorn is recommended for production only", err=True)
72
+ pid, log_file = pm.start_gunicorn(env, port, host)
73
+ click.echo("Server type: Gunicorn")
74
+ else:
75
+ debug = env == "development"
76
+ pid, log_file = pm.start_flask(env, port, host, debug)
77
+ click.echo("Server type: Flask")
78
+ if debug:
79
+ click.echo("Debug mode: enabled (debug pin available in log file)")
80
+
81
+ click.echo(f"Dashboard started with PID: {pid}")
82
+ click.echo(f"Log file: {log_file}")
83
+ click.echo("Server is running in the background.")
84
+ click.echo("Use 'fld dash stop' to stop the server.")
85
+ click.echo("Use 'fld dash status' to check server status.")
86
+
87
+ except FileNotFoundError as e:
88
+ if "gunicorn" in str(e):
89
+ click.echo("Error: Gunicorn is not installed.", err=True)
90
+ click.echo("Install it with: pip install flshbd[gunicorn]", err=True)
91
+ else:
92
+ click.echo(f"Command not found: {e}", err=True)
93
+ sys.exit(1)
94
+ except RuntimeError as e:
95
+ click.echo(f"Error: {e}", err=True)
96
+ sys.exit(1)
97
+ except Exception as e:
98
+ click.echo(f"Error starting server: {e}", err=True)
99
+ sys.exit(1)
100
+
101
+
102
+ @dash.command()
103
+ @click.pass_context
104
+ def stop(ctx):
105
+ """
106
+ Stop the running FlashBoard server.
107
+
108
+ This command stops the server that was started with 'fld dash start'.
109
+ It sends a SIGTERM signal for graceful shutdown.
110
+ """
111
+ # ProcessManager will use the current config set by CLI
112
+ pm = ProcessManager()
113
+
114
+ if not pm.is_running():
115
+ click.echo("No dashboard server is running.")
116
+ return
117
+
118
+ status_info = pm.status()
119
+ click.echo(f"Stopping dashboard server (PID: {status_info['pid']})...")
120
+
121
+ if pm.stop():
122
+ click.echo("Dashboard server stopped.")
123
+ else:
124
+ click.echo("Failed to stop dashboard server.", err=True)
125
+
126
+
127
+ @dash.command()
128
+ @click.pass_context
129
+ def status(ctx):
130
+ """
131
+ Show the status of the FlashBoard server.
132
+
133
+ This command displays information about the currently running server,
134
+ including PID, environment, port, and log file location.
135
+ """
136
+ # ProcessManager will use the current config set by CLI
137
+ pm = ProcessManager()
138
+ status_info = pm.status()
139
+
140
+ if status_info.get("running"):
141
+ click.echo("Dashboard server is RUNNING")
142
+ click.echo(f" PID: {status_info['pid']}")
143
+ click.echo(f" Environment: {status_info['env']}")
144
+ click.echo(f" URL: http://{status_info['host']}:{status_info['port']}")
145
+ click.echo(f" Started: {status_info['start_time']}")
146
+ click.echo(f" Log file: {status_info['log_file']}")
147
+ if status_info.get("debug_pin"):
148
+ click.echo(f" Debug pin: {status_info['debug_pin']}")
149
+ if status_info.get("message"):
150
+ click.echo(f" Note: {status_info['message']}")
151
+ else:
152
+ click.echo("Dashboard server is NOT RUNNING")
153
+ if "message" in status_info:
154
+ click.echo(f" {status_info['message']}")
155
+ if "pid" in status_info:
156
+ click.echo(f" Last PID: {status_info['pid']}")
157
+ if "env" in status_info:
158
+ click.echo(f" Last environment: {status_info['env']}")
159
+ if "port" in status_info:
160
+ click.echo(f" Last port: {status_info['port']}")
161
+ if "log_file" in status_info:
162
+ click.echo(f" Last log file: {status_info['log_file']}")
163
+
164
+
165
+ @dash.command()
166
+ def generate_secret_key():
167
+ """
168
+ Generate a new secret key for configuration.
169
+
170
+ This generates a cryptographically secure secret key that can be used
171
+ as the FLSHBD_SECRET_KEY environment variable. When FLSHBD_SECRET_KEY is set via
172
+ environment variable, ephemeral key generation is disabled and all
173
+ workers will use this persistent key.
174
+
175
+ To use the generated key:
176
+ export FLSHBD_SECRET_KEY=<generated-key>
177
+
178
+ Or add it to your environment configuration file.
179
+ """
180
+ import secrets
181
+
182
+ key = secrets.token_hex(32)
183
+ click.echo(f"FLSHBD_SECRET_KEY={key}")
184
+ click.echo("\nTo use this key, set it as an environment variable:")
185
+ click.echo(f" export FLSHBD_SECRET_KEY={key}")
186
+ click.echo("\nNote: Setting FLSHBD_SECRET_KEY disables ephemeral key generation.")
187
+ click.echo(" All server restarts will use this key until it is changed.")
188
+
189
+
190
+ def get_dash_commands():
191
+ """Get the dash command group for registration"""
192
+ return dash
flshbd/cli/db.py ADDED
@@ -0,0 +1,195 @@
1
+ # SPDX-FileCopyrightText: 2026-present Robert Kormann <rokor@kormann.info>
2
+ #
3
+ # SPDX-License-Identifier: Unlicense
4
+ """
5
+ CLI commands for database management in FlashBoard
6
+
7
+ This module provides CLI commands for database initialization, backup, and restore.
8
+ """
9
+
10
+ import os
11
+ import sys
12
+
13
+ import click
14
+
15
+ from flshbd.core.config import get_current_config
16
+ from flshbd.data.database import Database, get_database
17
+
18
+
19
+ def get_db() -> Database:
20
+ """Get the configured database instance for the current environment."""
21
+ return get_database()
22
+
23
+
24
+ @click.group()
25
+ @click.pass_context
26
+ def db(ctx):
27
+ """Database management commands."""
28
+ pass
29
+
30
+
31
+ @db.command()
32
+ @click.option("--force", is_flag=True, help="Overwrite existing database if it exists")
33
+ @click.pass_context
34
+ def init(ctx, force: bool):
35
+ """
36
+ Initialize the database with schema.
37
+
38
+ Creates all required tables for users, groups, sites, and access control.
39
+ If the database already exists and --force is not specified, the command
40
+ will fail.
41
+ """
42
+
43
+ config = get_current_config()
44
+ db_instance = Database(config.DB_PATH)
45
+
46
+ if os.path.exists(db_instance.db_path) and not force:
47
+ click.echo(f"Database already exists at: {db_instance.db_path}")
48
+ click.echo("Use --force to overwrite the existing database.")
49
+ sys.exit(1)
50
+
51
+ click.echo(f"Initializing database at: {db_instance.db_path}")
52
+ db_instance.initialize(force=force)
53
+ click.echo("Database initialized successfully.")
54
+
55
+
56
+ @db.command()
57
+ @click.pass_context
58
+ def migrate(ctx):
59
+ """
60
+ Seed the database with the static demo data.
61
+
62
+ Populates an empty database with the sample data from the static models
63
+ (users, groups, sites, site groups, access). Does nothing if the database
64
+ already contains users. This is the explicit seeding command for a new
65
+ database; the schema itself is created automatically at server start.
66
+ """
67
+ from flshbd.data.models import seed_static_data
68
+
69
+ db_instance = get_db()
70
+
71
+ # Check if database exists and has tables
72
+ if not db_instance.table_exists("users"):
73
+ click.echo("Database not initialized. Running 'db init' first...")
74
+ db_instance.initialize()
75
+
76
+ seed_static_data()
77
+
78
+ click.echo("Data migration completed successfully.")
79
+
80
+
81
+ @db.command()
82
+ @click.option("--path", "-p", default=None, help="Backup file path. Defaults to timestamped file in same directory.")
83
+ @click.pass_context
84
+ def backup(ctx, path: str | None):
85
+ """
86
+ Create a backup of the current database.
87
+
88
+ Creates a copy of the database file. If no path is specified,
89
+ creates a timestamped backup in the same directory as the database.
90
+ """
91
+ db_instance = get_db()
92
+
93
+ if not os.path.exists(db_instance.db_path):
94
+ click.echo(f"Database file not found: {db_instance.db_path}")
95
+ sys.exit(1)
96
+
97
+ try:
98
+ backup_path = db_instance.backup(path)
99
+ click.echo(f"Backup created at: {backup_path}")
100
+ except Exception as e:
101
+ click.echo(f"Failed to create backup: {e}")
102
+ sys.exit(1)
103
+
104
+
105
+ @db.command()
106
+ @click.argument("backup_path")
107
+ @click.pass_context
108
+ def restore(ctx, backup_path: str):
109
+ """
110
+ Restore database from a backup file.
111
+
112
+ Replaces the current database with the specified backup file.
113
+ """
114
+ db_instance = get_db()
115
+
116
+ backup_path = os.path.expanduser(backup_path)
117
+ if not os.path.exists(backup_path):
118
+ click.echo(f"Backup file not found: {backup_path}")
119
+ sys.exit(1)
120
+
121
+ try:
122
+ click.echo(f"Restoring database from: {backup_path}")
123
+ db_instance.restore(backup_path)
124
+ click.echo("Database restored successfully.")
125
+ except Exception as e:
126
+ click.echo(f"Failed to restore database: {e}")
127
+ sys.exit(1)
128
+
129
+
130
+ @db.group()
131
+ @click.pass_context
132
+ def config(ctx):
133
+ """Manage database configuration key/value pairs."""
134
+ pass
135
+
136
+
137
+ @config.command("get")
138
+ @click.argument("key")
139
+ @click.pass_context
140
+ def config_get(ctx, key: str):
141
+ """
142
+ Get a configuration value by key.
143
+
144
+ Args:
145
+ key: The configuration key to retrieve
146
+ """
147
+ from flshbd.data.models import get_config_value
148
+
149
+ value = get_config_value(key)
150
+ if value is not None:
151
+ click.echo(f"{key} = {value}")
152
+ else:
153
+ click.echo(f"Configuration key '{key}' not found.")
154
+ sys.exit(1)
155
+
156
+
157
+ @config.command("set")
158
+ @click.argument("key")
159
+ @click.argument("value")
160
+ @click.pass_context
161
+ def config_set(ctx, key: str, value: str):
162
+ """
163
+ Set a configuration key/value pair.
164
+
165
+ Args:
166
+ key: The configuration key
167
+ value: The configuration value
168
+ """
169
+ from flshbd.data.models import set_config_value
170
+
171
+ if set_config_value(key, value):
172
+ click.echo(f"Configuration set: {key} = {value}")
173
+ else:
174
+ click.echo(f"Failed to set configuration: {key} = {value}")
175
+ sys.exit(1)
176
+
177
+
178
+ @config.command("list")
179
+ @click.pass_context
180
+ def config_list(ctx):
181
+ """List all configuration entries."""
182
+ from flshbd.data.models import get_all_config_entries
183
+
184
+ entries = get_all_config_entries()
185
+ if entries:
186
+ click.echo("Configuration entries:")
187
+ for entry in entries:
188
+ click.echo(f" {entry.key} = {entry.value}")
189
+ else:
190
+ click.echo("No configuration entries found.")
191
+
192
+
193
+ def get_database_commands():
194
+ """Get the database command group for registration."""
195
+ return db