eval-hub-sdk 0.1.0a0__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.
- eval_hub_sdk-0.1.0a0.dist-info/METADATA +711 -0
- eval_hub_sdk-0.1.0a0.dist-info/RECORD +27 -0
- eval_hub_sdk-0.1.0a0.dist-info/WHEEL +5 -0
- eval_hub_sdk-0.1.0a0.dist-info/entry_points.txt +2 -0
- eval_hub_sdk-0.1.0a0.dist-info/licenses/LICENSE +201 -0
- eval_hub_sdk-0.1.0a0.dist-info/top_level.txt +1 -0
- evalhub/__init__.py +84 -0
- evalhub/adapter/__init__.py +28 -0
- evalhub/adapter/api/__init__.py +6 -0
- evalhub/adapter/api/endpoints.py +342 -0
- evalhub/adapter/api/router.py +135 -0
- evalhub/adapter/cli.py +331 -0
- evalhub/adapter/client/__init__.py +6 -0
- evalhub/adapter/client/adapter_client.py +418 -0
- evalhub/adapter/client/discovery.py +275 -0
- evalhub/adapter/models/__init__.py +9 -0
- evalhub/adapter/models/framework.py +404 -0
- evalhub/adapter/oci/__init__.py +5 -0
- evalhub/adapter/oci/persister.py +76 -0
- evalhub/adapter/server/__init__.py +5 -0
- evalhub/adapter/server/app.py +157 -0
- evalhub/cli.py +331 -0
- evalhub/models/__init__.py +32 -0
- evalhub/models/api.py +388 -0
- evalhub/py.typed +0 -0
- evalhub/utils/__init__.py +5 -0
- evalhub/utils/logging.py +41 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
"""Standard API endpoints for framework adapters."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import AsyncGenerator
|
|
5
|
+
|
|
6
|
+
from fastapi import APIRouter, BackgroundTasks, HTTPException
|
|
7
|
+
from fastapi.responses import StreamingResponse
|
|
8
|
+
|
|
9
|
+
from ...models.api import (
|
|
10
|
+
BenchmarkInfo,
|
|
11
|
+
EvaluationJob,
|
|
12
|
+
EvaluationRequest,
|
|
13
|
+
EvaluationResponse,
|
|
14
|
+
FrameworkInfo,
|
|
15
|
+
HealthResponse,
|
|
16
|
+
JobStatus,
|
|
17
|
+
OCICoordinate,
|
|
18
|
+
PersistResponse,
|
|
19
|
+
)
|
|
20
|
+
from ..models.framework import FrameworkAdapter
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def create_adapter_api(adapter: FrameworkAdapter) -> APIRouter:
|
|
26
|
+
"""Create FastAPI router with standard endpoints for a framework adapter.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
adapter: The framework adapter instance
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
APIRouter: Router with standard endpoints
|
|
33
|
+
"""
|
|
34
|
+
router = APIRouter()
|
|
35
|
+
|
|
36
|
+
@router.get("/health", response_model=HealthResponse, tags=["Health"])
|
|
37
|
+
@router.options("/health", tags=["Health"])
|
|
38
|
+
async def health_check() -> HealthResponse:
|
|
39
|
+
"""Check the health of the framework adapter."""
|
|
40
|
+
try:
|
|
41
|
+
return await adapter.health_check()
|
|
42
|
+
except Exception as e:
|
|
43
|
+
logger.exception("Health check failed")
|
|
44
|
+
raise HTTPException(
|
|
45
|
+
status_code=503, detail=f"Health check failed: {str(e)}"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@router.get("/info", response_model=FrameworkInfo, tags=["Info"])
|
|
49
|
+
async def get_framework_info() -> FrameworkInfo:
|
|
50
|
+
"""Get information about the framework adapter."""
|
|
51
|
+
try:
|
|
52
|
+
return await adapter.get_framework_info()
|
|
53
|
+
except Exception as e:
|
|
54
|
+
logger.exception("Failed to get framework info")
|
|
55
|
+
raise HTTPException(
|
|
56
|
+
status_code=500, detail=f"Failed to get framework info: {str(e)}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
@router.get("/benchmarks", response_model=list[BenchmarkInfo], tags=["Benchmarks"])
|
|
60
|
+
async def list_benchmarks() -> list[BenchmarkInfo]:
|
|
61
|
+
"""List all available benchmarks."""
|
|
62
|
+
try:
|
|
63
|
+
return await adapter.list_benchmarks()
|
|
64
|
+
except Exception as e:
|
|
65
|
+
logger.exception("Failed to list benchmarks")
|
|
66
|
+
raise HTTPException(
|
|
67
|
+
status_code=500, detail=f"Failed to list benchmarks: {str(e)}"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
@router.get(
|
|
71
|
+
"/benchmarks/{benchmark_id}", response_model=BenchmarkInfo, tags=["Benchmarks"]
|
|
72
|
+
)
|
|
73
|
+
async def get_benchmark_info(benchmark_id: str) -> BenchmarkInfo:
|
|
74
|
+
"""Get detailed information about a specific benchmark."""
|
|
75
|
+
try:
|
|
76
|
+
benchmark_info = await adapter.get_benchmark_info(benchmark_id)
|
|
77
|
+
if not benchmark_info:
|
|
78
|
+
raise HTTPException(
|
|
79
|
+
status_code=404, detail=f"Benchmark '{benchmark_id}' not found"
|
|
80
|
+
)
|
|
81
|
+
return benchmark_info
|
|
82
|
+
except HTTPException:
|
|
83
|
+
raise
|
|
84
|
+
except Exception as e:
|
|
85
|
+
logger.exception(f"Failed to get benchmark info for {benchmark_id}")
|
|
86
|
+
raise HTTPException(
|
|
87
|
+
status_code=500, detail=f"Failed to get benchmark info: {str(e)}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
@router.post(
|
|
91
|
+
"/evaluations",
|
|
92
|
+
response_model=EvaluationJob,
|
|
93
|
+
status_code=201,
|
|
94
|
+
tags=["Evaluations"],
|
|
95
|
+
)
|
|
96
|
+
async def submit_evaluation(
|
|
97
|
+
request: EvaluationRequest, background_tasks: BackgroundTasks
|
|
98
|
+
) -> EvaluationJob:
|
|
99
|
+
"""Submit an evaluation job."""
|
|
100
|
+
try:
|
|
101
|
+
# Validate the request
|
|
102
|
+
benchmark_info = await adapter.get_benchmark_info(request.benchmark_id)
|
|
103
|
+
if not benchmark_info:
|
|
104
|
+
raise HTTPException(
|
|
105
|
+
status_code=404,
|
|
106
|
+
detail=f"Benchmark '{request.benchmark_id}' not found",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Submit the evaluation
|
|
110
|
+
job = await adapter.submit_evaluation(request)
|
|
111
|
+
|
|
112
|
+
logger.info(
|
|
113
|
+
f"Submitted evaluation job {job.job_id} for benchmark {request.benchmark_id}"
|
|
114
|
+
)
|
|
115
|
+
return job
|
|
116
|
+
|
|
117
|
+
except HTTPException:
|
|
118
|
+
raise
|
|
119
|
+
except ValueError as e:
|
|
120
|
+
raise HTTPException(
|
|
121
|
+
status_code=400, detail=f"Invalid evaluation request: {str(e)}"
|
|
122
|
+
)
|
|
123
|
+
except Exception as e:
|
|
124
|
+
logger.exception("Failed to submit evaluation")
|
|
125
|
+
raise HTTPException(
|
|
126
|
+
status_code=500, detail=f"Failed to submit evaluation: {str(e)}"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
@router.get(
|
|
130
|
+
"/evaluations/{job_id}", response_model=EvaluationJob, tags=["Evaluations"]
|
|
131
|
+
)
|
|
132
|
+
async def get_job_status(job_id: str) -> EvaluationJob:
|
|
133
|
+
"""Get the status of an evaluation job."""
|
|
134
|
+
try:
|
|
135
|
+
job = await adapter.get_job_status(job_id)
|
|
136
|
+
if not job:
|
|
137
|
+
raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found")
|
|
138
|
+
return job
|
|
139
|
+
except HTTPException:
|
|
140
|
+
raise
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.exception(f"Failed to get job status for {job_id}")
|
|
143
|
+
raise HTTPException(
|
|
144
|
+
status_code=500, detail=f"Failed to get job status: {str(e)}"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
@router.get(
|
|
148
|
+
"/evaluations/{job_id}/results",
|
|
149
|
+
response_model=EvaluationResponse,
|
|
150
|
+
tags=["Evaluations"],
|
|
151
|
+
)
|
|
152
|
+
async def get_evaluation_results(job_id: str) -> EvaluationResponse:
|
|
153
|
+
"""Get the results of a completed evaluation."""
|
|
154
|
+
try:
|
|
155
|
+
# First check if job exists
|
|
156
|
+
job = await adapter.get_job_status(job_id)
|
|
157
|
+
if not job:
|
|
158
|
+
raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found")
|
|
159
|
+
|
|
160
|
+
# Check if results are available
|
|
161
|
+
if job.status == JobStatus.PENDING:
|
|
162
|
+
raise HTTPException(
|
|
163
|
+
status_code=202, detail=f"Job '{job_id}' is still pending"
|
|
164
|
+
)
|
|
165
|
+
elif job.status == JobStatus.RUNNING:
|
|
166
|
+
raise HTTPException(
|
|
167
|
+
status_code=202, detail=f"Job '{job_id}' is still running"
|
|
168
|
+
)
|
|
169
|
+
elif job.status == JobStatus.FAILED:
|
|
170
|
+
raise HTTPException(
|
|
171
|
+
status_code=422,
|
|
172
|
+
detail=f"Job '{job_id}' failed: {job.error_message}",
|
|
173
|
+
)
|
|
174
|
+
elif job.status == JobStatus.CANCELLED:
|
|
175
|
+
raise HTTPException(
|
|
176
|
+
status_code=410, detail=f"Job '{job_id}' was cancelled"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Get results
|
|
180
|
+
results = await adapter.get_evaluation_results(job_id)
|
|
181
|
+
if not results:
|
|
182
|
+
raise HTTPException(
|
|
183
|
+
status_code=404, detail=f"Results for job '{job_id}' not found"
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
return results
|
|
187
|
+
|
|
188
|
+
except HTTPException:
|
|
189
|
+
raise
|
|
190
|
+
except Exception as e:
|
|
191
|
+
logger.exception(f"Failed to get evaluation results for {job_id}")
|
|
192
|
+
raise HTTPException(
|
|
193
|
+
status_code=500, detail=f"Failed to get evaluation results: {str(e)}"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@router.delete("/evaluations/{job_id}", tags=["Evaluations"])
|
|
197
|
+
async def cancel_job(job_id: str) -> dict[str, bool | str]:
|
|
198
|
+
"""Cancel an evaluation job."""
|
|
199
|
+
try:
|
|
200
|
+
success = await adapter.cancel_job(job_id)
|
|
201
|
+
if not success:
|
|
202
|
+
# Check if job exists
|
|
203
|
+
job = await adapter.get_job_status(job_id)
|
|
204
|
+
if not job:
|
|
205
|
+
raise HTTPException(
|
|
206
|
+
status_code=404, detail=f"Job '{job_id}' not found"
|
|
207
|
+
)
|
|
208
|
+
else:
|
|
209
|
+
raise HTTPException(
|
|
210
|
+
status_code=409,
|
|
211
|
+
detail=f"Job '{job_id}' cannot be cancelled (status: {job.status})",
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
"success": True,
|
|
216
|
+
"message": f"Job '{job_id}' cancelled successfully",
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
except HTTPException:
|
|
220
|
+
raise
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.exception(f"Failed to cancel job {job_id}")
|
|
223
|
+
raise HTTPException(
|
|
224
|
+
status_code=500, detail=f"Failed to cancel job: {str(e)}"
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
@router.get(
|
|
228
|
+
"/evaluations", response_model=list[EvaluationJob], tags=["Evaluations"]
|
|
229
|
+
)
|
|
230
|
+
async def list_jobs(
|
|
231
|
+
status: JobStatus | None = None, limit: int | None = None
|
|
232
|
+
) -> list[EvaluationJob]:
|
|
233
|
+
"""List evaluation jobs, optionally filtered by status."""
|
|
234
|
+
try:
|
|
235
|
+
jobs = await adapter.list_active_jobs()
|
|
236
|
+
|
|
237
|
+
# Filter by status if specified
|
|
238
|
+
if status:
|
|
239
|
+
jobs = [job for job in jobs if job.status == status]
|
|
240
|
+
|
|
241
|
+
# Apply limit if specified
|
|
242
|
+
if limit and limit > 0:
|
|
243
|
+
jobs = jobs[:limit]
|
|
244
|
+
|
|
245
|
+
return jobs
|
|
246
|
+
|
|
247
|
+
except Exception as e:
|
|
248
|
+
logger.exception("Failed to list jobs")
|
|
249
|
+
raise HTTPException(
|
|
250
|
+
status_code=500, detail=f"Failed to list jobs: {str(e)}"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
@router.get(
|
|
254
|
+
"/evaluations/{job_id}/stream",
|
|
255
|
+
response_class=StreamingResponse,
|
|
256
|
+
tags=["Evaluations"],
|
|
257
|
+
)
|
|
258
|
+
async def stream_job_updates(job_id: str) -> StreamingResponse:
|
|
259
|
+
"""Stream real-time updates for an evaluation job."""
|
|
260
|
+
try:
|
|
261
|
+
# Check if job exists
|
|
262
|
+
job = await adapter.get_job_status(job_id)
|
|
263
|
+
if not job:
|
|
264
|
+
raise HTTPException(status_code=404, detail=f"Job '{job_id}' not found")
|
|
265
|
+
|
|
266
|
+
async def event_stream() -> AsyncGenerator[str, None]:
|
|
267
|
+
"""Generate Server-Sent Events for job updates."""
|
|
268
|
+
async for updated_job in adapter.stream_job_updates(job_id):
|
|
269
|
+
# Format as Server-Sent Event
|
|
270
|
+
yield f"data: {updated_job.model_dump_json()}\n\n"
|
|
271
|
+
|
|
272
|
+
# Stop streaming when job is complete
|
|
273
|
+
if updated_job.status in [
|
|
274
|
+
JobStatus.COMPLETED,
|
|
275
|
+
JobStatus.FAILED,
|
|
276
|
+
JobStatus.CANCELLED,
|
|
277
|
+
]:
|
|
278
|
+
break
|
|
279
|
+
|
|
280
|
+
return StreamingResponse(
|
|
281
|
+
event_stream(),
|
|
282
|
+
media_type="text/event-stream",
|
|
283
|
+
headers={
|
|
284
|
+
"Cache-Control": "no-cache",
|
|
285
|
+
"Connection": "keep-alive",
|
|
286
|
+
},
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
except HTTPException:
|
|
290
|
+
raise
|
|
291
|
+
except Exception as e:
|
|
292
|
+
logger.exception(f"Failed to stream job updates for {job_id}")
|
|
293
|
+
raise HTTPException(
|
|
294
|
+
status_code=500, detail=f"Failed to stream job updates: {str(e)}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
@router.post(
|
|
298
|
+
"/evaluations/{job_id}/persist",
|
|
299
|
+
response_model=PersistResponse,
|
|
300
|
+
tags=["Evaluations"],
|
|
301
|
+
)
|
|
302
|
+
async def persist_job_files(
|
|
303
|
+
job_id: str, coordinate: OCICoordinate
|
|
304
|
+
) -> PersistResponse:
|
|
305
|
+
"""Persist job files as OCI artifact.
|
|
306
|
+
|
|
307
|
+
Manually trigger OCI artifact persistence for completed job files.
|
|
308
|
+
|
|
309
|
+
Args:
|
|
310
|
+
job_id: The job identifier
|
|
311
|
+
coordinate: OCI coordinates (reference and optional subject)
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
PersistResponse: Persistence status and artifact information
|
|
315
|
+
|
|
316
|
+
Raises:
|
|
317
|
+
HTTPException: If job not found, not completed, or has no files to persist
|
|
318
|
+
"""
|
|
319
|
+
try:
|
|
320
|
+
result = await adapter.persist_job_files_oci(job_id, coordinate)
|
|
321
|
+
if result is None:
|
|
322
|
+
raise HTTPException(
|
|
323
|
+
status_code=404,
|
|
324
|
+
detail=f"Job {job_id} has no files to persist",
|
|
325
|
+
)
|
|
326
|
+
return result
|
|
327
|
+
except ValueError as e:
|
|
328
|
+
error_msg = str(e)
|
|
329
|
+
# Job not found -> 404, job not completed -> 409
|
|
330
|
+
if "not found" in error_msg:
|
|
331
|
+
raise HTTPException(status_code=404, detail=error_msg)
|
|
332
|
+
else:
|
|
333
|
+
raise HTTPException(status_code=409, detail=error_msg)
|
|
334
|
+
except HTTPException:
|
|
335
|
+
raise
|
|
336
|
+
except Exception as e:
|
|
337
|
+
logger.exception(f"Failed to persist files for job {job_id}")
|
|
338
|
+
raise HTTPException(
|
|
339
|
+
status_code=500, detail=f"Failed to persist files: {str(e)}"
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
return router
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""API router utilities for framework adapters."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from fastapi import FastAPI, Request
|
|
7
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
8
|
+
from fastapi.responses import JSONResponse
|
|
9
|
+
|
|
10
|
+
from ..models.framework import FrameworkAdapter
|
|
11
|
+
from .endpoints import create_adapter_api
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class AdapterAPIRouter:
|
|
17
|
+
"""Router for creating standardized API servers for framework adapters."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, adapter: FrameworkAdapter):
|
|
20
|
+
"""Initialize the router with a framework adapter.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
adapter: The framework adapter to expose via API
|
|
24
|
+
"""
|
|
25
|
+
self.adapter = adapter
|
|
26
|
+
self.app = FastAPI(
|
|
27
|
+
title="EvalHub Framework Adapter",
|
|
28
|
+
description=f"{adapter.config.adapter_name} - API for {adapter.config.framework_id} framework adapter",
|
|
29
|
+
version=adapter.config.version,
|
|
30
|
+
docs_url="/docs",
|
|
31
|
+
redoc_url="/redoc",
|
|
32
|
+
)
|
|
33
|
+
self._setup_middleware()
|
|
34
|
+
self._setup_routes()
|
|
35
|
+
self._setup_exception_handlers()
|
|
36
|
+
self._setup_events()
|
|
37
|
+
|
|
38
|
+
def _setup_middleware(self) -> None:
|
|
39
|
+
"""Set up middleware for the FastAPI app."""
|
|
40
|
+
# CORS middleware
|
|
41
|
+
self.app.add_middleware(
|
|
42
|
+
CORSMiddleware,
|
|
43
|
+
allow_origins=["*"], # Configure appropriately for production
|
|
44
|
+
allow_credentials=True,
|
|
45
|
+
allow_methods=["*"],
|
|
46
|
+
allow_headers=["*"],
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Note: Request logging removed to avoid middleware compatibility issues
|
|
50
|
+
|
|
51
|
+
def _setup_routes(self) -> None:
|
|
52
|
+
"""Set up API routes."""
|
|
53
|
+
# Include the standard adapter endpoints
|
|
54
|
+
api_router = create_adapter_api(self.adapter)
|
|
55
|
+
self.app.include_router(
|
|
56
|
+
api_router, prefix="/api/v1", tags=["Framework Adapter API"]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Root endpoint
|
|
60
|
+
@self.app.get("/", tags=["Root"])
|
|
61
|
+
async def root() -> dict[str, str]:
|
|
62
|
+
"""Root endpoint with basic information."""
|
|
63
|
+
framework_info = await self.adapter.get_framework_info()
|
|
64
|
+
return {
|
|
65
|
+
"message": f"Welcome to {self.adapter.config.adapter_name} API",
|
|
66
|
+
"framework_id": framework_info.framework_id,
|
|
67
|
+
"version": framework_info.version,
|
|
68
|
+
"api_docs": "/docs",
|
|
69
|
+
"health_check": "/api/v1/health",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def _setup_exception_handlers(self) -> None:
|
|
73
|
+
"""Set up global exception handlers."""
|
|
74
|
+
|
|
75
|
+
@self.app.exception_handler(404)
|
|
76
|
+
async def not_found_handler(request: Request, exc: Any) -> JSONResponse:
|
|
77
|
+
return JSONResponse(
|
|
78
|
+
status_code=404,
|
|
79
|
+
content={
|
|
80
|
+
"error_type": "NotFound",
|
|
81
|
+
"error_message": "Resource not found",
|
|
82
|
+
"path": str(request.url.path),
|
|
83
|
+
},
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@self.app.exception_handler(500)
|
|
87
|
+
async def internal_error_handler(request: Request, exc: Any) -> JSONResponse:
|
|
88
|
+
logger.exception("Internal server error")
|
|
89
|
+
return JSONResponse(
|
|
90
|
+
status_code=500,
|
|
91
|
+
content={
|
|
92
|
+
"error_type": "InternalError",
|
|
93
|
+
"error_message": "An internal error occurred",
|
|
94
|
+
},
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def _setup_events(self) -> None:
|
|
98
|
+
"""Set up startup and shutdown event handlers."""
|
|
99
|
+
|
|
100
|
+
@self.app.on_event("startup")
|
|
101
|
+
async def startup_event() -> None:
|
|
102
|
+
await self.startup()
|
|
103
|
+
|
|
104
|
+
@self.app.on_event("shutdown")
|
|
105
|
+
async def shutdown_event() -> None:
|
|
106
|
+
await self.shutdown()
|
|
107
|
+
|
|
108
|
+
async def startup(self) -> None:
|
|
109
|
+
"""Startup handler for the API server."""
|
|
110
|
+
try:
|
|
111
|
+
await self.adapter.initialize()
|
|
112
|
+
logger.info(
|
|
113
|
+
f"Framework adapter {self.adapter.config.framework_id} initialized"
|
|
114
|
+
)
|
|
115
|
+
except Exception:
|
|
116
|
+
logger.exception("Failed to initialize framework adapter")
|
|
117
|
+
raise
|
|
118
|
+
|
|
119
|
+
async def shutdown(self) -> None:
|
|
120
|
+
"""Shutdown handler for the API server."""
|
|
121
|
+
try:
|
|
122
|
+
await self.adapter.shutdown()
|
|
123
|
+
logger.info(
|
|
124
|
+
f"Framework adapter {self.adapter.config.framework_id} shut down"
|
|
125
|
+
)
|
|
126
|
+
except Exception:
|
|
127
|
+
logger.exception("Error during adapter shutdown")
|
|
128
|
+
|
|
129
|
+
def get_app(self) -> FastAPI:
|
|
130
|
+
"""Get the FastAPI application.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
FastAPI: The configured FastAPI application
|
|
134
|
+
"""
|
|
135
|
+
return self.app
|