phlo-postgres 0.2.4__tar.gz → 0.3.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.
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/PKG-INFO +1 -1
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/pyproject.toml +1 -1
- phlo_postgres-0.3.0/src/phlo_postgres/__init__.py +30 -0
- phlo_postgres-0.3.0/src/phlo_postgres/authorization.py +173 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres/cli.py +184 -20
- phlo_postgres-0.3.0/src/phlo_postgres/cli_plugin.py +88 -0
- phlo_postgres-0.3.0/src/phlo_postgres/plugin.py +213 -0
- phlo_postgres-0.3.0/src/phlo_postgres/publish_target.py +85 -0
- phlo_postgres-0.3.0/src/phlo_postgres/resource.py +583 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres/service.yaml +1 -2
- phlo_postgres-0.3.0/src/phlo_postgres/settings.py +152 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres/volume_setup.yaml +1 -1
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres.egg-info/PKG-INFO +1 -1
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres.egg-info/SOURCES.txt +2 -0
- phlo_postgres-0.3.0/tests/test_authorization.py +168 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/tests/test_postgres_cli.py +6 -6
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/tests/test_resource.py +1 -0
- phlo_postgres-0.2.4/src/phlo_postgres/__init__.py +0 -15
- phlo_postgres-0.2.4/src/phlo_postgres/cli_plugin.py +0 -24
- phlo_postgres-0.2.4/src/phlo_postgres/plugin.py +0 -127
- phlo_postgres-0.2.4/src/phlo_postgres/publish_target.py +0 -21
- phlo_postgres-0.2.4/src/phlo_postgres/resource.py +0 -296
- phlo_postgres-0.2.4/src/phlo_postgres/settings.py +0 -52
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/README.md +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/setup.cfg +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres/exporter_service.yaml +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres.egg-info/dependency_links.txt +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres.egg-info/entry_points.txt +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres.egg-info/requires.txt +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/src/phlo_postgres.egg-info/top_level.txt +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/tests/test_integration_postgres.py +0 -0
- {phlo_postgres-0.2.4 → phlo_postgres-0.3.0}/tests/test_postgres_plugin.py +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Phlo PostgreSQL metadata store package.
|
|
2
|
+
|
|
3
|
+
This package provides the PostgreSQL integration for Phlo, including:
|
|
4
|
+
- Service plugin for managing PostgreSQL containers
|
|
5
|
+
- Resource management with connection pooling
|
|
6
|
+
- CLI commands for database operations
|
|
7
|
+
- Configuration settings management
|
|
8
|
+
- Publish targets for serving data
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> from phlo_postgres import PostgresResource, get_settings
|
|
12
|
+
>>> settings = get_settings()
|
|
13
|
+
>>> with PostgresResource() as db:
|
|
14
|
+
... rows = db.query("SELECT * FROM users LIMIT 10")
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
19
|
+
from phlo_postgres.publish_target import PostgresPublishTarget
|
|
20
|
+
from phlo_postgres.resource import PostgresResource
|
|
21
|
+
from phlo_postgres.settings import PostgresSettings, get_settings
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"PostgresPublishTarget",
|
|
25
|
+
"PostgresResource",
|
|
26
|
+
"PostgresServicePlugin",
|
|
27
|
+
"PostgresSettings",
|
|
28
|
+
"get_settings",
|
|
29
|
+
]
|
|
30
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"""CLI regulated surface adapter for PostgreSQL.
|
|
2
|
+
|
|
3
|
+
This module provides the regulated surface adapter for the PostgreSQL CLI,
|
|
4
|
+
declaring mutation commands that require authorization and enforcing
|
|
5
|
+
through core enforcement.
|
|
6
|
+
|
|
7
|
+
Commands:
|
|
8
|
+
- postgres query: Execute SQL (mutation - can be SELECT or DDL/DML)
|
|
9
|
+
- postgres dump: Create database dump (mutation)
|
|
10
|
+
- postgres restore: Restore from dump (mutation)
|
|
11
|
+
- postgres vacuum: Run vacuumdb maintenance (mutation)
|
|
12
|
+
- postgres: Raw psql passthrough (mutation)
|
|
13
|
+
|
|
14
|
+
Resource/Action mapping:
|
|
15
|
+
- dataset.query: SQL execution
|
|
16
|
+
- dataset.manage: Database maintenance operations
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import threading
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
from phlo.cli.authorization import CliPrincipalResolver
|
|
26
|
+
from phlo.logging import get_logger
|
|
27
|
+
from phlo.security.adapters import (
|
|
28
|
+
EnforcementResult,
|
|
29
|
+
SurfaceOperation,
|
|
30
|
+
)
|
|
31
|
+
from phlo.security.enforcement import enforce
|
|
32
|
+
|
|
33
|
+
logger = get_logger(__name__)
|
|
34
|
+
|
|
35
|
+
SURFACE_NAME = "phlo-postgres-cli"
|
|
36
|
+
FRAMEWORK_TYPE = "cli"
|
|
37
|
+
|
|
38
|
+
MUTATION_COMMANDS: frozenset[str] = frozenset(
|
|
39
|
+
{
|
|
40
|
+
"postgres.query",
|
|
41
|
+
"postgres.dump",
|
|
42
|
+
"postgres.restore",
|
|
43
|
+
"postgres.vacuum",
|
|
44
|
+
"postgres",
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
READ_COMMANDS: frozenset[str] = frozenset({})
|
|
49
|
+
|
|
50
|
+
COMMAND_RESOURCE_MAP: dict[str, str] = {
|
|
51
|
+
"postgres.query": "dataset",
|
|
52
|
+
"postgres.dump": "dataset",
|
|
53
|
+
"postgres.restore": "dataset",
|
|
54
|
+
"postgres.vacuum": "dataset",
|
|
55
|
+
"postgres": "dataset",
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
COMMAND_ACTION_MAP: dict[str, str] = {
|
|
59
|
+
"postgres.query": "dataset.query",
|
|
60
|
+
"postgres.dump": "dataset.manage",
|
|
61
|
+
"postgres.restore": "dataset.manage",
|
|
62
|
+
"postgres.vacuum": "dataset.manage",
|
|
63
|
+
"postgres": "dataset.query",
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class PostgresCliSurfaceAdapter:
|
|
68
|
+
"""Regulated surface adapter for PostgreSQL CLI."""
|
|
69
|
+
|
|
70
|
+
_instance: PostgresCliSurfaceAdapter | None = None
|
|
71
|
+
_lock = threading.Lock()
|
|
72
|
+
|
|
73
|
+
def __init__(self) -> None:
|
|
74
|
+
self._resolver = CliPrincipalResolver()
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def get_instance(cls) -> PostgresCliSurfaceAdapter:
|
|
78
|
+
if cls._instance is None:
|
|
79
|
+
with cls._lock:
|
|
80
|
+
if cls._instance is None:
|
|
81
|
+
cls._instance = cls()
|
|
82
|
+
return cls._instance
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def surface_name(self) -> str:
|
|
86
|
+
return SURFACE_NAME
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def framework_type(self) -> str:
|
|
90
|
+
return FRAMEWORK_TYPE
|
|
91
|
+
|
|
92
|
+
def list_operations(self) -> list[SurfaceOperation]:
|
|
93
|
+
operations: list[SurfaceOperation] = []
|
|
94
|
+
for command in MUTATION_COMMANDS:
|
|
95
|
+
resource_type = COMMAND_RESOURCE_MAP.get(command, "dataset")
|
|
96
|
+
action = COMMAND_ACTION_MAP.get(command, "dataset.query")
|
|
97
|
+
operations.append(
|
|
98
|
+
SurfaceOperation(
|
|
99
|
+
action=action,
|
|
100
|
+
resource_type=resource_type,
|
|
101
|
+
operation_name=command,
|
|
102
|
+
resource_id_strategy=None,
|
|
103
|
+
framework_metadata={"command": command},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
return operations
|
|
107
|
+
|
|
108
|
+
def is_active(self, runtime: Any) -> bool:
|
|
109
|
+
return True
|
|
110
|
+
|
|
111
|
+
def install(self, runtime: Any) -> None:
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
def enforce_mutation(self, command: str, resource_id: str | None = None) -> EnforcementResult:
|
|
115
|
+
"""Enforce authorization for a mutation command."""
|
|
116
|
+
if command not in MUTATION_COMMANDS:
|
|
117
|
+
return EnforcementResult.allow()
|
|
118
|
+
|
|
119
|
+
action = COMMAND_ACTION_MAP.get(command, "dataset.query")
|
|
120
|
+
resource_type = COMMAND_RESOURCE_MAP.get(command, "dataset")
|
|
121
|
+
resource_id_final = resource_id or f"postgres:{command}"
|
|
122
|
+
|
|
123
|
+
principal = self._resolver.resolve()
|
|
124
|
+
|
|
125
|
+
from phlo.capabilities.interfaces import ResourceRef
|
|
126
|
+
|
|
127
|
+
resource = ResourceRef(
|
|
128
|
+
resource_type=resource_type,
|
|
129
|
+
resource_id=resource_id_final,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
request_id = os.environ.get("PHLO_REQUEST_ID")
|
|
133
|
+
|
|
134
|
+
result = enforce(
|
|
135
|
+
principal=principal,
|
|
136
|
+
action=action,
|
|
137
|
+
resource=resource,
|
|
138
|
+
context=None,
|
|
139
|
+
request_id=request_id,
|
|
140
|
+
surface=SURFACE_NAME,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
logger.debug(
|
|
144
|
+
"postgres_cli_mutation_enforcement_result",
|
|
145
|
+
command=command,
|
|
146
|
+
action=action,
|
|
147
|
+
result=result.variant,
|
|
148
|
+
subject=principal.subject,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return result
|
|
152
|
+
|
|
153
|
+
def check_command_authorization(self, command_path: str) -> EnforcementResult:
|
|
154
|
+
"""Check if a command is authorized to run."""
|
|
155
|
+
if command_path in READ_COMMANDS:
|
|
156
|
+
return EnforcementResult.allow()
|
|
157
|
+
|
|
158
|
+
if command_path in MUTATION_COMMANDS:
|
|
159
|
+
return self.enforce_mutation(command_path)
|
|
160
|
+
|
|
161
|
+
logger.warning(
|
|
162
|
+
"postgres_cli_unknown_command_classification",
|
|
163
|
+
command=command_path,
|
|
164
|
+
)
|
|
165
|
+
return EnforcementResult.deny(
|
|
166
|
+
reason_code="unknown_command",
|
|
167
|
+
explanation=f"Command '{command_path}' is not classified as read or mutation",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_postgres_cli_adapter() -> PostgresCliSurfaceAdapter:
|
|
172
|
+
"""Get the singleton PostgreSQL CLI surface adapter."""
|
|
173
|
+
return PostgresCliSurfaceAdapter.get_instance()
|
|
@@ -1,16 +1,32 @@
|
|
|
1
|
-
"""CLI commands for
|
|
1
|
+
"""CLI commands for PostgreSQL service management.
|
|
2
|
+
|
|
3
|
+
This module provides Click-based CLI commands for interacting with the PostgreSQL
|
|
4
|
+
service, including running queries, dumping/restoring databases, and performing
|
|
5
|
+
maintenance operations like vacuuming.
|
|
6
|
+
|
|
7
|
+
All commands execute within the PostgreSQL container backend container via docker compose exec.
|
|
8
|
+
|
|
9
|
+
Example:
|
|
10
|
+
$ phlo postgres query "SELECT * FROM users LIMIT 10"
|
|
11
|
+
$ phlo postgres dump --file backup.sql.gz
|
|
12
|
+
$ phlo postgres restore --file backup.sql.gz
|
|
13
|
+
$ phlo postgres vacuum --analyze
|
|
14
|
+
|
|
15
|
+
"""
|
|
2
16
|
|
|
3
17
|
from __future__ import annotations
|
|
4
18
|
|
|
5
19
|
import gzip
|
|
6
20
|
import subprocess
|
|
7
21
|
from pathlib import Path
|
|
8
|
-
from shutil import which
|
|
9
22
|
from subprocess import TimeoutExpired
|
|
10
23
|
|
|
11
24
|
import click
|
|
12
25
|
|
|
13
|
-
from phlo.cli.commands.services.utils import
|
|
26
|
+
from phlo.cli.commands.services.utils import (
|
|
27
|
+
ensure_phlo_dir,
|
|
28
|
+
require_container_backend as _require_selected_container_backend,
|
|
29
|
+
)
|
|
14
30
|
from phlo.cli.infrastructure.command import CommandError, run_command
|
|
15
31
|
from phlo.cli.infrastructure.compose import compose_base_cmd
|
|
16
32
|
from phlo.cli.infrastructure.utils import get_project_name
|
|
@@ -18,7 +34,27 @@ from phlo_postgres.settings import get_settings
|
|
|
18
34
|
|
|
19
35
|
|
|
20
36
|
def _read_sql(*, query: str | None, file: Path | None) -> str:
|
|
21
|
-
"""
|
|
37
|
+
"""Read SQL from inline query string or file path.
|
|
38
|
+
|
|
39
|
+
Validates that exactly one of query or file is provided and returns the
|
|
40
|
+
SQL content. Handles empty file detection and encoding issues.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
query: Inline SQL query string.
|
|
44
|
+
file: Path to SQL file to read.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
str: The SQL content to execute.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
click.ClickException: If both query and file are provided, neither is
|
|
51
|
+
provided, or the file is empty/cannot be read.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> sql = _read_sql(query="SELECT 1")
|
|
55
|
+
>>> sql = _read_sql(file=Path("query.sql"))
|
|
56
|
+
|
|
57
|
+
"""
|
|
22
58
|
if query and file:
|
|
23
59
|
raise click.ClickException("Use either an inline query or --file, not both.")
|
|
24
60
|
if file is not None:
|
|
@@ -34,14 +70,29 @@ def _read_sql(*, query: str | None, file: Path | None) -> str:
|
|
|
34
70
|
raise click.ClickException("Provide a SQL query argument or --file.")
|
|
35
71
|
|
|
36
72
|
|
|
37
|
-
def
|
|
38
|
-
"""Validate that
|
|
39
|
-
|
|
40
|
-
raise click.ClickException("docker command not found.")
|
|
73
|
+
def _require_container_backend() -> None:
|
|
74
|
+
"""Validate that the selected container backend is available."""
|
|
75
|
+
_require_selected_container_backend()
|
|
41
76
|
|
|
42
77
|
|
|
43
78
|
def _postgres_exec_base(*, tty: bool) -> list[str]:
|
|
44
|
-
"""Build the docker compose exec command for
|
|
79
|
+
"""Build the docker compose exec base command for PostgreSQL container.
|
|
80
|
+
|
|
81
|
+
Constructs the initial portion of the docker compose exec command including
|
|
82
|
+
project name and service name. Used as a base for all container operations.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
tty: Whether to allocate a TTY (-t flag). Disable for non-interactive
|
|
86
|
+
commands that capture output.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
list[str]: Base command as a list of strings ready for subprocess.
|
|
90
|
+
|
|
91
|
+
Example:
|
|
92
|
+
>>> cmd = _postgres_exec_base(tty=True)
|
|
93
|
+
>>> # Returns: ['docker', 'compose', '-p', 'phlo', '-f', '...', 'exec', '-t', 'postgres']
|
|
94
|
+
|
|
95
|
+
"""
|
|
45
96
|
phlo_dir = ensure_phlo_dir()
|
|
46
97
|
project_name = get_project_name()
|
|
47
98
|
cmd = compose_base_cmd(phlo_dir=phlo_dir, project_name=project_name)
|
|
@@ -53,7 +104,26 @@ def _postgres_exec_base(*, tty: bool) -> list[str]:
|
|
|
53
104
|
|
|
54
105
|
|
|
55
106
|
def _postgres_identity(*, user: str | None, database: str | None) -> tuple[str, str]:
|
|
56
|
-
"""Resolve
|
|
107
|
+
"""Resolve PostgreSQL connection identity (user and database).
|
|
108
|
+
|
|
109
|
+
Returns explicit values if provided, otherwise falls back to settings
|
|
110
|
+
defaults. This allows CLI commands to use configured defaults while
|
|
111
|
+
permitting overrides.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
user: Database username override, or None to use settings default.
|
|
115
|
+
database: Database name override, or None to use settings default.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
tuple[str, str]: Tuple of (resolved_user, resolved_database).
|
|
119
|
+
|
|
120
|
+
Example:
|
|
121
|
+
>>> user, db = _postgres_identity(user=None, database=None)
|
|
122
|
+
>>> # Uses settings.postgres_user and settings.postgres_db
|
|
123
|
+
>>> user, db = _postgres_identity(user="admin", database=None)
|
|
124
|
+
>>> # Uses "admin" for user, settings default for database
|
|
125
|
+
|
|
126
|
+
"""
|
|
57
127
|
settings = get_settings()
|
|
58
128
|
return user or settings.postgres_user, database or settings.postgres_db
|
|
59
129
|
|
|
@@ -65,7 +135,24 @@ def _postgres_identity(*, user: str | None, database: str | None) -> tuple[str,
|
|
|
65
135
|
@click.argument("postgres_args", nargs=-1, type=click.UNPROCESSED)
|
|
66
136
|
@click.pass_context
|
|
67
137
|
def postgres_group(ctx: click.Context, postgres_args: tuple[str, ...]) -> None:
|
|
68
|
-
"""Run psql or
|
|
138
|
+
"""Run psql or PostgreSQL helper commands against the project database.
|
|
139
|
+
|
|
140
|
+
This is the main entry point for PostgreSQL CLI operations. It supports:
|
|
141
|
+
- Interactive psql sessions (default if no subcommand)
|
|
142
|
+
- Subcommands: query, dump, restore, vacuum
|
|
143
|
+
- Direct psql arguments passthrough
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
ctx: Click context object.
|
|
147
|
+
postgres_args: Additional arguments passed to psql or subcommands.
|
|
148
|
+
|
|
149
|
+
Example:
|
|
150
|
+
$ phlo postgres # Interactive psql
|
|
151
|
+
$ phlo postgres -c "SELECT 1" # One-off query via psql
|
|
152
|
+
$ phlo postgres query "SELECT * FROM users"
|
|
153
|
+
$ phlo postgres dump --file backup.sql.gz
|
|
154
|
+
|
|
155
|
+
"""
|
|
69
156
|
if postgres_args and postgres_args[0] == "query":
|
|
70
157
|
postgres_query.main(
|
|
71
158
|
args=list(postgres_args[1:]),
|
|
@@ -95,7 +182,7 @@ def postgres_group(ctx: click.Context, postgres_args: tuple[str, ...]) -> None:
|
|
|
95
182
|
)
|
|
96
183
|
return
|
|
97
184
|
|
|
98
|
-
|
|
185
|
+
_require_container_backend()
|
|
99
186
|
user, database = _postgres_identity(user=None, database=None)
|
|
100
187
|
cmd = _postgres_exec_base(tty=True)
|
|
101
188
|
cmd.extend(["psql", "-U", user, "-d", database])
|
|
@@ -122,8 +209,27 @@ def postgres_query(
|
|
|
122
209
|
database: str | None,
|
|
123
210
|
timeout_seconds: int,
|
|
124
211
|
) -> None:
|
|
125
|
-
"""Execute a SQL query against the running PostgreSQL service.
|
|
126
|
-
|
|
212
|
+
"""Execute a SQL query against the running PostgreSQL service.
|
|
213
|
+
|
|
214
|
+
Executes a SQL query inside the PostgreSQL container and prints results
|
|
215
|
+
to stdout. Supports inline queries or reading from a file.
|
|
216
|
+
|
|
217
|
+
Args:
|
|
218
|
+
query: SQL query string to execute.
|
|
219
|
+
query_file: Path to file containing SQL query.
|
|
220
|
+
user: Database user (default from settings).
|
|
221
|
+
database: Database name (default from settings).
|
|
222
|
+
timeout_seconds: Maximum time to wait for query completion.
|
|
223
|
+
|
|
224
|
+
Raises:
|
|
225
|
+
click.ClickException: If the query fails or times out.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
$ phlo postgres query "SELECT * FROM users"
|
|
229
|
+
$ phlo postgres query --file query.sql --timeout 60
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
_require_container_backend()
|
|
127
233
|
sql = _read_sql(query=query, file=query_file)
|
|
128
234
|
resolved_user, resolved_db = _postgres_identity(user=user, database=database)
|
|
129
235
|
cmd = _postgres_exec_base(tty=False)
|
|
@@ -163,8 +269,26 @@ def postgres_dump(
|
|
|
163
269
|
database: str | None,
|
|
164
270
|
timeout_seconds: int,
|
|
165
271
|
) -> None:
|
|
166
|
-
"""
|
|
167
|
-
|
|
272
|
+
"""Create a PostgreSQL logical backup (pg_dump) to a local file.
|
|
273
|
+
|
|
274
|
+
Dumps the entire database using pg_dump, with optional gzip compression
|
|
275
|
+
if the output file has a .gz extension.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
output_file: Path to write the dump. Use .gz extension for compression.
|
|
279
|
+
user: Database user (default from settings).
|
|
280
|
+
database: Database name (default from settings).
|
|
281
|
+
timeout_seconds: Maximum time to wait for dump completion.
|
|
282
|
+
|
|
283
|
+
Raises:
|
|
284
|
+
click.ClickException: If the dump fails or times out.
|
|
285
|
+
|
|
286
|
+
Example:
|
|
287
|
+
$ phlo postgres dump --file backup.sql
|
|
288
|
+
$ phlo postgres dump --file backup.sql.gz --timeout 300
|
|
289
|
+
|
|
290
|
+
"""
|
|
291
|
+
_require_container_backend()
|
|
168
292
|
resolved_user, resolved_db = _postgres_identity(user=user, database=database)
|
|
169
293
|
cmd = _postgres_exec_base(tty=False)
|
|
170
294
|
cmd.extend(["pg_dump", "-U", resolved_user, resolved_db])
|
|
@@ -212,8 +336,29 @@ def postgres_restore(
|
|
|
212
336
|
database: str | None,
|
|
213
337
|
timeout_seconds: int,
|
|
214
338
|
) -> None:
|
|
215
|
-
"""Restore a PostgreSQL
|
|
216
|
-
|
|
339
|
+
"""Restore a PostgreSQL database from a logical backup file.
|
|
340
|
+
|
|
341
|
+
Restores the database from a SQL dump file (plain or gzip-compressed).
|
|
342
|
+
Uses psql internally to execute the dump SQL.
|
|
343
|
+
|
|
344
|
+
Warning:
|
|
345
|
+
This may overwrite existing data. Use with caution on production databases.
|
|
346
|
+
|
|
347
|
+
Args:
|
|
348
|
+
input_file: Path to the dump file (.sql or .sql.gz).
|
|
349
|
+
user: Database user (default from settings).
|
|
350
|
+
database: Database name (default from settings).
|
|
351
|
+
timeout_seconds: Maximum time to wait for restore completion.
|
|
352
|
+
|
|
353
|
+
Raises:
|
|
354
|
+
click.ClickException: If the restore fails or times out.
|
|
355
|
+
|
|
356
|
+
Example:
|
|
357
|
+
$ phlo postgres restore --file backup.sql
|
|
358
|
+
$ phlo postgres restore --file backup.sql.gz --db mydb --timeout 600
|
|
359
|
+
|
|
360
|
+
"""
|
|
361
|
+
_require_container_backend()
|
|
217
362
|
resolved_user, resolved_db = _postgres_identity(user=user, database=database)
|
|
218
363
|
cmd = _postgres_exec_base(tty=False)
|
|
219
364
|
cmd.extend(["psql", "-U", resolved_user, "-d", resolved_db, "-v", "ON_ERROR_STOP=1"])
|
|
@@ -257,8 +402,27 @@ def postgres_vacuum(
|
|
|
257
402
|
analyze: bool,
|
|
258
403
|
timeout_seconds: int,
|
|
259
404
|
) -> None:
|
|
260
|
-
"""Run vacuumdb inside the
|
|
261
|
-
|
|
405
|
+
"""Run vacuumdb for PostgreSQL maintenance inside the container.
|
|
406
|
+
|
|
407
|
+
Executes vacuumdb to reclaim storage and optionally update statistics.
|
|
408
|
+
This is useful for routine database maintenance after large operations.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
user: Database user (default from settings).
|
|
412
|
+
database: Database name (default from settings).
|
|
413
|
+
analyze: Whether to run ANALYZE after vacuum (updates statistics).
|
|
414
|
+
timeout_seconds: Maximum time to wait for vacuum completion.
|
|
415
|
+
|
|
416
|
+
Raises:
|
|
417
|
+
click.ClickException: If vacuum fails or times out.
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
$ phlo postgres vacuum
|
|
421
|
+
$ phlo postgres vacuum --no-analyze
|
|
422
|
+
$ phlo postgres vacuum --db analytics --timeout 300
|
|
423
|
+
|
|
424
|
+
"""
|
|
425
|
+
_require_container_backend()
|
|
262
426
|
resolved_user, resolved_db = _postgres_identity(user=user, database=database)
|
|
263
427
|
cmd = _postgres_exec_base(tty=False)
|
|
264
428
|
cmd.extend(["vacuumdb", "-U", resolved_user])
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""CLI plugin for PostgreSQL commands.
|
|
2
|
+
|
|
3
|
+
This module provides the CLI plugin implementation that registers PostgreSQL
|
|
4
|
+
commands with the phlo CLI system. It exposes the postgres command group and
|
|
5
|
+
its subcommands (query, dump, restore, vacuum) to the main phlo CLI.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from phlo_postgres.cli_plugin import PostgresCliPlugin
|
|
9
|
+
>>> plugin = PostgresCliPlugin()
|
|
10
|
+
>>> commands = plugin.get_cli_commands()
|
|
11
|
+
>>> print(commands[0].name)
|
|
12
|
+
postgres
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import click
|
|
19
|
+
|
|
20
|
+
from phlo.plugins.base import CliCommandPlugin, PluginMetadata
|
|
21
|
+
|
|
22
|
+
from phlo_postgres.cli import postgres_group
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class PostgresCliPlugin(CliCommandPlugin):
|
|
26
|
+
"""CLI plugin that registers PostgreSQL commands with the phlo CLI.
|
|
27
|
+
|
|
28
|
+
This plugin provides the main entry point for PostgreSQL-related CLI
|
|
29
|
+
commands. It registers the postgres command group which includes
|
|
30
|
+
subcommands for querying, dumping, restoring, and maintaining PostgreSQL
|
|
31
|
+
databases.
|
|
32
|
+
|
|
33
|
+
Attributes:
|
|
34
|
+
None (uses class-level plugin registration).
|
|
35
|
+
|
|
36
|
+
Example:
|
|
37
|
+
>>> plugin = PostgresCliPlugin()
|
|
38
|
+
>>> print(plugin.metadata.name)
|
|
39
|
+
postgres
|
|
40
|
+
>>> commands = plugin.get_cli_commands()
|
|
41
|
+
>>> print([cmd.name for cmd in commands])
|
|
42
|
+
['postgres']
|
|
43
|
+
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def metadata(self) -> PluginMetadata:
|
|
48
|
+
"""Return plugin metadata for the PostgreSQL CLI plugin.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
PluginMetadata: Metadata describing the CLI plugin including name,
|
|
52
|
+
version, and description for plugin discovery.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> plugin = PostgresCliPlugin()
|
|
56
|
+
>>> meta = plugin.metadata
|
|
57
|
+
>>> print(f"{meta.name} v{meta.version}")
|
|
58
|
+
postgres v0.1.0
|
|
59
|
+
>>> print(meta.description)
|
|
60
|
+
CLI commands for PostgreSQL service access
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
return PluginMetadata(
|
|
64
|
+
name="postgres",
|
|
65
|
+
version="0.1.0",
|
|
66
|
+
description="CLI commands for PostgreSQL service access",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def get_cli_commands(self) -> list[click.Command]:
|
|
70
|
+
"""Return CLI commands provided by this plugin.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
list[click.Command]: List of Click command objects to register
|
|
74
|
+
with the main phlo CLI. Currently provides the postgres
|
|
75
|
+
command group which includes query, dump, restore, and
|
|
76
|
+
vacuum subcommands.
|
|
77
|
+
|
|
78
|
+
Example:
|
|
79
|
+
>>> plugin = PostgresCliPlugin()
|
|
80
|
+
>>> commands = plugin.get_cli_commands()
|
|
81
|
+
>>> cmd = commands[0]
|
|
82
|
+
>>> print(cmd.name)
|
|
83
|
+
postgres
|
|
84
|
+
>>> print([c.name for c in cmd.commands.values()])
|
|
85
|
+
['query', 'dump', 'restore', 'vacuum']
|
|
86
|
+
|
|
87
|
+
"""
|
|
88
|
+
return [postgres_group]
|