tsdb-sql-tool 0.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.
sql_tool/__about__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
sql_tool/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """SQL Tool - PostgreSQL query and administration tool."""
2
+
3
+ from sql_tool.__about__ import __version__
4
+
5
+ __all__ = ["__version__"]
sql_tool/__main__.py ADDED
@@ -0,0 +1,6 @@
1
+ """Module entry point for python -m sql_tool."""
2
+
3
+ from sql_tool.cli.main import app
4
+
5
+ if __name__ == "__main__":
6
+ app()
File without changes
File without changes
@@ -0,0 +1,125 @@
1
+ """Shared CLI plumbing for command modules.
2
+
3
+ Client creation, format-option handling, and output helpers.
4
+ Distinct from cli.helpers which contains pure data-formatting functions.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from typing import TYPE_CHECKING, Any
11
+
12
+ from sql_tool.cli.helpers import fmt_size
13
+ from sql_tool.cli.output import get_formatter, resolve_format, write_output
14
+ from sql_tool.core.client import PgClient
15
+ from sql_tool.core.config import load_config, resolve_config
16
+
17
+ if TYPE_CHECKING:
18
+ import typer
19
+
20
+ from sql_tool.cli.output import OutputFormat
21
+ from sql_tool.core.models import QueryResult
22
+
23
+
24
+ def get_client(ctx: typer.Context, timeout: float | None = None) -> PgClient:
25
+ obj = ctx.ensure_object(dict)
26
+ config = load_config(obj.get("config_file"))
27
+
28
+ cli_overrides: dict[str, Any] = {}
29
+ for key in ("host", "port", "database", "user", "password", "schema"):
30
+ val = obj.get(key)
31
+ if val is not None:
32
+ cli_overrides[key] = val
33
+ if timeout is not None:
34
+ cli_overrides["timeout"] = timeout
35
+
36
+ resolved = resolve_config(
37
+ config,
38
+ profile_name=obj.get("profile"),
39
+ dsn=obj.get("dsn"),
40
+ **cli_overrides,
41
+ )
42
+
43
+ return PgClient(resolved)
44
+
45
+
46
+ def format_options(ctx: typer.Context) -> dict[str, Any]:
47
+ obj = ctx.ensure_object(dict)
48
+ return {
49
+ "format_flag": obj.get("format"),
50
+ "compact": obj.get("compact", False),
51
+ "width": obj.get("width", 40),
52
+ "no_header": obj.get("no_header", False),
53
+ }
54
+
55
+
56
+ def output_result(ctx: typer.Context, result: QueryResult) -> None:
57
+ opts = format_options(ctx)
58
+ formatter = get_formatter(**opts)
59
+ write_output(formatter, result)
60
+
61
+
62
+ def apply_local_format_options(
63
+ ctx: typer.Context,
64
+ *,
65
+ format: OutputFormat | None = None,
66
+ table: bool = False,
67
+ compact: bool = False,
68
+ width: int | None = None,
69
+ no_header: bool = False,
70
+ ) -> None:
71
+ if format is not None or table or compact or width is not None or no_header:
72
+ obj = ctx.ensure_object(dict)
73
+ if format is not None:
74
+ obj["format"] = format.value
75
+ if table:
76
+ obj["format"] = "table"
77
+ if compact:
78
+ obj["compact"] = compact
79
+ if width is not None:
80
+ obj["width"] = width
81
+ if no_header:
82
+ obj["no_header"] = no_header
83
+
84
+
85
+ def is_table_format(ctx: typer.Context) -> bool:
86
+ obj = ctx.ensure_object(dict)
87
+ return resolve_format(obj.get("format")) == "table"
88
+
89
+
90
+ def size_formatter(ctx: typer.Context) -> tuple[Any, bool]:
91
+ is_tbl = is_table_format(ctx)
92
+ fmt = fmt_size if is_tbl else (lambda b: str(b or 0))
93
+ return fmt, is_tbl
94
+
95
+
96
+ def parse_table_arg(table_arg: str) -> tuple[str, str]:
97
+ if "." in table_arg:
98
+ schema, table = table_arg.split(".", 1)
99
+ return schema, table
100
+ return "public", table_arg
101
+
102
+
103
+ def preprocess_optional_int_flags() -> None:
104
+ """Insert default '10' after bare --head/--tail/--sample when used without a value.
105
+
106
+ Typer 0.23 doesn't support Click's flag_value parameter, so these options
107
+ always require an explicit argument. This preprocessor lets users write
108
+ ``--head`` instead of ``--head 10`` by inserting the default before Typer
109
+ parses argv.
110
+ """
111
+ if "table" not in sys.argv:
112
+ return
113
+ flags = {"--head", "--tail", "--sample"}
114
+ new_argv: list[str] = []
115
+ i = 0
116
+ while i < len(sys.argv):
117
+ new_argv.append(sys.argv[i])
118
+ if sys.argv[i] in flags:
119
+ next_is_value = i + 1 < len(sys.argv) and not sys.argv[i + 1].startswith(
120
+ "-"
121
+ )
122
+ if not next_is_value:
123
+ new_argv.append("10")
124
+ i += 1
125
+ sys.argv = new_argv
@@ -0,0 +1,118 @@
1
+ """Configuration management CLI commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import TYPE_CHECKING
6
+
7
+ import typer
8
+
9
+ from sql_tool.core.config import DEFAULT_CONFIG_PATH, load_config, resolve_config
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+ from sql_tool.core.config import ResolvedConfig
15
+
16
+ config_app = typer.Typer(help="Configuration management commands")
17
+
18
+
19
+ @config_app.callback(invoke_without_command=True)
20
+ def config_callback(ctx: typer.Context) -> None:
21
+ if not ctx.invoked_subcommand:
22
+ typer.echo(ctx.get_help())
23
+ raise typer.Exit()
24
+
25
+
26
+ def _get_resolved_config(ctx: typer.Context) -> tuple[ResolvedConfig, Path | None]:
27
+ config_path: Path | None = ctx.obj.get("config_file")
28
+ app_config = load_config(config_path)
29
+ resolved = resolve_config(
30
+ app_config,
31
+ profile_name=ctx.obj.get("profile"),
32
+ dsn=ctx.obj.get("dsn"),
33
+ host=ctx.obj.get("host"),
34
+ port=ctx.obj.get("port"),
35
+ database=ctx.obj.get("database"),
36
+ user=ctx.obj.get("user"),
37
+ password=ctx.obj.get("password"),
38
+ )
39
+ return resolved, config_path
40
+
41
+
42
+ def _mask_password(value: str | None) -> str:
43
+ if value is None:
44
+ return "not set"
45
+ return "***"
46
+
47
+
48
+ @config_app.command("show")
49
+ def config_show(ctx: typer.Context) -> None:
50
+ """Display resolved configuration with source attribution."""
51
+ resolved, config_path = _get_resolved_config(ctx)
52
+ sources = resolved.sources
53
+
54
+ typer.echo("Connection Settings (resolved):")
55
+ connection_fields = [
56
+ ("host", resolved.host),
57
+ ("port", str(resolved.port)),
58
+ ("database", resolved.dbname),
59
+ ("user", resolved.user or "not set"),
60
+ ("password", _mask_password(resolved.password)),
61
+ ("sslmode", resolved.sslmode),
62
+ ]
63
+ for field_name, value in connection_fields:
64
+ source_key = "dbname" if field_name == "database" else field_name
65
+ source = sources.get(source_key, "default")
66
+ typer.echo(f" {field_name}: {value} ({source})")
67
+
68
+ typer.echo("")
69
+ typer.echo("General:")
70
+ timeout_source = sources.get("default_timeout", "default")
71
+ typer.echo(f" timeout: {resolved.default_timeout}s ({timeout_source})")
72
+ format_source = sources.get("default_format", "default")
73
+ typer.echo(f" format: {resolved.default_format} ({format_source})")
74
+
75
+ typer.echo("")
76
+ if resolved.active_profile:
77
+ typer.echo(f"Active Profile: {resolved.active_profile}")
78
+ else:
79
+ typer.echo("Active Profile: none")
80
+
81
+ display_path = config_path or DEFAULT_CONFIG_PATH
82
+ typer.echo(f"Config File: {display_path}")
83
+
84
+
85
+ @config_app.command("profiles")
86
+ def config_profiles(ctx: typer.Context) -> None:
87
+ """List available connection profiles."""
88
+ config_path: Path | None = ctx.obj.get("config_file")
89
+ app_config = load_config(config_path)
90
+ active_profile = ctx.obj.get("profile") or None
91
+
92
+ if not app_config.profiles:
93
+ typer.echo("No profiles configured.")
94
+ display_path = config_path or DEFAULT_CONFIG_PATH
95
+ typer.echo(f"Add profiles to: {display_path}")
96
+ return
97
+
98
+ typer.echo("Available Profiles:")
99
+ typer.echo("")
100
+ for name, profile in sorted(app_config.profiles.items()):
101
+ is_active = name == active_profile
102
+ marker = "* " if is_active else " "
103
+ label = " (active)" if is_active else ""
104
+ typer.echo(f"{marker}{name}{label}")
105
+
106
+ display_fields = [
107
+ ("host", profile.host),
108
+ ("port", str(profile.port)),
109
+ ("database", profile.dbname),
110
+ ]
111
+ if profile.user:
112
+ display_fields.append(("user", profile.user))
113
+ if profile.sslmode != "prefer":
114
+ display_fields.append(("sslmode", profile.sslmode))
115
+
116
+ for field_name, value in display_fields:
117
+ typer.echo(f" {field_name}: {value}")
118
+ typer.echo("")
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated
4
+
5
+ import structlog
6
+ import typer
7
+
8
+ from sql_tool.cli.commands._shared import get_client, output_result
9
+ from sql_tool.core.query_source import resolve_query_source
10
+
11
+ log = structlog.get_logger()
12
+
13
+
14
+ def query_command(
15
+ ctx: typer.Context,
16
+ file: Annotated[
17
+ str | None,
18
+ typer.Argument(help="SQL file to execute"),
19
+ ] = None,
20
+ execute: Annotated[
21
+ str | None,
22
+ typer.Option("--execute", "-e", help="Execute inline SQL query"),
23
+ ] = None,
24
+ timeout: Annotated[
25
+ float | None,
26
+ typer.Option("--timeout", "-t", help="Query timeout in seconds"),
27
+ ] = None,
28
+ ) -> None:
29
+ """Execute a SQL query from file, inline (-e), or stdin."""
30
+ sql = resolve_query_source(inline=execute, file_path=file)
31
+
32
+ with get_client(ctx, timeout=timeout) as client:
33
+ result = client.execute_query(sql)
34
+
35
+ output_result(ctx, result)
@@ -0,0 +1,94 @@
1
+ """Service and maintenance CLI commands.
2
+
3
+ Thin CLI layer: typer decorators, argument parsing, output formatting.
4
+ Business logic delegated to core.postgres module.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Annotated
10
+
11
+ import typer
12
+
13
+ from sql_tool.cli.commands._shared import get_client, output_result
14
+ from sql_tool.core.postgres import (
15
+ check_server,
16
+ kill_backend,
17
+ list_user_tables,
18
+ vacuum_tables,
19
+ )
20
+
21
+ service_app = typer.Typer(help="Service and maintenance commands")
22
+
23
+
24
+ @service_app.callback(invoke_without_command=True)
25
+ def service_callback(
26
+ ctx: typer.Context,
27
+ ) -> None:
28
+ if not ctx.invoked_subcommand:
29
+ typer.echo(ctx.get_help())
30
+ raise typer.Exit()
31
+
32
+
33
+ @service_app.command("vacuum")
34
+ def vacuum_command(
35
+ ctx: typer.Context,
36
+ table_arg: Annotated[
37
+ str | None,
38
+ typer.Argument(help="Table name to vacuum"),
39
+ ] = None,
40
+ full: Annotated[
41
+ bool,
42
+ typer.Option("--full", help="Run VACUUM FULL (locks table, rewrites)"),
43
+ ] = False,
44
+ all_tables: Annotated[
45
+ bool,
46
+ typer.Option("--all", help="Vacuum all user tables"),
47
+ ] = False,
48
+ ) -> None:
49
+ """VACUUM marks dead rows as reusable. VACUUM FULL rewrites tables but requires exclusive lock."""
50
+ if not table_arg and not all_tables:
51
+ typer.echo("Error: Must specify table name or --all", err=True)
52
+ raise typer.Exit(2)
53
+
54
+ with get_client(ctx) as client:
55
+ table_names = list_user_tables(client) if all_tables else [table_arg] # type: ignore[list-item]
56
+
57
+ count = vacuum_tables(client, table_names, full=full)
58
+
59
+ typer.echo(f"Vacuumed {count} table(s)")
60
+
61
+
62
+ @service_app.command("kill")
63
+ def kill_command(
64
+ ctx: typer.Context,
65
+ pid: Annotated[int, typer.Argument(help="Backend process ID to terminate")],
66
+ cancel: Annotated[
67
+ bool,
68
+ typer.Option("--cancel", help="Cancel query only (don't kill connection)"),
69
+ ] = False,
70
+ ) -> None:
71
+ """pg_terminate_backend() kills the connection. pg_cancel_backend() cancels query but keeps connection alive."""
72
+ with get_client(ctx) as client:
73
+ success = kill_backend(client, pid, cancel=cancel)
74
+
75
+ action = "Cancelled" if cancel else "Terminated"
76
+ if success:
77
+ typer.echo(f"{action} backend {pid}")
78
+ else:
79
+ typer.echo(f"Error: Backend {pid} not found or already terminated", err=True)
80
+ raise typer.Exit(1)
81
+
82
+
83
+ @service_app.command("check")
84
+ def check_command(ctx: typer.Context) -> None:
85
+ """
86
+ Check PostgreSQL server connectivity and version.
87
+
88
+ Reports server version, current database, connected user, and uptime.
89
+ Useful for verifying connection parameters and server availability.
90
+ """
91
+ with get_client(ctx) as client:
92
+ result = check_server(client)
93
+
94
+ output_result(ctx, result)