phlo-postgres 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.
- phlo_postgres-0.1.0/PKG-INFO +15 -0
- phlo_postgres-0.1.0/README.md +65 -0
- phlo_postgres-0.1.0/pyproject.toml +42 -0
- phlo_postgres-0.1.0/setup.cfg +4 -0
- phlo_postgres-0.1.0/src/phlo_postgres/__init__.py +7 -0
- phlo_postgres-0.1.0/src/phlo_postgres/exporter_service.yaml +39 -0
- phlo_postgres-0.1.0/src/phlo_postgres/plugin.py +47 -0
- phlo_postgres-0.1.0/src/phlo_postgres/service.yaml +60 -0
- phlo_postgres-0.1.0/src/phlo_postgres/settings.py +34 -0
- phlo_postgres-0.1.0/src/phlo_postgres.egg-info/PKG-INFO +15 -0
- phlo_postgres-0.1.0/src/phlo_postgres.egg-info/SOURCES.txt +15 -0
- phlo_postgres-0.1.0/src/phlo_postgres.egg-info/dependency_links.txt +1 -0
- phlo_postgres-0.1.0/src/phlo_postgres.egg-info/entry_points.txt +3 -0
- phlo_postgres-0.1.0/src/phlo_postgres.egg-info/requires.txt +6 -0
- phlo_postgres-0.1.0/src/phlo_postgres.egg-info/top_level.txt +1 -0
- phlo_postgres-0.1.0/tests/test_integration_postgres.py +295 -0
- phlo_postgres-0.1.0/tests/test_postgres_plugin.py +11 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phlo-postgres
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Postgres service plugin for Phlo
|
|
5
|
+
Author-email: Phlo Team <team@phlo.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/plain
|
|
9
|
+
Requires-Dist: phlo>=0.1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
14
|
+
|
|
15
|
+
Postgres service plugin for Phlo.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# phlo-postgres
|
|
2
|
+
|
|
3
|
+
PostgreSQL database service for Phlo.
|
|
4
|
+
|
|
5
|
+
## Description
|
|
6
|
+
|
|
7
|
+
Core PostgreSQL database for metadata storage, lineage tracking, and operational data. Includes optional Prometheus exporter for database metrics.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install phlo-postgres
|
|
13
|
+
# or
|
|
14
|
+
phlo plugin install postgres
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Configuration
|
|
18
|
+
|
|
19
|
+
| Variable | Default | Description |
|
|
20
|
+
| ------------------------ | -------- | ---------------------- |
|
|
21
|
+
| `POSTGRES_PORT` | `5432` | PostgreSQL port |
|
|
22
|
+
| `POSTGRES_USER` | `phlo` | Database username |
|
|
23
|
+
| `POSTGRES_PASSWORD` | `phlo` | Database password |
|
|
24
|
+
| `POSTGRES_DB` | `phlo` | Database name |
|
|
25
|
+
| `POSTGRES_SSL_MODE` | `prefer` | SSL mode |
|
|
26
|
+
| `POSTGRES_EXPORTER_PORT` | `9187` | Postgres exporter port |
|
|
27
|
+
|
|
28
|
+
## Auto-Configuration
|
|
29
|
+
|
|
30
|
+
This package is **fully auto-configured**:
|
|
31
|
+
|
|
32
|
+
| Feature | How It Works |
|
|
33
|
+
| ---------------------- | ---------------------------------------------------------- |
|
|
34
|
+
| **Grafana Datasource** | Auto-registers as Grafana datasource via labels |
|
|
35
|
+
| **postgres-exporter** | Optional Prometheus exporter for native PostgreSQL metrics |
|
|
36
|
+
| **Service Discovery** | Exporter auto-scraped by Prometheus |
|
|
37
|
+
|
|
38
|
+
### Grafana Labels
|
|
39
|
+
|
|
40
|
+
```yaml
|
|
41
|
+
compose:
|
|
42
|
+
labels:
|
|
43
|
+
phlo.grafana.datasource: "true"
|
|
44
|
+
phlo.grafana.datasource.type: "postgres"
|
|
45
|
+
phlo.grafana.datasource.name: "PostgreSQL"
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Start PostgreSQL
|
|
52
|
+
phlo services start --service postgres
|
|
53
|
+
|
|
54
|
+
# Start with exporter (for observability)
|
|
55
|
+
phlo services start --service postgres,postgres-exporter
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Endpoints
|
|
59
|
+
|
|
60
|
+
- **PostgreSQL**: `localhost:5432`
|
|
61
|
+
- **Exporter Metrics**: `http://localhost:9187/metrics`
|
|
62
|
+
|
|
63
|
+
## Entry Points
|
|
64
|
+
|
|
65
|
+
- `phlo.plugins.services` - Provides `PostgresServicePlugin` and `PostgresExporterServicePlugin`
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=45", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "phlo-postgres"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Postgres service plugin for Phlo"
|
|
9
|
+
readme = {text = "Postgres service plugin for Phlo.", content-type = "text/plain"}
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
authors = [
|
|
12
|
+
{name = "Phlo Team", email = "team@phlo.dev"},
|
|
13
|
+
]
|
|
14
|
+
license = {text = "MIT"}
|
|
15
|
+
dependencies = [
|
|
16
|
+
"phlo>=0.1.0",
|
|
17
|
+
"pyyaml>=6.0.1",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = [
|
|
22
|
+
"pytest>=7.0",
|
|
23
|
+
"ruff>=0.1.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.entry-points."phlo.plugins.services"]
|
|
27
|
+
postgres = "phlo_postgres.plugin:PostgresServicePlugin"
|
|
28
|
+
postgres-exporter = "phlo_postgres.plugin:PostgresExporterServicePlugin"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
package-dir = {"" = "src"}
|
|
32
|
+
include-package-data = true
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.packages.find]
|
|
35
|
+
where = ["src"]
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.package-data]
|
|
38
|
+
phlo_postgres = ["service.yaml", "exporter_service.yaml"]
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py311"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: postgres-exporter
|
|
2
|
+
description: Prometheus exporter for PostgreSQL metrics
|
|
3
|
+
category: observability
|
|
4
|
+
default: false
|
|
5
|
+
profile: observability
|
|
6
|
+
|
|
7
|
+
image: quay.io/prometheuscommunity/postgres-exporter:v0.15.0
|
|
8
|
+
|
|
9
|
+
depends_on:
|
|
10
|
+
- postgres
|
|
11
|
+
|
|
12
|
+
compose:
|
|
13
|
+
restart: unless-stopped
|
|
14
|
+
labels:
|
|
15
|
+
phlo.metrics.enabled: "true"
|
|
16
|
+
phlo.metrics.port: "postgres-exporter:9187"
|
|
17
|
+
phlo.metrics.path: "/metrics"
|
|
18
|
+
environment:
|
|
19
|
+
DATA_SOURCE_NAME: postgresql://${POSTGRES_USER:-phlo}:${POSTGRES_PASSWORD:-phlo}@postgres:5432/${POSTGRES_DB:-phlo}?sslmode=disable
|
|
20
|
+
ports:
|
|
21
|
+
- "${POSTGRES_EXPORTER_PORT:-9187}:9187"
|
|
22
|
+
healthcheck:
|
|
23
|
+
test:
|
|
24
|
+
[
|
|
25
|
+
"CMD",
|
|
26
|
+
"wget",
|
|
27
|
+
"--quiet",
|
|
28
|
+
"--tries=1",
|
|
29
|
+
"--spider",
|
|
30
|
+
"http://localhost:9187/metrics",
|
|
31
|
+
]
|
|
32
|
+
interval: 10s
|
|
33
|
+
timeout: 5s
|
|
34
|
+
retries: 5
|
|
35
|
+
|
|
36
|
+
env_vars:
|
|
37
|
+
POSTGRES_EXPORTER_PORT:
|
|
38
|
+
default: 9187
|
|
39
|
+
description: Postgres exporter metrics port
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Postgres service plugin."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from importlib import resources
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
from phlo.plugins import PluginMetadata, ServicePlugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PostgresServicePlugin(ServicePlugin):
|
|
13
|
+
"""Service plugin for Postgres."""
|
|
14
|
+
|
|
15
|
+
@property
|
|
16
|
+
def metadata(self) -> PluginMetadata:
|
|
17
|
+
return PluginMetadata(
|
|
18
|
+
name="postgres",
|
|
19
|
+
version="0.1.0",
|
|
20
|
+
description="PostgreSQL database for metadata and operational storage",
|
|
21
|
+
author="Phlo Team",
|
|
22
|
+
tags=["core", "database", "postgres"],
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def service_definition(self) -> dict[str, Any]:
|
|
27
|
+
service_path = resources.files("phlo_postgres").joinpath("service.yaml")
|
|
28
|
+
return yaml.safe_load(service_path.read_text(encoding="utf-8"))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class PostgresExporterServicePlugin(ServicePlugin):
|
|
32
|
+
"""Service plugin for Postgres Prometheus exporter."""
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def metadata(self) -> PluginMetadata:
|
|
36
|
+
return PluginMetadata(
|
|
37
|
+
name="postgres-exporter",
|
|
38
|
+
version="0.1.0",
|
|
39
|
+
description="Prometheus exporter for PostgreSQL metrics",
|
|
40
|
+
author="Phlo Team",
|
|
41
|
+
tags=["observability", "metrics", "postgres"],
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def service_definition(self) -> dict[str, Any]:
|
|
46
|
+
service_path = resources.files("phlo_postgres").joinpath("exporter_service.yaml")
|
|
47
|
+
return yaml.safe_load(service_path.read_text(encoding="utf-8"))
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
name: postgres
|
|
2
|
+
description: PostgreSQL database for metadata and operational storage
|
|
3
|
+
category: core
|
|
4
|
+
default: true
|
|
5
|
+
|
|
6
|
+
image: postgres:16-alpine
|
|
7
|
+
|
|
8
|
+
compose:
|
|
9
|
+
restart: unless-stopped
|
|
10
|
+
labels:
|
|
11
|
+
phlo.metrics.enabled: "false"
|
|
12
|
+
phlo.grafana.datasource: "true"
|
|
13
|
+
phlo.grafana.datasource.type: "postgres"
|
|
14
|
+
phlo.grafana.datasource.name: "PostgreSQL"
|
|
15
|
+
phlo.grafana.datasource.url: "postgres:5432"
|
|
16
|
+
phlo.grafana.datasource.database: "${POSTGRES_DB:-phlo}"
|
|
17
|
+
environment:
|
|
18
|
+
POSTGRES_USER: ${POSTGRES_USER:-phlo}
|
|
19
|
+
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-phlo}
|
|
20
|
+
POSTGRES_DB: ${POSTGRES_DB:-phlo}
|
|
21
|
+
# SSL/TLS
|
|
22
|
+
POSTGRES_SSL_MODE: ${POSTGRES_SSL_MODE:-prefer}
|
|
23
|
+
ports:
|
|
24
|
+
- "${POSTGRES_PORT:-5432}:5432"
|
|
25
|
+
volumes:
|
|
26
|
+
- ./volumes/postgres:/var/lib/postgresql/data
|
|
27
|
+
- ./volumes/postgres-certs:/var/lib/postgresql/certs:ro
|
|
28
|
+
healthcheck:
|
|
29
|
+
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-phlo}"]
|
|
30
|
+
interval: 10s
|
|
31
|
+
timeout: 5s
|
|
32
|
+
retries: 10
|
|
33
|
+
|
|
34
|
+
env_vars:
|
|
35
|
+
POSTGRES_USER:
|
|
36
|
+
default: phlo
|
|
37
|
+
description: PostgreSQL username
|
|
38
|
+
POSTGRES_PASSWORD:
|
|
39
|
+
default: phlo
|
|
40
|
+
description: PostgreSQL password
|
|
41
|
+
secret: true
|
|
42
|
+
POSTGRES_DB:
|
|
43
|
+
default: phlo
|
|
44
|
+
description: PostgreSQL database name
|
|
45
|
+
POSTGRES_PORT:
|
|
46
|
+
default: 5432
|
|
47
|
+
description: PostgreSQL host port
|
|
48
|
+
# SSL/TLS
|
|
49
|
+
POSTGRES_SSL_MODE:
|
|
50
|
+
default: "prefer"
|
|
51
|
+
description: "SSL mode: disable, allow, prefer, require, verify-ca, verify-full"
|
|
52
|
+
POSTGRES_SSL_CERT_FILE:
|
|
53
|
+
default: ""
|
|
54
|
+
description: "Path to SSL certificate file"
|
|
55
|
+
POSTGRES_SSL_KEY_FILE:
|
|
56
|
+
default: ""
|
|
57
|
+
description: "Path to SSL private key file"
|
|
58
|
+
POSTGRES_SSL_CA_FILE:
|
|
59
|
+
default: ""
|
|
60
|
+
description: "Path to SSL CA certificate file"
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Postgres settings."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from functools import lru_cache
|
|
6
|
+
from urllib.parse import quote_plus
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from phlo.config.base import BaseConfig
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class PostgresSettings(BaseConfig):
|
|
14
|
+
"""PostgreSQL database connection and schema configuration."""
|
|
15
|
+
|
|
16
|
+
postgres_host: str = Field(default="postgres", description="PostgreSQL host")
|
|
17
|
+
postgres_port: int = Field(default=10000, description="PostgreSQL port")
|
|
18
|
+
postgres_user: str = Field(default="lake", description="PostgreSQL username")
|
|
19
|
+
postgres_password: str = Field(default="phlo", description="PostgreSQL password")
|
|
20
|
+
postgres_db: str = Field(default="lakehouse", description="PostgreSQL database name")
|
|
21
|
+
postgres_mart_schema: str = Field(
|
|
22
|
+
default="marts", description="Schema for published mart tables"
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
def get_postgres_connection_string(self, include_db: bool = True) -> str:
|
|
26
|
+
db_part = f"/{self.postgres_db}" if include_db else ""
|
|
27
|
+
user = quote_plus(self.postgres_user)
|
|
28
|
+
password = quote_plus(self.postgres_password)
|
|
29
|
+
return f"postgresql://{user}:{password}@{self.postgres_host}:{self.postgres_port}{db_part}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@lru_cache(maxsize=1)
|
|
33
|
+
def get_settings() -> PostgresSettings:
|
|
34
|
+
return PostgresSettings()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: phlo-postgres
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Postgres service plugin for Phlo
|
|
5
|
+
Author-email: Phlo Team <team@phlo.dev>
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/plain
|
|
9
|
+
Requires-Dist: phlo>=0.1.0
|
|
10
|
+
Requires-Dist: pyyaml>=6.0.1
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
14
|
+
|
|
15
|
+
Postgres service plugin for Phlo.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/phlo_postgres/__init__.py
|
|
4
|
+
src/phlo_postgres/exporter_service.yaml
|
|
5
|
+
src/phlo_postgres/plugin.py
|
|
6
|
+
src/phlo_postgres/service.yaml
|
|
7
|
+
src/phlo_postgres/settings.py
|
|
8
|
+
src/phlo_postgres.egg-info/PKG-INFO
|
|
9
|
+
src/phlo_postgres.egg-info/SOURCES.txt
|
|
10
|
+
src/phlo_postgres.egg-info/dependency_links.txt
|
|
11
|
+
src/phlo_postgres.egg-info/entry_points.txt
|
|
12
|
+
src/phlo_postgres.egg-info/requires.txt
|
|
13
|
+
src/phlo_postgres.egg-info/top_level.txt
|
|
14
|
+
tests/test_integration_postgres.py
|
|
15
|
+
tests/test_postgres_plugin.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
phlo_postgres
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
"""Comprehensive integration tests for phlo-postgres.
|
|
2
|
+
|
|
3
|
+
Per TEST_STRATEGY.md Level 2 (Functional):
|
|
4
|
+
- Connection String Building: Test connection string construction
|
|
5
|
+
- DB Ops: Create user, create DB, verify connectivity
|
|
6
|
+
- Service Plugin: Verify plugin registration
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from unittest.mock import patch, MagicMock
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
pytestmark = pytest.mark.integration
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# =============================================================================
|
|
17
|
+
# Connection Configuration Tests
|
|
18
|
+
# =============================================================================
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestPostgresConfiguration:
|
|
22
|
+
"""Test Postgres configuration and connection building."""
|
|
23
|
+
|
|
24
|
+
def test_postgres_config_accessible(self):
|
|
25
|
+
"""Test Postgres configuration is accessible from phlo_postgres.settings."""
|
|
26
|
+
from phlo_postgres.settings import get_settings
|
|
27
|
+
|
|
28
|
+
settings = get_settings()
|
|
29
|
+
|
|
30
|
+
assert hasattr(settings, "postgres_host")
|
|
31
|
+
assert hasattr(settings, "postgres_port")
|
|
32
|
+
assert hasattr(settings, "postgres_db")
|
|
33
|
+
assert hasattr(settings, "postgres_user")
|
|
34
|
+
|
|
35
|
+
def test_postgres_config_has_defaults(self):
|
|
36
|
+
"""Test Postgres config has sensible defaults."""
|
|
37
|
+
from phlo_postgres.settings import get_settings
|
|
38
|
+
|
|
39
|
+
settings = get_settings()
|
|
40
|
+
|
|
41
|
+
assert settings.postgres_host is not None
|
|
42
|
+
assert settings.postgres_port > 0
|
|
43
|
+
assert settings.postgres_db is not None
|
|
44
|
+
|
|
45
|
+
def test_connection_string_building(self):
|
|
46
|
+
"""Test building a connection string from config."""
|
|
47
|
+
from phlo_postgres.settings import get_settings
|
|
48
|
+
|
|
49
|
+
settings = get_settings()
|
|
50
|
+
|
|
51
|
+
# Build connection string
|
|
52
|
+
conn_str = (
|
|
53
|
+
f"postgresql://{settings.postgres_user}:{settings.postgres_password}"
|
|
54
|
+
f"@{settings.postgres_host}:{settings.postgres_port}/{settings.postgres_db}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
assert "postgresql://" in conn_str
|
|
58
|
+
assert str(settings.postgres_port) in conn_str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# Service Plugin Tests
|
|
63
|
+
# =============================================================================
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestPostgresServicePlugin:
|
|
67
|
+
"""Test Postgres service plugin."""
|
|
68
|
+
|
|
69
|
+
def test_plugin_initializes(self):
|
|
70
|
+
"""Test PostgresServicePlugin can be instantiated."""
|
|
71
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
72
|
+
|
|
73
|
+
plugin = PostgresServicePlugin()
|
|
74
|
+
assert plugin is not None
|
|
75
|
+
|
|
76
|
+
def test_plugin_metadata(self):
|
|
77
|
+
"""Test plugin metadata is correctly defined."""
|
|
78
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
79
|
+
|
|
80
|
+
plugin = PostgresServicePlugin()
|
|
81
|
+
metadata = plugin.metadata
|
|
82
|
+
|
|
83
|
+
assert metadata.name == "postgres"
|
|
84
|
+
assert metadata.version is not None
|
|
85
|
+
|
|
86
|
+
def test_service_definition_loads(self):
|
|
87
|
+
"""Test service definition YAML can be loaded."""
|
|
88
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
89
|
+
|
|
90
|
+
plugin = PostgresServicePlugin()
|
|
91
|
+
service_def = plugin.service_definition
|
|
92
|
+
|
|
93
|
+
assert isinstance(service_def, dict)
|
|
94
|
+
# Service definitions have flat structure with 'name' and 'compose' keys
|
|
95
|
+
assert "name" in service_def or "compose" in service_def
|
|
96
|
+
|
|
97
|
+
def test_service_definition_has_image(self):
|
|
98
|
+
"""Test service definition has container image specified."""
|
|
99
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
100
|
+
|
|
101
|
+
plugin = PostgresServicePlugin()
|
|
102
|
+
service_def = plugin.service_definition
|
|
103
|
+
|
|
104
|
+
# Navigate to find image
|
|
105
|
+
services = service_def.get("services", {})
|
|
106
|
+
if services:
|
|
107
|
+
# Should have postgres service with image
|
|
108
|
+
for svc_name, svc_config in services.items():
|
|
109
|
+
if "postgres" in svc_name.lower():
|
|
110
|
+
assert "image" in svc_config or "build" in svc_config
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# =============================================================================
|
|
114
|
+
# Psycopg2 Connection Tests (with mocks)
|
|
115
|
+
# =============================================================================
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestPostgresConnectionMocked:
|
|
119
|
+
"""Test Postgres connections with mocks."""
|
|
120
|
+
|
|
121
|
+
def test_psycopg2_connect_with_config(self):
|
|
122
|
+
"""Test psycopg2 connection using config values."""
|
|
123
|
+
import psycopg2
|
|
124
|
+
from phlo_postgres.settings import get_settings
|
|
125
|
+
|
|
126
|
+
settings = get_settings()
|
|
127
|
+
|
|
128
|
+
mock_conn = MagicMock()
|
|
129
|
+
|
|
130
|
+
with patch("psycopg2.connect", return_value=mock_conn) as mock_connect:
|
|
131
|
+
# Simulate what most code does
|
|
132
|
+
psycopg2.connect(
|
|
133
|
+
host=settings.postgres_host,
|
|
134
|
+
port=settings.postgres_port,
|
|
135
|
+
database=settings.postgres_db,
|
|
136
|
+
user=settings.postgres_user,
|
|
137
|
+
password=settings.postgres_password,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
mock_connect.assert_called_once()
|
|
141
|
+
# Verify kwargs
|
|
142
|
+
call_kwargs = mock_connect.call_args.kwargs
|
|
143
|
+
assert call_kwargs["host"] == settings.postgres_host
|
|
144
|
+
assert call_kwargs["database"] == settings.postgres_db
|
|
145
|
+
|
|
146
|
+
def test_connection_with_cursor_context(self):
|
|
147
|
+
"""Test connection with cursor context manager."""
|
|
148
|
+
mock_cursor = MagicMock()
|
|
149
|
+
mock_cursor.fetchall.return_value = [(1,)]
|
|
150
|
+
|
|
151
|
+
mock_conn = MagicMock()
|
|
152
|
+
mock_conn.cursor.return_value.__enter__ = lambda _: mock_cursor
|
|
153
|
+
mock_conn.cursor.return_value.__exit__ = MagicMock(return_value=False)
|
|
154
|
+
|
|
155
|
+
with patch("psycopg2.connect", return_value=mock_conn):
|
|
156
|
+
import psycopg2
|
|
157
|
+
|
|
158
|
+
conn = psycopg2.connect(host="localhost", database="test")
|
|
159
|
+
with conn.cursor() as cur:
|
|
160
|
+
cur.execute("SELECT 1")
|
|
161
|
+
result = cur.fetchall()
|
|
162
|
+
|
|
163
|
+
assert result == [(1,)]
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# =============================================================================
|
|
167
|
+
# Functional Integration Tests (Real Postgres if available)
|
|
168
|
+
# =============================================================================
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
@pytest.fixture
|
|
172
|
+
def postgres_connection():
|
|
173
|
+
"""Fixture providing a real Postgres connection if available."""
|
|
174
|
+
from phlo_postgres.settings import get_settings
|
|
175
|
+
import psycopg2
|
|
176
|
+
|
|
177
|
+
settings = get_settings()
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
conn = psycopg2.connect(
|
|
181
|
+
host=settings.postgres_host,
|
|
182
|
+
port=settings.postgres_port,
|
|
183
|
+
database=settings.postgres_db,
|
|
184
|
+
user=settings.postgres_user,
|
|
185
|
+
password=settings.postgres_password,
|
|
186
|
+
connect_timeout=5,
|
|
187
|
+
)
|
|
188
|
+
yield conn
|
|
189
|
+
conn.close()
|
|
190
|
+
except Exception as e:
|
|
191
|
+
pytest.skip(f"Postgres not available: {e}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class TestPostgresIntegrationReal:
|
|
195
|
+
"""Real integration tests against a running Postgres instance."""
|
|
196
|
+
|
|
197
|
+
def test_simple_query(self, postgres_connection):
|
|
198
|
+
"""Test simple query execution against real Postgres."""
|
|
199
|
+
with postgres_connection.cursor() as cur:
|
|
200
|
+
cur.execute("SELECT 1 AS one")
|
|
201
|
+
result = cur.fetchall()
|
|
202
|
+
|
|
203
|
+
assert result == [(1,)]
|
|
204
|
+
|
|
205
|
+
def test_version_query(self, postgres_connection):
|
|
206
|
+
"""Test querying Postgres version."""
|
|
207
|
+
with postgres_connection.cursor() as cur:
|
|
208
|
+
cur.execute("SELECT version()")
|
|
209
|
+
result = cur.fetchone()
|
|
210
|
+
|
|
211
|
+
assert result is not None
|
|
212
|
+
assert "PostgreSQL" in result[0]
|
|
213
|
+
|
|
214
|
+
def test_create_temp_table(self, postgres_connection):
|
|
215
|
+
"""Test creating and querying a temporary table."""
|
|
216
|
+
with postgres_connection.cursor() as cur:
|
|
217
|
+
cur.execute("""
|
|
218
|
+
CREATE TEMP TABLE test_table (
|
|
219
|
+
id SERIAL PRIMARY KEY,
|
|
220
|
+
name VARCHAR(100)
|
|
221
|
+
)
|
|
222
|
+
""")
|
|
223
|
+
cur.execute("INSERT INTO test_table (name) VALUES ('test1'), ('test2')")
|
|
224
|
+
cur.execute("SELECT COUNT(*) FROM test_table")
|
|
225
|
+
result = cur.fetchone()
|
|
226
|
+
|
|
227
|
+
assert result[0] == 2
|
|
228
|
+
|
|
229
|
+
def test_list_databases(self, postgres_connection):
|
|
230
|
+
"""Test listing databases."""
|
|
231
|
+
with postgres_connection.cursor() as cur:
|
|
232
|
+
cur.execute("""
|
|
233
|
+
SELECT datname FROM pg_database
|
|
234
|
+
WHERE datistemplate = false
|
|
235
|
+
""")
|
|
236
|
+
databases = [row[0] for row in cur.fetchall()]
|
|
237
|
+
|
|
238
|
+
# Should have at least the connected database
|
|
239
|
+
assert len(databases) >= 1
|
|
240
|
+
|
|
241
|
+
def test_list_tables(self, postgres_connection):
|
|
242
|
+
"""Test listing tables in public schema."""
|
|
243
|
+
with postgres_connection.cursor() as cur:
|
|
244
|
+
cur.execute("""
|
|
245
|
+
SELECT table_name
|
|
246
|
+
FROM information_schema.tables
|
|
247
|
+
WHERE table_schema = 'public'
|
|
248
|
+
""")
|
|
249
|
+
tables = [row[0] for row in cur.fetchall()]
|
|
250
|
+
|
|
251
|
+
# May be empty if no tables
|
|
252
|
+
assert isinstance(tables, list)
|
|
253
|
+
|
|
254
|
+
def test_transaction_rollback(self, postgres_connection):
|
|
255
|
+
"""Test transaction rollback."""
|
|
256
|
+
postgres_connection.autocommit = False
|
|
257
|
+
|
|
258
|
+
with postgres_connection.cursor() as cur:
|
|
259
|
+
cur.execute("CREATE TEMP TABLE rollback_test (id INT)")
|
|
260
|
+
cur.execute("INSERT INTO rollback_test VALUES (1)")
|
|
261
|
+
|
|
262
|
+
# Rollback
|
|
263
|
+
postgres_connection.rollback()
|
|
264
|
+
|
|
265
|
+
# Table should not exist after rollback
|
|
266
|
+
with postgres_connection.cursor() as cur:
|
|
267
|
+
cur.execute("""
|
|
268
|
+
SELECT COUNT(*) FROM information_schema.tables
|
|
269
|
+
WHERE table_name = 'rollback_test'
|
|
270
|
+
""")
|
|
271
|
+
result = cur.fetchone()
|
|
272
|
+
|
|
273
|
+
# This may vary based on temp table behavior
|
|
274
|
+
assert result is not None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# =============================================================================
|
|
278
|
+
# Version and Export Tests
|
|
279
|
+
# =============================================================================
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
class TestPostgresExports:
|
|
283
|
+
"""Test module exports and version."""
|
|
284
|
+
|
|
285
|
+
def test_plugin_importable(self):
|
|
286
|
+
"""Test PostgresServicePlugin is importable."""
|
|
287
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
288
|
+
|
|
289
|
+
assert PostgresServicePlugin is not None
|
|
290
|
+
|
|
291
|
+
def test_module_importable(self):
|
|
292
|
+
"""Test phlo_postgres module is importable."""
|
|
293
|
+
import phlo_postgres
|
|
294
|
+
|
|
295
|
+
assert phlo_postgres is not None
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Tests for Postgres service plugin."""
|
|
2
|
+
|
|
3
|
+
from phlo_postgres.plugin import PostgresServicePlugin
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_postgres_service_definition():
|
|
7
|
+
plugin = PostgresServicePlugin()
|
|
8
|
+
service_definition = plugin.service_definition
|
|
9
|
+
|
|
10
|
+
assert service_definition["name"] == "postgres"
|
|
11
|
+
assert service_definition["category"] == "core"
|