phlo-api 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.
- phlo_api/Dockerfile +17 -0
- phlo_api/__init__.py +1 -0
- phlo_api/main.py +218 -0
- phlo_api/observatory_api/__init__.py +6 -0
- phlo_api/observatory_api/dagster.py +535 -0
- phlo_api/observatory_api/extension_settings.py +100 -0
- phlo_api/observatory_api/extensions.py +142 -0
- phlo_api/observatory_api/iceberg.py +347 -0
- phlo_api/observatory_api/lineage.py +399 -0
- phlo_api/observatory_api/loki.py +288 -0
- phlo_api/observatory_api/maintenance.py +96 -0
- phlo_api/observatory_api/nessie.py +364 -0
- phlo_api/observatory_api/quality.py +553 -0
- phlo_api/observatory_api/search.py +136 -0
- phlo_api/observatory_api/settings.py +138 -0
- phlo_api/observatory_api/trino.py +641 -0
- phlo_api/plugin.py +29 -0
- phlo_api/service.yaml +52 -0
- phlo_api-0.1.0.dist-info/METADATA +23 -0
- phlo_api-0.1.0.dist-info/RECORD +23 -0
- phlo_api-0.1.0.dist-info/WHEEL +5 -0
- phlo_api-0.1.0.dist-info/entry_points.txt +2 -0
- phlo_api-0.1.0.dist-info/top_level.txt +1 -0
phlo_api/Dockerfile
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install phlo and dependencies
|
|
6
|
+
RUN pip install --no-cache-dir phlo fastapi uvicorn pyyaml
|
|
7
|
+
|
|
8
|
+
# Copy the API application
|
|
9
|
+
COPY main.py .
|
|
10
|
+
|
|
11
|
+
# Health check
|
|
12
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
|
13
|
+
CMD curl -f http://localhost:4000/health || exit 1
|
|
14
|
+
|
|
15
|
+
EXPOSE 4000
|
|
16
|
+
|
|
17
|
+
CMD ["python", "main.py"]
|
phlo_api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
phlo_api/main.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Phlo API - Backend service exposing phlo internals to Observatory.
|
|
3
|
+
|
|
4
|
+
This FastAPI service provides endpoints for Observatory to:
|
|
5
|
+
- List installed plugins
|
|
6
|
+
- Get service status and configuration
|
|
7
|
+
- Read phlo.yaml config
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import importlib
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
from fastapi import FastAPI, HTTPException
|
|
20
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
app = FastAPI(
|
|
25
|
+
title="Phlo API",
|
|
26
|
+
description="Backend API for Phlo Observatory",
|
|
27
|
+
version="0.1.0",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Allow CORS for Observatory
|
|
31
|
+
app.add_middleware(
|
|
32
|
+
CORSMiddleware,
|
|
33
|
+
allow_origins=["*"],
|
|
34
|
+
allow_credentials=True,
|
|
35
|
+
allow_methods=["*"],
|
|
36
|
+
allow_headers=["*"],
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Auto-discover and register Observatory API routers
|
|
40
|
+
_OBSERVATORY_ROUTERS = [
|
|
41
|
+
("trino", "/api/trino"),
|
|
42
|
+
("iceberg", "/api/iceberg"),
|
|
43
|
+
("dagster", "/api/dagster"),
|
|
44
|
+
("nessie", "/api/nessie"),
|
|
45
|
+
("quality", "/api/quality"),
|
|
46
|
+
("loki", "/api/loki"),
|
|
47
|
+
("lineage", "/api/lineage"),
|
|
48
|
+
("maintenance", "/api/maintenance"),
|
|
49
|
+
("search", "/api/search"),
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
_OBSERVATORY_ROUTERS_NO_PREFIX = [
|
|
53
|
+
"extensions",
|
|
54
|
+
"extension_settings",
|
|
55
|
+
"settings",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _register_observatory_routers() -> None:
|
|
60
|
+
"""Register Observatory API routers if available."""
|
|
61
|
+
# Combine routers with prefix and without prefix into single iterable
|
|
62
|
+
all_routers = [
|
|
63
|
+
*_OBSERVATORY_ROUTERS,
|
|
64
|
+
*((name, None) for name in _OBSERVATORY_ROUTERS_NO_PREFIX),
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
for name, prefix in all_routers:
|
|
68
|
+
try:
|
|
69
|
+
module = importlib.import_module(f"phlo_api.observatory_api.{name}")
|
|
70
|
+
router = getattr(module, "router", None)
|
|
71
|
+
if router:
|
|
72
|
+
if prefix is not None:
|
|
73
|
+
app.include_router(router, prefix=prefix)
|
|
74
|
+
else:
|
|
75
|
+
app.include_router(router)
|
|
76
|
+
except ImportError as e:
|
|
77
|
+
logger.debug("Failed to import observatory router %s: %s", name, e)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
_register_observatory_routers()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_project_path() -> Path:
|
|
84
|
+
"""Get the phlo project path from environment or default."""
|
|
85
|
+
project_path = os.environ.get("PHLO_PROJECT_PATH", "/app/project")
|
|
86
|
+
return Path(project_path)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_phlo_config() -> dict[str, Any]:
|
|
90
|
+
"""Load phlo.yaml configuration."""
|
|
91
|
+
config_path = get_project_path() / "phlo.yaml"
|
|
92
|
+
if not config_path.exists():
|
|
93
|
+
return {"name": "unknown", "description": ""}
|
|
94
|
+
|
|
95
|
+
with open(config_path) as f:
|
|
96
|
+
return yaml.safe_load(f) or {}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.get("/health")
|
|
100
|
+
def health() -> dict[str, str]:
|
|
101
|
+
"""Health check endpoint."""
|
|
102
|
+
return {"status": "healthy"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@app.get("/api/config")
|
|
106
|
+
def get_config() -> dict[str, Any]:
|
|
107
|
+
"""Get phlo.yaml configuration."""
|
|
108
|
+
return load_phlo_config()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.get("/api/plugins")
|
|
112
|
+
def get_plugins() -> dict[str, list[str]]:
|
|
113
|
+
"""List all installed plugins by type."""
|
|
114
|
+
try:
|
|
115
|
+
from phlo.plugins.discovery import list_plugins
|
|
116
|
+
|
|
117
|
+
return list_plugins()
|
|
118
|
+
except ImportError:
|
|
119
|
+
return {
|
|
120
|
+
"source_connectors": [],
|
|
121
|
+
"quality_checks": [],
|
|
122
|
+
"transformations": [],
|
|
123
|
+
"services": [],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.get("/api/plugins/{plugin_type}")
|
|
128
|
+
def get_plugins_by_type(plugin_type: str) -> list[str]:
|
|
129
|
+
"""List plugins of a specific type."""
|
|
130
|
+
try:
|
|
131
|
+
from phlo.plugins.discovery import list_plugins
|
|
132
|
+
|
|
133
|
+
all_plugins = list_plugins()
|
|
134
|
+
if plugin_type not in all_plugins:
|
|
135
|
+
raise HTTPException(status_code=404, detail=f"Unknown plugin type: {plugin_type}")
|
|
136
|
+
return all_plugins[plugin_type]
|
|
137
|
+
except ImportError:
|
|
138
|
+
return []
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.get("/api/plugins/{plugin_type}/{name}")
|
|
142
|
+
def get_plugin_info(plugin_type: str, name: str) -> dict[str, Any]:
|
|
143
|
+
"""Get detailed information about a specific plugin."""
|
|
144
|
+
try:
|
|
145
|
+
from phlo.plugins.discovery import get_plugin_info as _get_plugin_info
|
|
146
|
+
|
|
147
|
+
info = _get_plugin_info(plugin_type, name)
|
|
148
|
+
if not info:
|
|
149
|
+
raise HTTPException(status_code=404, detail=f"Plugin not found: {name}")
|
|
150
|
+
return info
|
|
151
|
+
except ImportError as e:
|
|
152
|
+
raise HTTPException(status_code=500, detail="Plugin system not available") from e
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@app.get("/api/services")
|
|
156
|
+
def get_services() -> list[dict[str, Any]]:
|
|
157
|
+
"""List all discovered services."""
|
|
158
|
+
try:
|
|
159
|
+
from phlo.plugins.discovery import ServiceDiscovery
|
|
160
|
+
|
|
161
|
+
discovery = ServiceDiscovery()
|
|
162
|
+
services = discovery.discover()
|
|
163
|
+
return [
|
|
164
|
+
{
|
|
165
|
+
"name": s.name,
|
|
166
|
+
"description": s.description,
|
|
167
|
+
"category": s.category,
|
|
168
|
+
"default": s.default,
|
|
169
|
+
"profile": s.profile,
|
|
170
|
+
"core": getattr(s, "core", False),
|
|
171
|
+
}
|
|
172
|
+
for s in services.values()
|
|
173
|
+
]
|
|
174
|
+
except ImportError:
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@app.get("/api/services/{name}")
|
|
179
|
+
def get_service_info(name: str) -> dict[str, Any]:
|
|
180
|
+
"""Get detailed information about a specific service."""
|
|
181
|
+
try:
|
|
182
|
+
from phlo.plugins.discovery import ServiceDiscovery
|
|
183
|
+
|
|
184
|
+
discovery = ServiceDiscovery()
|
|
185
|
+
service = discovery.get_service(name)
|
|
186
|
+
if not service:
|
|
187
|
+
raise HTTPException(status_code=404, detail=f"Service not found: {name}")
|
|
188
|
+
return {
|
|
189
|
+
"name": service.name,
|
|
190
|
+
"description": service.description,
|
|
191
|
+
"category": service.category,
|
|
192
|
+
"default": service.default,
|
|
193
|
+
"profile": service.profile,
|
|
194
|
+
"depends_on": service.depends_on,
|
|
195
|
+
"env_vars": service.env_vars,
|
|
196
|
+
"core": getattr(service, "core", False),
|
|
197
|
+
}
|
|
198
|
+
except ImportError as e:
|
|
199
|
+
raise HTTPException(status_code=500, detail="Service discovery not available") from e
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.get("/api/registry")
|
|
203
|
+
def get_registry() -> dict[str, Any]:
|
|
204
|
+
"""Get the plugin registry (available plugins for installation)."""
|
|
205
|
+
try:
|
|
206
|
+
from phlo.plugins.registry_client import get_registry_data
|
|
207
|
+
|
|
208
|
+
return get_registry_data()
|
|
209
|
+
except ImportError:
|
|
210
|
+
return {"plugins": {}}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
import uvicorn
|
|
215
|
+
|
|
216
|
+
host = os.environ.get("HOST", "0.0.0.0")
|
|
217
|
+
port = int(os.environ.get("PORT", "4000"))
|
|
218
|
+
uvicorn.run(app, host=host, port=port)
|