Initial upload: API Mock CLI v0.1.0
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / type-check (push) Has been cancelled
CI / build (push) Has been cancelled
Some checks failed
CI / test (3.10) (push) Has been cancelled
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.9) (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / type-check (push) Has been cancelled
CI / build (push) Has been cancelled
This commit is contained in:
130
src/core/server.py
Normal file
130
src/core/server.py
Normal file
@@ -0,0 +1,130 @@
|
||||
import asyncio
|
||||
import signal
|
||||
import sys
|
||||
from typing import Optional, Dict, Any
|
||||
from contextlib import asynccontextmanager
|
||||
import uvicorn
|
||||
from fastapi import FastAPI, Request, Response
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from src.core.matcher import Matcher
|
||||
from src.core.templating import TemplatingEngine
|
||||
from src.core.validator import Validator
|
||||
from src.core.tunnel import Tunnel, TunnelError
|
||||
from src.models.endpoint import Endpoint
|
||||
from src.models.config import ServerConfig
|
||||
from src.models.request import RequestValidation
|
||||
|
||||
|
||||
class MockServer:
|
||||
def __init__(self, config: ServerConfig):
|
||||
self.config = config
|
||||
self.matcher = Matcher()
|
||||
self.templating = TemplatingEngine()
|
||||
self.tunnel: Optional[Tunnel] = None
|
||||
self._server_task: Optional[asyncio.Task] = None
|
||||
self._shutdown_event = asyncio.Event()
|
||||
|
||||
def register_endpoint(self, endpoint: Endpoint) -> None:
|
||||
if endpoint.enabled:
|
||||
self.matcher.add_endpoint(endpoint)
|
||||
|
||||
def register_endpoints(self, endpoints: list[Endpoint]) -> None:
|
||||
for endpoint in endpoints:
|
||||
self.register_endpoint(endpoint)
|
||||
|
||||
def _build_response(self, endpoint: Endpoint, context: Dict[str, Any]) -> Dict[str, Any]:
|
||||
response_config = endpoint.response
|
||||
status_code = response_config.get("status_code", 200)
|
||||
headers = response_config.get("headers", {})
|
||||
body = response_config.get("body")
|
||||
if body:
|
||||
body = self.templating.render_dict(body, context)
|
||||
return {"status_code": status_code, "headers": headers, "body": body}
|
||||
|
||||
async def _handle_request(self, request: Request, endpoint: Endpoint, path_params: Dict[str, str]) -> Response:
|
||||
try:
|
||||
body = None
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
body = await request.json()
|
||||
query_params = dict(request.query_params)
|
||||
headers = dict(request.headers)
|
||||
headers.pop("host", None)
|
||||
context = self.templating.build_context(
|
||||
path_params=path_params,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
body=body
|
||||
)
|
||||
if endpoint.validators:
|
||||
validation_rules = RequestValidation(**endpoint.validators)
|
||||
validator = Validator(validation_rules)
|
||||
valid, errors = validator.validate(body=body, query=query_params, headers=headers, path=path_params)
|
||||
if not valid:
|
||||
return JSONResponse(
|
||||
status_code=400,
|
||||
content={"validation_error": validator.format_errors(errors)}
|
||||
)
|
||||
response_data = self._build_response(endpoint, context)
|
||||
return JSONResponse(
|
||||
status_code=response_data["status_code"],
|
||||
content=response_data["body"],
|
||||
headers=response_data["headers"]
|
||||
)
|
||||
except Exception as e:
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"error": str(e)}
|
||||
)
|
||||
|
||||
def _create_app(self) -> FastAPI:
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
if not self.config.offline and self.config.tunnel:
|
||||
try:
|
||||
tunnel = Tunnel(self.config.ngrok_authtoken)
|
||||
public_url = tunnel.start(self.config.port)
|
||||
print(f"Public URL: {public_url}")
|
||||
self.tunnel = tunnel
|
||||
except TunnelError as e:
|
||||
print(f"Tunnel creation failed: {e}. Running in offline mode.")
|
||||
yield
|
||||
if self.tunnel:
|
||||
self.tunnel.stop()
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=self.config.cors_origins or ["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
async def dynamic_handler(request: Request):
|
||||
path = request.url.path
|
||||
method = request.method
|
||||
match = self.matcher.match(path, method)
|
||||
if match:
|
||||
endpoint, path_params = match
|
||||
return await self._handle_request(request, endpoint, path_params)
|
||||
return JSONResponse(status_code=404, content={"error": "Not Found"})
|
||||
for method in ["GET", "POST", "PUT", "DELETE", "PATCH"]:
|
||||
app.add_api_route("/{path:path}", dynamic_handler, methods=[method])
|
||||
return app
|
||||
|
||||
async def start(self) -> None:
|
||||
app = self._create_app()
|
||||
config = uvicorn.Config(
|
||||
app,
|
||||
host=self.config.host,
|
||||
port=self.config.port,
|
||||
reload=self.config.reload,
|
||||
log_level=self.config.log_level
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
await server.serve()
|
||||
|
||||
def run(self) -> None:
|
||||
try:
|
||||
asyncio.run(self.start())
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down...")
|
||||
Reference in New Issue
Block a user