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 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)
@@ -0,0 +1,6 @@
1
+ """Observatory API.
2
+
3
+ FastAPI routers for Observatory backend functionality.
4
+ These replace the TanStack Start server functions and enable
5
+ Observatory to run as a pure SPA with a Python backend.
6
+ """