phlo-postgres 0.2.4__tar.gz → 0.3.1__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.
Files changed (32) hide show
  1. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/PKG-INFO +1 -1
  2. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/pyproject.toml +1 -1
  3. phlo_postgres-0.3.1/src/phlo_postgres/__init__.py +30 -0
  4. phlo_postgres-0.3.1/src/phlo_postgres/authorization.py +173 -0
  5. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres/cli.py +208 -27
  6. phlo_postgres-0.3.1/src/phlo_postgres/cli_plugin.py +88 -0
  7. phlo_postgres-0.3.1/src/phlo_postgres/plugin.py +213 -0
  8. phlo_postgres-0.3.1/src/phlo_postgres/publish_target.py +85 -0
  9. phlo_postgres-0.3.1/src/phlo_postgres/resource.py +583 -0
  10. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres/service.yaml +1 -2
  11. phlo_postgres-0.3.1/src/phlo_postgres/settings.py +152 -0
  12. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres/volume_setup.yaml +1 -1
  13. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres.egg-info/PKG-INFO +1 -1
  14. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres.egg-info/SOURCES.txt +2 -0
  15. phlo_postgres-0.3.1/tests/test_authorization.py +168 -0
  16. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/tests/test_postgres_cli.py +38 -12
  17. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/tests/test_resource.py +1 -0
  18. phlo_postgres-0.2.4/src/phlo_postgres/__init__.py +0 -15
  19. phlo_postgres-0.2.4/src/phlo_postgres/cli_plugin.py +0 -24
  20. phlo_postgres-0.2.4/src/phlo_postgres/plugin.py +0 -127
  21. phlo_postgres-0.2.4/src/phlo_postgres/publish_target.py +0 -21
  22. phlo_postgres-0.2.4/src/phlo_postgres/resource.py +0 -296
  23. phlo_postgres-0.2.4/src/phlo_postgres/settings.py +0 -52
  24. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/README.md +0 -0
  25. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/setup.cfg +0 -0
  26. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres/exporter_service.yaml +0 -0
  27. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres.egg-info/dependency_links.txt +0 -0
  28. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres.egg-info/entry_points.txt +0 -0
  29. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres.egg-info/requires.txt +0 -0
  30. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/src/phlo_postgres.egg-info/top_level.txt +0 -0
  31. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/tests/test_integration_postgres.py +0 -0
  32. {phlo_postgres-0.2.4 → phlo_postgres-0.3.1}/tests/test_postgres_plugin.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phlo-postgres
3
- Version: 0.2.4
3
+ Version: 0.3.1
4
4
  Summary: Postgres service plugin for Phlo
5
5
  Author-email: Phlo Team <team@phlo.dev>
6
6
  License: MIT
