Merge pull request #3 from BlackRoad-OS/codex/setup-blackroad-api-as-public-api-gateway
Add FastAPI public API gateway scaffolding and deployment
This commit is contained in:
44
.github/workflows/deploy-api.yml
vendored
Normal file
44
.github/workflows/deploy-api.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Public API Gateway
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- dev
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
RAILWAY_ENVIRONMENT: ${{ github.ref == 'refs/heads/main' && 'prod' || github.ref == 'refs/heads/staging' && 'staging' || 'dev' }}
|
||||||
|
DEPLOY_URL: ${{ github.ref == 'refs/heads/main' && 'https://api.blackroad.systems' || github.ref == 'refs/heads/staging' && 'https://staging.api.blackroad.systems' || 'https://dev.api.blackroad.systems' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
- name: Install Railway CLI
|
||||||
|
run: npm install -g @railway/cli
|
||||||
|
|
||||||
|
- name: Deploy to Railway
|
||||||
|
env:
|
||||||
|
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
|
||||||
|
GIT_COMMIT: ${{ github.sha }}
|
||||||
|
run: |
|
||||||
|
railway login --token "$RAILWAY_TOKEN"
|
||||||
|
railway up --service public-api --project blackroad-core --environment "$RAILWAY_ENVIRONMENT" --detached
|
||||||
|
|
||||||
|
- name: Health check
|
||||||
|
run: |
|
||||||
|
curl --fail --retry 3 --retry-delay 5 "$DEPLOY_URL/health"
|
||||||
|
curl --fail --retry 3 --retry-delay 5 "$DEPLOY_URL/v1/health"
|
||||||
78
README.md
78
README.md
@@ -36,3 +36,81 @@ Sample tasks:
|
|||||||
* Route coverage expansion
|
* Route coverage expansion
|
||||||
* Agent protocol endpoints
|
* Agent protocol endpoints
|
||||||
* Pocket OS API handler
|
* Pocket OS API handler
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Repository overview
|
||||||
|
|
||||||
|
This repository implements the BlackRoad public API gateway using FastAPI. It provides:
|
||||||
|
|
||||||
|
- `/health` and `/version` liveness + build metadata.
|
||||||
|
- Versioned `/v1` surface with API key authentication by default.
|
||||||
|
- Proxy-style routes for core and agents upstreams with shared error handling.
|
||||||
|
- Consistent configuration via environment variables.
|
||||||
|
|
||||||
|
Project structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
app/
|
||||||
|
config.py # Environment-driven settings
|
||||||
|
main.py # FastAPI entrypoint
|
||||||
|
errors.py # Common error helpers
|
||||||
|
clients/ # Upstream HTTP clients
|
||||||
|
middleware/ # Request ID, auth, and error handling middleware
|
||||||
|
routes/ # Root and versioned routers
|
||||||
|
```
|
||||||
|
|
||||||
|
## Environment variables
|
||||||
|
|
||||||
|
| Variable | Required | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `NODE_ENV` | No (default `development`) | Deployment environment label. |
|
||||||
|
| `PUBLIC_API_URL` | Yes | External URL for this gateway. |
|
||||||
|
| `CORE_API_URL` | Yes | Upstream Core backend base URL. |
|
||||||
|
| `AGENTS_API_URL` | No | Upstream Agents API base URL. |
|
||||||
|
| `API_KEYS` | Yes | Comma-separated list of API keys authorized for `/v1` routes. |
|
||||||
|
| `RATE_LIMIT_WINDOW` | No | Optional rate limit window (seconds). |
|
||||||
|
| `RATE_LIMIT_MAX` | No | Optional rate limit max requests per window. |
|
||||||
|
| `GIT_COMMIT` | No | Commit SHA used for `/version`. |
|
||||||
|
| `BUILD_TIME` | No | Build timestamp used for `/version`. |
|
||||||
|
|
||||||
|
## Railway deployment
|
||||||
|
|
||||||
|
- **Railway project**: `blackroad-core`
|
||||||
|
- **Service name**: `public-api`
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
- **Build**: `pip install -r requirements.txt`
|
||||||
|
- **Start**: `uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}`
|
||||||
|
|
||||||
|
### Environment mappings
|
||||||
|
|
||||||
|
- **dev**
|
||||||
|
- `PUBLIC_API_URL` = dev Railway URL or `https://dev.api.blackroad.systems`
|
||||||
|
- `CORE_API_URL` = core dev Railway URL
|
||||||
|
- `AGENTS_API_URL` = agents dev Railway URL
|
||||||
|
- **staging**
|
||||||
|
- `PUBLIC_API_URL` = `https://staging.api.blackroad.systems`
|
||||||
|
- `CORE_API_URL` = `https://staging.core.blackroad.systems`
|
||||||
|
- `AGENTS_API_URL` = `https://staging.agents.blackroad.systems`
|
||||||
|
- **prod**
|
||||||
|
- `PUBLIC_API_URL` = `https://api.blackroad.systems`
|
||||||
|
- `CORE_API_URL` = `https://core.blackroad.systems`
|
||||||
|
- `AGENTS_API_URL` = `https://agents.blackroad.systems`
|
||||||
|
|
||||||
|
### DNS (Cloudflare)
|
||||||
|
|
||||||
|
- `api.blackroad.systems` → CNAME `public-api-prod.up.railway.app` (proxied)
|
||||||
|
- `staging.api.blackroad.systems` → CNAME `public-api-staging.up.railway.app` (proxied)
|
||||||
|
- `dev.api.blackroad.systems` → CNAME `public-api-dev.up.railway.app` (proxied)
|
||||||
|
|
||||||
|
### CI/CD
|
||||||
|
|
||||||
|
GitHub Actions workflow `.github/workflows/deploy-api.yml` deploys to Railway:
|
||||||
|
|
||||||
|
- `dev` branch → Railway `dev`
|
||||||
|
- `staging` branch → Railway `staging`
|
||||||
|
- `main` branch → Railway `prod`
|
||||||
|
|
||||||
|
After deployment, the workflow runs health checks against `/health` and `/v1/health`.
|
||||||
|
|||||||
2
app/__init__.py
Normal file
2
app/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
__all__ = ["__version__"]
|
||||||
|
__version__ = "0.1.0"
|
||||||
0
app/clients/__init__.py
Normal file
0
app/clients/__init__.py
Normal file
85
app/clients/upstream.py
Normal file
85
app/clients/upstream.py
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
from app.errors import UpstreamError
|
||||||
|
|
||||||
|
DEFAULT_TIMEOUT = 10.0
|
||||||
|
|
||||||
|
|
||||||
|
class UpstreamClient:
|
||||||
|
"""
|
||||||
|
A client for making HTTP requests to an upstream service.
|
||||||
|
|
||||||
|
Attributes:
|
||||||
|
name (str): The name of the upstream service.
|
||||||
|
base_url (Optional[str]): The base URL of the upstream service.
|
||||||
|
"""
|
||||||
|
def __init__(self, name: str, base_url: Optional[str]):
|
||||||
|
"""
|
||||||
|
Initialize an UpstreamClient.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name (str): The name of the upstream service.
|
||||||
|
base_url (Optional[str]): The base URL of the upstream service.
|
||||||
|
"""
|
||||||
|
self.name = name
|
||||||
|
self.base_url = base_url.rstrip("/") if base_url else None
|
||||||
|
|
||||||
|
async def request(self, method: str, path: str, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Make an HTTP request to the upstream service.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
method (str): The HTTP method (e.g., "GET", "POST").
|
||||||
|
path (str): The path to append to the base URL.
|
||||||
|
**kwargs: Additional arguments to pass to httpx.AsyncClient.request.
|
||||||
|
Common options include 'params', 'json', 'headers', etc.
|
||||||
|
'timeout' (float, optional): Request timeout in seconds (default: 10.0).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: The response data. If the response is JSON, returns the parsed JSON.
|
||||||
|
Otherwise, returns a dict with 'status_code' and 'content'.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
UpstreamError: If the upstream is not configured, times out, returns an error status,
|
||||||
|
or if the request fails for any reason.
|
||||||
|
"""
|
||||||
|
if not self.base_url:
|
||||||
|
raise UpstreamError(source=self.name, message=f"{self.name} upstream not configured", status_code=502)
|
||||||
|
|
||||||
|
url = f"{self.base_url}/{path.lstrip('/')}"
|
||||||
|
timeout = kwargs.pop("timeout", DEFAULT_TIMEOUT)
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
|
response = await client.request(method, url, **kwargs)
|
||||||
|
response.raise_for_status()
|
||||||
|
if response.headers.get("content-type", "").startswith("application/json"):
|
||||||
|
return response.json()
|
||||||
|
return {
|
||||||
|
"status_code": response.status_code,
|
||||||
|
"content": response.text,
|
||||||
|
"content_type": response.headers.get("content-type", ""),
|
||||||
|
}
|
||||||
|
except httpx.TimeoutException as exc:
|
||||||
|
raise UpstreamError(self.name, message=f"{self.name} upstream timeout", details={"path": path}) from exc
|
||||||
|
except httpx.HTTPStatusError as exc:
|
||||||
|
raise UpstreamError(
|
||||||
|
self.name,
|
||||||
|
status_code=exc.response.status_code,
|
||||||
|
message=f"{self.name} upstream returned {exc.response.status_code}",
|
||||||
|
details={"path": path},
|
||||||
|
)
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
raise UpstreamError(self.name, message=f"{self.name} upstream request failed", details={"path": path}) from exc
|
||||||
|
|
||||||
|
|
||||||
|
def get_core_client(core_api_url: Optional[str]) -> UpstreamClient:
|
||||||
|
return UpstreamClient("core", core_api_url)
|
||||||
|
|
||||||
|
|
||||||
|
def get_agents_client(agents_api_url: Optional[str]) -> UpstreamClient:
|
||||||
|
return UpstreamClient("agents", agents_api_url)
|
||||||
49
app/config.py
Normal file
49
app/config.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import AnyHttpUrl, Field, field_validator
|
||||||
|
from pydantic_settings import BaseSettings
|
||||||
|
|
||||||
|
|
||||||
|
class Settings(BaseSettings):
|
||||||
|
"""Application configuration loaded from environment variables."""
|
||||||
|
|
||||||
|
env: str = Field("development", alias="NODE_ENV")
|
||||||
|
public_api_url: AnyHttpUrl = Field(..., alias="PUBLIC_API_URL")
|
||||||
|
core_api_url: AnyHttpUrl = Field(..., alias="CORE_API_URL")
|
||||||
|
agents_api_url: Optional[AnyHttpUrl] = Field(None, alias="AGENTS_API_URL")
|
||||||
|
|
||||||
|
api_keys: List[str] = Field(default_factory=list, alias="API_KEYS")
|
||||||
|
app_version: str = "0.1.0"
|
||||||
|
build_time: str = Field(default_factory=lambda: datetime.utcnow().isoformat())
|
||||||
|
commit: Optional[str] = Field(None, alias="GIT_COMMIT")
|
||||||
|
|
||||||
|
model_config = {
|
||||||
|
"case_sensitive": False,
|
||||||
|
"populate_by_name": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
@field_validator("api_keys", mode="before")
|
||||||
|
@classmethod
|
||||||
|
def split_keys(cls, value: Optional[str]) -> List[str]:
|
||||||
|
if value in (None, ""):
|
||||||
|
return []
|
||||||
|
if isinstance(value, str):
|
||||||
|
return [key.strip() for key in value.split(",") if key.strip()]
|
||||||
|
return value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def core_configured(self) -> bool:
|
||||||
|
return bool(self.core_api_url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def agents_configured(self) -> bool:
|
||||||
|
return bool(self.agents_api_url)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache()
|
||||||
|
def get_settings() -> Settings:
|
||||||
|
return Settings()
|
||||||
26
app/errors.py
Normal file
26
app/errors.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
from fastapi import HTTPException
|
||||||
|
|
||||||
|
|
||||||
|
class UpstreamError(HTTPException):
|
||||||
|
def __init__(self, source: str, status_code: int = 502, message: str = "Upstream service error", details: Optional[Dict[str, Any]] = None):
|
||||||
|
self.source = source
|
||||||
|
self.details = details or {}
|
||||||
|
super().__init__(status_code=status_code, detail=message)
|
||||||
|
|
||||||
|
|
||||||
|
def build_error_response(code: str, message: str, request_id: Optional[str], details: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
response: Dict[str, Any] = {
|
||||||
|
"error": {
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if details:
|
||||||
|
response["error"]["details"] = details
|
||||||
|
if request_id:
|
||||||
|
response["error"]["requestId"] = request_id
|
||||||
|
return response
|
||||||
29
app/main.py
Normal file
29
app/main.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from app.middleware.errors import ErrorHandlerMiddleware
|
||||||
|
from app.middleware.request_id import RequestIdMiddleware
|
||||||
|
from app.routes import root
|
||||||
|
from app.routes.v1.router import router as v1_router
|
||||||
|
|
||||||
|
app = FastAPI(title="BlackRoad Public API Gateway", version="0.1.0")
|
||||||
|
|
||||||
|
app.add_middleware(ErrorHandlerMiddleware)
|
||||||
|
app.add_middleware(RequestIdMiddleware)
|
||||||
|
app.add_middleware(
|
||||||
|
CORSMiddleware,
|
||||||
|
allow_origins=["*"],
|
||||||
|
allow_credentials=True,
|
||||||
|
allow_methods=["*"],
|
||||||
|
allow_headers=["*"]
|
||||||
|
)
|
||||||
|
|
||||||
|
app.include_router(root.router)
|
||||||
|
app.include_router(v1_router)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/", include_in_schema=False)
|
||||||
|
async def index():
|
||||||
|
return {"message": "BlackRoad public API gateway", "docs": "/docs"}
|
||||||
0
app/middleware/__init__.py
Normal file
0
app/middleware/__init__.py
Normal file
29
app/middleware/auth.py
Normal file
29
app/middleware/auth.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from fastapi import Depends, Header, HTTPException, status
|
||||||
|
|
||||||
|
from app.config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_keys() -> List[str]:
|
||||||
|
return get_settings().api_keys
|
||||||
|
|
||||||
|
|
||||||
|
def api_key_auth(
|
||||||
|
x_api_key: Optional[str] = Header(None, alias="x-api-key"),
|
||||||
|
authorization: Optional[str] = Header(None),
|
||||||
|
api_keys: List[str] = Depends(get_api_keys),
|
||||||
|
):
|
||||||
|
if not api_keys:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="API key required")
|
||||||
|
|
||||||
|
provided_key = x_api_key
|
||||||
|
if not provided_key and authorization and authorization.startswith("Bearer "):
|
||||||
|
provided_key = authorization.split(" ", 1)[1]
|
||||||
|
|
||||||
|
if provided_key in api_keys:
|
||||||
|
return True
|
||||||
|
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid API key")
|
||||||
30
app/middleware/errors.py
Normal file
30
app/middleware/errors.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from fastapi.responses import JSONResponse
|
||||||
|
from starlette import status
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
from app.errors import UpstreamError, build_error_response
|
||||||
|
|
||||||
|
|
||||||
|
class ErrorHandlerMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
try:
|
||||||
|
return await call_next(request)
|
||||||
|
except UpstreamError as exc:
|
||||||
|
payload = build_error_response(
|
||||||
|
code="UPSTREAM_ERROR",
|
||||||
|
message=exc.detail,
|
||||||
|
details={"source": exc.source, **exc.details},
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
)
|
||||||
|
return JSONResponse(status_code=exc.status_code, content=payload)
|
||||||
|
except Exception as exc: # pylint: disable=broad-except
|
||||||
|
payload = build_error_response(
|
||||||
|
code="INTERNAL_ERROR",
|
||||||
|
message=str(exc) or "Internal server error",
|
||||||
|
details=None,
|
||||||
|
request_id=getattr(request.state, "request_id", None),
|
||||||
|
)
|
||||||
|
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=payload)
|
||||||
15
app/middleware/request_id.py
Normal file
15
app/middleware/request_id.py
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from typing import Callable
|
||||||
|
|
||||||
|
from fastapi import Request
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
|
||||||
|
|
||||||
|
class RequestIdMiddleware(BaseHTTPMiddleware):
|
||||||
|
async def dispatch(self, request: Request, call_next: Callable):
|
||||||
|
request.state.request_id = str(uuid.uuid4())
|
||||||
|
response = await call_next(request)
|
||||||
|
response.headers["X-Request-ID"] = request.state.request_id
|
||||||
|
return response
|
||||||
0
app/routes/__init__.py
Normal file
0
app/routes/__init__.py
Normal file
30
app/routes/root.py
Normal file
30
app/routes/root.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app.config import Settings, get_settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health", summary="Gateway health check")
|
||||||
|
async def health(settings: Settings = Depends(get_settings)) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"environment": settings.env,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/version", summary="Gateway version info")
|
||||||
|
async def version(settings: Settings = Depends(get_settings)) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"service": "public-api",
|
||||||
|
"appVersion": settings.app_version,
|
||||||
|
"commit": settings.commit,
|
||||||
|
"buildTime": settings.build_time,
|
||||||
|
"environment": settings.env,
|
||||||
|
}
|
||||||
0
app/routes/v1/__init__.py
Normal file
0
app/routes/v1/__init__.py
Normal file
17
app/routes/v1/agents.py
Normal file
17
app/routes/v1/agents.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
|
||||||
|
from app.clients.upstream import UpstreamClient, get_agents_client
|
||||||
|
from app.config import Settings, get_settings
|
||||||
|
from app.middleware.auth import api_key_auth
|
||||||
|
|
||||||
|
router = APIRouter(dependencies=[Depends(api_key_auth)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/agents/health", summary="Proxy agents health", status_code=status.HTTP_200_OK)
|
||||||
|
async def agents_health(settings: Settings = Depends(get_settings)) -> Dict[str, Any]:
|
||||||
|
agents_client: UpstreamClient = get_agents_client(settings.agents_api_url)
|
||||||
|
return await agents_client.request("GET", "/health")
|
||||||
19
app/routes/v1/core.py
Normal file
19
app/routes/v1/core.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, status
|
||||||
|
|
||||||
|
from app.clients.upstream import UpstreamClient, get_core_client
|
||||||
|
from app.config import Settings, get_settings
|
||||||
|
from app.middleware.auth import api_key_auth
|
||||||
|
|
||||||
|
router = APIRouter(dependencies=[Depends(api_key_auth)])
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/core/health", summary="Proxy core health", status_code=status.HTTP_200_OK)
|
||||||
|
async def core_health(
|
||||||
|
settings: Settings = Depends(get_settings),
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
core_client: UpstreamClient = get_core_client(settings.core_api_url)
|
||||||
|
return await core_client.request("GET", "/health")
|
||||||
23
app/routes/v1/health.py
Normal file
23
app/routes/v1/health.py
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app.config import Settings, get_settings
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health", summary="Versioned health check")
|
||||||
|
async def health(settings: Settings = Depends(get_settings)) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"timestamp": datetime.utcnow().isoformat(),
|
||||||
|
"environment": settings.env,
|
||||||
|
"upstreams": {
|
||||||
|
"coreConfigured": settings.core_configured,
|
||||||
|
"agentsConfigured": settings.agents_configured,
|
||||||
|
},
|
||||||
|
}
|
||||||
10
app/routes/v1/router.py
Normal file
10
app/routes/v1/router.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from app.routes.v1 import agents, core, health
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/v1")
|
||||||
|
router.include_router(health.router)
|
||||||
|
router.include_router(core.router)
|
||||||
|
router.include_router(agents.router)
|
||||||
20
railway.json
Normal file
20
railway.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://railway.app/railway.schema.json",
|
||||||
|
"name": "public-api",
|
||||||
|
"project": "blackroad-core",
|
||||||
|
"service": {
|
||||||
|
"name": "public-api",
|
||||||
|
"envVar": "PORT",
|
||||||
|
"startCommand": "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}",
|
||||||
|
"buildCommand": "pip install -r requirements.txt"
|
||||||
|
},
|
||||||
|
"variables": [
|
||||||
|
"NODE_ENV",
|
||||||
|
"PUBLIC_API_URL",
|
||||||
|
"CORE_API_URL",
|
||||||
|
"AGENTS_API_URL",
|
||||||
|
"API_KEYS",
|
||||||
|
"RATE_LIMIT_WINDOW",
|
||||||
|
"RATE_LIMIT_MAX"
|
||||||
|
]
|
||||||
|
}
|
||||||
5
requirements.txt
Normal file
5
requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
fastapi>=0.115.0
|
||||||
|
uvicorn[standard]>=0.30.0
|
||||||
|
httpx>=0.27.0
|
||||||
|
pydantic>=2.6.0
|
||||||
|
pydantic-settings>=2.2.1
|
||||||
Reference in New Issue
Block a user