Skip to content

Commit a2e9f06

Browse files
committed
feat: add full server implementation (core, app, adapters, metrics, business routes)
- Added reliapi/core: cache, circuit_breaker, idempotency, retry, rate_limiter, key_pool - Added reliapi/app: FastAPI main app with /proxy/http and /proxy/llm endpoints - Added reliapi/adapters: LLM providers (OpenAI, Anthropic, Mistral) - Added reliapi/config: configuration loader - Added reliapi/metrics: Prometheus metrics - Added business routes: paddle, onboarding, analytics, calculators, dashboard - Consolidated from Docker image kikudoc/reliapi:latest
1 parent 928d49c commit a2e9f06

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

68 files changed

+15603
-29
lines changed

__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""ReliAPI - Universal Resilient API Gateway."""
2+
3+
__version__ = "0.1.0"
4+
5+

adapters/http_generic/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""HTTP Generic Adapter for ReliAPI."""
2+
3+
from reliapi.adapters.http_generic.service import HTTPGenericService
4+
5+
__all__ = ["HTTPGenericService"]
6+
7+

adapters/http_generic/service.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
"""HTTP Generic Service - Universal REST/SSE proxy."""
2+
import asyncio
3+
import hashlib
4+
import json
5+
import time
6+
from typing import Any, Dict, Optional
7+
8+
from fastapi import Request, Response
9+
from fastapi.responses import StreamingResponse
10+
11+
from reliapi.core.cache import Cache
12+
from reliapi.core.circuit_breaker import CircuitBreaker
13+
from reliapi.core.http_client import UpstreamHTTPClient
14+
from reliapi.core.idempotency import IdempotencyManager
15+
from reliapi.core.retry import RetryMatrix
16+
17+
18+
class HTTPGenericService:
19+
"""Universal HTTP proxy service with resilience features."""
20+
21+
def __init__(
22+
self,
23+
redis_url: str,
24+
config: Dict[str, Any],
25+
):
26+
"""
27+
Args:
28+
redis_url: Redis connection URL
29+
config: Route configuration
30+
"""
31+
self.config = config
32+
self.upstream_config = config.get("upstream_config", {})
33+
34+
# Initialize core components
35+
self.cache = Cache(redis_url, key_prefix="reliapi")
36+
self.idempotency = IdempotencyManager(redis_url, key_prefix="reliapi")
37+
38+
# Circuit breaker
39+
cb_config = self.upstream_config.get("circuit_breaker", {})
40+
self.circuit_breaker = CircuitBreaker(
41+
failures_to_open=cb_config.get("failures_to_open", 3),
42+
open_ttl_s=cb_config.get("open_ttl_s", 60),
43+
)
44+
45+
# Retry matrix
46+
retry_config = self.upstream_config.get("retry_matrix", {})
47+
retry_matrix = {}
48+
for error_class, policy in retry_config.items():
49+
retry_matrix[error_class] = RetryMatrix(
50+
attempts=policy.get("attempts", 3),
51+
backoff=policy.get("backoff", "exp-jitter"),
52+
base_s=policy.get("base_s", 1.0),
53+
)
54+
55+
# HTTP client
56+
self.client = UpstreamHTTPClient(
57+
base_url=self.upstream_config["base_url"],
58+
timeout_s=self.upstream_config.get("timeout_s", 30.0),
59+
retry_matrix=retry_matrix,
60+
circuit_breaker=self.circuit_breaker,
61+
auth=self.upstream_config.get("auth"),
62+
)
63+
64+
async def proxy_request(
65+
self,
66+
request: Request,
67+
route_config: Dict[str, Any],
68+
) -> Response:
69+
"""
70+
Proxy HTTP request with resilience features.
71+
72+
Args:
73+
request: FastAPI request
74+
route_config: Route configuration
75+
76+
Returns:
77+
HTTP response
78+
"""
79+
method = request.method
80+
path = request.url.path
81+
headers = dict(request.headers)
82+
body = await request.body()
83+
84+
# Generate request ID
85+
request_id = f"req_{int(time.time())}_{hashlib.md5(f'{method}:{path}:{body}'.encode()).hexdigest()[:8]}"
86+
87+
# Check cache for GET/HEAD
88+
cache_policy = route_config.get("cache_policy", {})
89+
if cache_policy.get("enabled") and method.upper() in cache_policy.get("methods", ["GET", "HEAD"]):
90+
cached = self.cache.get(method, path, headers, body)
91+
if cached:
92+
return Response(
93+
content=cached.get("body", ""),
94+
status_code=cached.get("status_code", 200),
95+
headers=cached.get("headers", {}),
96+
)
97+
98+
# Check idempotency for POST/PUT/PATCH
99+
idempotency_policy = route_config.get("idempotency", {})
100+
idempotency_key = None
101+
if idempotency_policy.get("enabled") and method.upper() in idempotency_policy.get("for_methods", ["POST"]):
102+
idempotency_header = idempotency_policy.get("header", "Idempotency-Key")
103+
idempotency_key = headers.get(idempotency_header)
104+
105+
if idempotency_key:
106+
# Check for existing result
107+
existing_result = self.idempotency.get_result(idempotency_key)
108+
if existing_result:
109+
return Response(
110+
content=json.dumps(existing_result.get("body", {})),
111+
status_code=existing_result.get("status_code", 200),
112+
headers={"Content-Type": "application/json"},
113+
)
114+
115+
# Register request
116+
is_new, existing_id, existing_hash = self.idempotency.register_request(
117+
idempotency_key, method, path, headers, body, request_id
118+
)
119+
120+
if not is_new:
121+
# Request in progress or conflict
122+
if existing_hash != self.idempotency.make_request_hash(method, path, headers, body):
123+
return Response(
124+
content=json.dumps({"error": "Idempotency key conflict"}),
125+
status_code=409,
126+
headers={"Content-Type": "application/json"},
127+
)
128+
129+
# Wait for existing request (simplified - in production use proper coalescing)
130+
await asyncio.sleep(0.1)
131+
existing_result = self.idempotency.get_result(idempotency_key)
132+
if existing_result:
133+
return Response(
134+
content=json.dumps(existing_result.get("body", {})),
135+
status_code=existing_result.get("status_code", 200),
136+
headers={"Content-Type": "application/json"},
137+
)
138+
139+
self.idempotency.mark_in_progress(idempotency_key)
140+
141+
# Make upstream request
142+
try:
143+
start_time = time.time()
144+
upstream_response = await self.client.request(
145+
method=method,
146+
path=path,
147+
headers=headers,
148+
body=body if body else None,
149+
params=dict(request.query_params),
150+
)
151+
152+
latency_ms = int((time.time() - start_time) * 1000)
153+
154+
# Read response body
155+
response_body = await upstream_response.aread()
156+
response_status = upstream_response.status_code
157+
response_headers = dict(upstream_response.headers)
158+
159+
# Normalize response
160+
normalized = {
161+
"status_code": response_status,
162+
"body": response_body.decode() if response_body else "",
163+
"headers": response_headers,
164+
"latency_ms": latency_ms,
165+
}
166+
167+
# Store in cache if applicable
168+
if cache_policy.get("enabled") and method.upper() in cache_policy.get("methods", ["GET", "HEAD"]):
169+
if response_status < 400: # Only cache successful responses
170+
ttl = cache_policy.get("ttl_s", 3600)
171+
self.cache.set(method, path, headers, body, normalized, ttl)
172+
173+
# Store idempotency result
174+
if idempotency_key:
175+
self.idempotency.store_result(
176+
idempotency_key,
177+
{
178+
"status_code": response_status,
179+
"body": json.loads(response_body.decode()) if response_body else {},
180+
},
181+
)
182+
self.idempotency.clear_in_progress(idempotency_key)
183+
184+
# Return response
185+
return Response(
186+
content=response_body,
187+
status_code=response_status,
188+
headers=response_headers,
189+
)
190+
191+
except Exception as e:
192+
# Clear in-progress on error
193+
if idempotency_key:
194+
self.idempotency.clear_in_progress(idempotency_key)
195+
196+
# Return error response
197+
return Response(
198+
content=json.dumps({"error": str(e)}),
199+
status_code=502,
200+
headers={"Content-Type": "application/json"},
201+
)
202+
203+
async def close(self):
204+
"""Close service and cleanup."""
205+
await self.client.close()
206+
207+

adapters/llm/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
"""LLM adapters for ReliAPI."""
2+

0 commit comments

Comments
 (0)