@@ -14,7 +14,7 @@ dependencies = [
14
14
  description = "Postgres service plugin for Phlo"
15
15
  name = "phlo-postgres"
16
16
  requires-python = ">=3.11"
17
- version = "0.2.4"
17
+ version = "0.3.1"
18
18
 
19
19
  [[project.authors]]
20
20
  email = "team@phlo.dev"
@@ -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.1"
@@ -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,48 +1,106 @@
1
- """CLI commands for the PostgreSQL service."""
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 ensure_phlo_dir
26
+ from phlo.cli.commands.services.utils import (
27
+ ensure_compose_project,
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
33
+ from phlo.cli.output import (
34
+ command_failed_error,
35
+ empty_file_error,
36
+ exclusive_options_error,
37
+ file_read_error,
38
+ )
39
+ from phlo.cli.output import missing_query_error
17
40
  from phlo_postgres.settings import get_settings
18
41
 
19
42
 
20
43
  def _read_sql(*, query: str | None, file: Path | None) -> str:
21
- """Return SQL text from inline query or file input."""
44
+ """Read SQL from inline query string or file path.
45
+
46
+ Validates that exactly one of query or file is provided and returns the
47
+ SQL content. Handles empty file detection and encoding issues.
48
+
49
+ Args:
50
+ query: Inline SQL query string.
51
+ file: Path to SQL file to read.
52
+
53
+ Returns:
54
+ str: The SQL content to execute.
55
+
56
+ Raises:
57
+ click.ClickException: If both query and file are provided, neither is
58
+ provided, or the file is empty/cannot be read.
59
+
60
+ Example:
61
+ >>> sql = _read_sql(query="SELECT 1")
62
+ >>> sql = _read_sql(file=Path("query.sql"))
63
+
64
+ """
22
65
  if query and file:
23
- raise click.ClickException("Use either an inline query or --file, not both.")
66
+ raise exclusive_options_error("an inline query", "--file")
24
67
  if file is not None:
25
68
  try:
26
69
  sql = file.read_text(encoding="utf-8")
27
70
  except OSError as exc:
28
- raise click.ClickException(f"Failed to read SQL file: {file}") from exc
71
+ raise file_read_error(file) from exc
29
72
  if sql.strip():
30
73
  return sql
31
- raise click.ClickException(f"SQL file is empty: {file}")
74
+ raise empty_file_error(file)
32
75
  if query and query.strip():
33
76
  return query
34
- raise click.ClickException("Provide a SQL query argument or --file.")
77
+ raise missing_query_error(command_hint='phlo postgres query "SELECT 1"')
35
78
 
36
79
 
37
- def _require_docker() -> None:
38
- """Validate that Docker CLI is installed."""
39
- if which("docker") is None:
40
- raise click.ClickException("docker command not found.")
80
+ def _require_container_backend() -> None:
81
+ """Validate that the selected container backend is available."""
82
+ _require_selected_container_backend()
41
83
 
42
84
 
43
85
  def _postgres_exec_base(*, tty: bool) -> list[str]:
44
- """Build the docker compose exec command for the Postgres container."""
45
- phlo_dir = ensure_phlo_dir()
86
+ """Build the docker compose exec base command for PostgreSQL container.
87
+
88
+ Constructs the initial portion of the docker compose exec command including
89
+ project name and service name. Used as a base for all container operations.
90
+
91
+ Args:
92
+ tty: Whether to allocate a TTY (-t flag). Disable for non-interactive
93
+ commands that capture output.
94
+
95
+ Returns:
96
+ list[str]: Base command as a list of strings ready for subprocess.
97
+
98
+ Example:
99
+ >>> cmd = _postgres_exec_base(tty=True)
100
+ >>> # Returns: ['docker', 'compose', '-p', 'phlo', '-f', '...', 'exec', '-t', 'postgres']
101
+
102
+ """
103
+ phlo_dir = ensure_compose_project()
46
104
  project_name = get_project_name()
47
105
  cmd = compose_base_cmd(phlo_dir=phlo_dir, project_name=project_name)
48
106
  cmd.append("exec")
@@ -53,7 +111,26 @@ def _postgres_exec_base(*, tty: bool) -> list[str]:
53
111
 
54
112
 
55
113
  def _postgres_identity(*, user: str | None, database: str | None) -> tuple[str, str]:
56
- """Resolve effective Postgres user/database defaults."""
114
+ """Resolve PostgreSQL connection identity (user and database).
115
+
116
+ Returns explicit values if provided, otherwise falls back to settings
117
+ defaults. This allows CLI commands to use configured defaults while
118
+ permitting overrides.
119
+
120
+ Args:
121
+ user: Database username override, or None to use settings default.
122
+ database: Database name override, or None to use settings default.
123
+
124
+ Returns:
125
+ tuple[str, str]: Tuple of (resolved_user, resolved_database).
126
+
127
+ Example:
128
+ >>> user, db = _postgres_identity(user=None, database=None)
129
+ >>> # Uses settings.postgres_user and settings.postgres_db
130
+ >>> user, db = _postgres_identity(user="admin", database=None)
131
+ >>> # Uses "admin" for user, settings default for database
132
+
133
+ """
57
134
  settings = get_settings()
58
135
  return user or settings.postgres_user, database or settings.postgres_db
59
136
 
@@ -65,7 +142,24 @@ def _postgres_identity(*, user: str | None, database: str | None) -> tuple[str,
65
142
  @click.argument("postgres_args", nargs=-1, type=click.UNPROCESSED)
66
143
  @click.pass_context
67
144
  def postgres_group(ctx: click.Context, postgres_args: tuple[str, ...]) -> None:
68
- """Run psql or Postgres helper commands against the project database."""
145
+ """Run psql or PostgreSQL helper commands against the project database.
146
+
147
+ This is the main entry point for PostgreSQL CLI operations. It supports:
148
+ - Interactive psql sessions (default if no subcommand)
149
+ - Subcommands: query, dump, restore, vacuum
150
+ - Direct psql arguments passthrough
151
+
152
+ Args:
153
+ ctx: Click context object.
154
+ postgres_args: Additional arguments passed to psql or subcommands.
155
+
156
+ Example:
157
+ $ phlo postgres # Interactive psql
158
+ $ phlo postgres -c "SELECT 1" # One-off query via psql
159
+ $ phlo postgres query "SELECT * FROM users"
160
+ $ phlo postgres dump --file backup.sql.gz
161
+
162
+ """
69
163
  if postgres_args and postgres_args[0] == "query":
70
164
  postgres_query.main(
71
165
  args=list(postgres_args[1:]),
@@ -95,7 +189,7 @@ def postgres_group(ctx: click.Context, postgres_args: tuple[str, ...]) -> None:
95
189
  )
96
190
  return
97
191
 
98
- _require_docker()
192
+ _require_container_backend()
99
193
  user, database = _postgres_identity(user=None, database=None)
100
194
  cmd = _postgres_exec_base(tty=True)
101
195
  cmd.extend(["psql", "-U", user, "-d", database])
@@ -122,8 +216,27 @@ def postgres_query(
122
216
  database: str | None,
123
217
  timeout_seconds: int,
124
218
  ) -> None:
125
- """Execute a SQL query against the running PostgreSQL service."""
126
- _require_docker()
219
+ """Execute a SQL query against the running PostgreSQL service.
220
+
221
+ Executes a SQL query inside the PostgreSQL container and prints results
222
+ to stdout. Supports inline queries or reading from a file.
223
+
224
+ Args:
225
+ query: SQL query string to execute.
226
+ query_file: Path to file containing SQL query.
227
+ user: Database user (default from settings).
228
+ database: Database name (default from settings).
229
+ timeout_seconds: Maximum time to wait for query completion.
230
+
231
+ Raises:
232
+ click.ClickException: If the query fails or times out.
233
+
234
+ Example:
235
+ $ phlo postgres query "SELECT * FROM users"
236
+ $ phlo postgres query --file query.sql --timeout 60
237
+
238
+ """
239
+ _require_container_backend()
127
240
  sql = _read_sql(query=query, file=query_file)
128
241
  resolved_user, resolved_db = _postgres_identity(user=user, database=database)
129
242
  cmd = _postgres_exec_base(tty=False)
@@ -138,7 +251,12 @@ def postgres_query(
138
251
  )
139
252
  except CommandError as exc:
140
253
  stderr = exc.stderr.strip()
141
- raise click.ClickException(stderr or str(exc)) from exc
254
+ raise command_failed_error(
255
+ "psql",
256
+ exit_code=exc.returncode,
257
+ details=[stderr] if stderr else ["PostgreSQL did not complete the query."],
258
+ run="phlo services status postgres",
259
+ ) from exc
142
260
  except TimeoutExpired as exc:
143
261
  raise click.ClickException(f"Query timed out after {timeout_seconds} seconds.") from exc
144
262
 
@@ -163,8 +281,26 @@ def postgres_dump(
163
281
  database: str | None,
164
282
  timeout_seconds: int,
165
283
  ) -> None:
166
- """Write a PostgreSQL logical backup to a local file."""
167
- _require_docker()
284
+ """Create a PostgreSQL logical backup (pg_dump) to a local file.
285
+
286
+ Dumps the entire database using pg_dump, with optional gzip compression
287
+ if the output file has a .gz extension.
288
+
289
+ Args:
290
+ output_file: Path to write the dump. Use .gz extension for compression.
291
+ user: Database user (default from settings).
292
+ database: Database name (default from settings).
293
+ timeout_seconds: Maximum time to wait for dump completion.
294
+
295
+ Raises:
296
+ click.ClickException: If the dump fails or times out.
297
+
298
+ Example:
299
+ $ phlo postgres dump --file backup.sql
300
+ $ phlo postgres dump --file backup.sql.gz --timeout 300
301
+
302
+ """
303
+ _require_container_backend()
168
304
  resolved_user, resolved_db = _postgres_identity(user=user, database=database)
169
305
  cmd = _postgres_exec_base(tty=False)
170
306
  cmd.extend(["pg_dump", "-U", resolved_user, resolved_db])
@@ -178,7 +314,12 @@ def postgres_dump(
178
314
  )
179
315
  except CommandError as exc:
180
316
  stderr = exc.stderr.strip()
181
- raise click.ClickException(stderr or str(exc)) from exc
317
+ raise command_failed_error(
318
+ "pg_dump",
319
+ exit_code=exc.returncode,
320
+ details=[stderr] if stderr else ["PostgreSQL did not create the dump."],
321
+ run="phlo services status postgres",
322
+ ) from exc
182
323
  except TimeoutExpired as exc:
183
324
  raise click.ClickException(f"Dump timed out after {timeout_seconds} seconds.") from exc
184
325
 
@@ -212,8 +353,29 @@ def postgres_restore(
212
353
  database: str | None,
213
354
  timeout_seconds: int,
214
355
  ) -> None:
215
- """Restore a PostgreSQL logical backup from a local file."""
216
- _require_docker()
356
+ """Restore a PostgreSQL database from a logical backup file.
357
+
358
+ Restores the database from a SQL dump file (plain or gzip-compressed).
359
+ Uses psql internally to execute the dump SQL.
360
+
361
+ Warning:
362
+ This may overwrite existing data. Use with caution on production databases.
363
+
364
+ Args:
365
+ input_file: Path to the dump file (.sql or .sql.gz).
366
+ user: Database user (default from settings).
367
+ database: Database name (default from settings).
368
+ timeout_seconds: Maximum time to wait for restore completion.
369
+
370
+ Raises:
371
+ click.ClickException: If the restore fails or times out.
372
+
373
+ Example:
374
+ $ phlo postgres restore --file backup.sql
375
+ $ phlo postgres restore --file backup.sql.gz --db mydb --timeout 600
376
+
377
+ """
378
+ _require_container_backend()
217
379
  resolved_user, resolved_db = _postgres_identity(user=user, database=database)
218
380
  cmd = _postgres_exec_base(tty=False)
219
381
  cmd.extend(["psql", "-U", resolved_user, "-d", resolved_db, "-v", "ON_ERROR_STOP=1"])
@@ -257,8 +419,27 @@ def postgres_vacuum(
257
419
  analyze: bool,
258
420
  timeout_seconds: int,
259
421
  ) -> None:
260
- """Run vacuumdb inside the PostgreSQL service container."""
261
- _require_docker()
422
+ """Run vacuumdb for PostgreSQL maintenance inside the container.
423
+
424
+ Executes vacuumdb to reclaim storage and optionally update statistics.
425
+ This is useful for routine database maintenance after large operations.
426
+
427
+ Args:
428
+ user: Database user (default from settings).
429
+ database: Database name (default from settings).
430
+ analyze: Whether to run ANALYZE after vacuum (updates statistics).
431
+ timeout_seconds: Maximum time to wait for vacuum completion.
432
+
433
+ Raises:
434
+ click.ClickException: If vacuum fails or times out.
435
+
436
+ Example:
437
+ $ phlo postgres vacuum
438
+ $ phlo postgres vacuum --no-analyze
439
+ $ phlo postgres vacuum --db analytics --timeout 300
440
+
441
+ """
442
+ _require_container_backend()
262
443
  resolved_user, resolved_db = _postgres_identity(user=user, database=database)
263
444
  cmd = _postgres_exec_base(tty=False)
264
445
  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]