88import logging
99import importlib
1010import inspect
11+ import os
12+ import signal
1113
1214from datetime import datetime
1315from fastapi import FastAPI , APIRouter , HTTPException , Request
1416from fastapi .middleware .cors import CORSMiddleware
1517from fastapi .middleware .wsgi import WSGIMiddleware
1618from fastapi .exceptions import RequestValidationError
1719from fastapi .responses import JSONResponse
20+ from contextlib import asynccontextmanager
21+ from termcolor import colored
1822
19- from typing import Dict , Optional , Type , Union , Set , ForwardRef
23+ from typing import Dict , Optional , Type , Union , Set
2024
2125from healthchain .gateway .core .base import BaseGateway
2226from healthchain .gateway .events .dispatcher import EventDispatcher
2529logger = logging .getLogger (__name__ )
2630
2731
28- # Forward reference for type hints
29- HealthChainAPIRef = ForwardRef ("HealthChainAPI" )
30-
31-
3232class HealthChainAPI (FastAPI ):
3333 """
3434 HealthChainAPI wraps FastAPI to provide healthcare-specific integrations.
@@ -83,6 +83,10 @@ def __init__(
8383 event_dispatcher: Optional event dispatcher to use (for testing/DI)
8484 **kwargs: Additional keyword arguments to pass to FastAPI
8585 """
86+ # Set up the lifespan
87+ if "lifespan" not in kwargs :
88+ kwargs ["lifespan" ] = self .lifespan
89+
8690 super ().__init__ (
8791 title = title , description = description , version = version , ** kwargs
8892 )
@@ -122,6 +126,13 @@ def __init__(
122126 # Register self as a dependency for get_app
123127 self .dependency_overrides [get_app ] = lambda : self
124128
129+ # Add a shutdown route
130+ shutdown_router = APIRouter ()
131+ shutdown_router .add_api_route (
132+ "/shutdown" , self ._shutdown , methods = ["GET" ], include_in_schema = False
133+ )
134+ self .include_router (shutdown_router )
135+
125136 def get_event_dispatcher (self ) -> Optional [EventDispatcher ]:
126137 """Get the event dispatcher instance.
127138
@@ -233,7 +244,7 @@ def _add_gateway_routes(
233244 self .gateway_endpoints [gateway_name ].add (
234245 f"{ method } :{ route_path } "
235246 )
236- logger .info (
247+ logger .debug (
237248 f"Registered { method } route { route_path } for { gateway_name } "
238249 )
239250
@@ -257,7 +268,7 @@ def _add_gateway_routes(
257268 # Mount the WSGI app
258269 self .mount (mount_path , WSGIMiddleware (wsgi_app ))
259270 self .gateway_endpoints [gateway_name ].add (f"WSGI:{ mount_path } " )
260- logger .info (f"Registered WSGI gateway { gateway_name } at { mount_path } " )
271+ logger .debug (f"Registered WSGI gateway { gateway_name } at { mount_path } " )
261272
262273 # Case 3: Gateway instances that are also APIRouters (like FHIRGateway)
263274 elif isinstance (gateway , APIRouter ):
@@ -269,11 +280,11 @@ def _add_gateway_routes(
269280 self .gateway_endpoints [gateway_name ].add (
270281 f"{ method } :{ route .path } "
271282 )
272- logger .info (
283+ logger .debug (
273284 f"Registered { method } route { route .path } from { gateway_name } router"
274285 )
275286 else :
276- logger .info (f"Registered { gateway_name } as router (routes unknown)" )
287+ logger .debug (f"Registered { gateway_name } as router (routes unknown)" )
277288
278289 elif not (
279290 hasattr (gateway , "get_routes" )
@@ -282,15 +293,23 @@ def _add_gateway_routes(
282293 ):
283294 logger .warning (f"Gateway { gateway_name } does not provide any routes" )
284295
285- def register_router (self , router : Union [APIRouter , Type , str ], ** options ) -> None :
296+ def register_router (
297+ self , router : Union [APIRouter , Type , str , list ], ** options
298+ ) -> None :
286299 """
287- Register a router with the API.
300+ Register one or more routers with the API.
288301
289302 Args:
290- router: The router to register (can be an instance, class, or import path)
303+ router: The router(s) to register (can be an instance, class, import path, or list of any of these )
291304 **options: Options to pass to the router constructor or include_router
292305 """
293306 try :
307+ # Handle list of routers
308+ if isinstance (router , list ):
309+ for r in router :
310+ self .register_router (r , ** options )
311+ return
312+
294313 # Case 1: Direct APIRouter instance
295314 if isinstance (router , APIRouter ):
296315 self .include_router (router , ** options )
@@ -403,6 +422,47 @@ async def _general_exception_handler(
403422 content = {"detail" : "Internal server error" },
404423 )
405424
425+ @asynccontextmanager
426+ async def lifespan (self , app : FastAPI ):
427+ """Lifecycle manager for the application."""
428+ self ._startup ()
429+ yield
430+ self ._shutdown ()
431+
432+ def _startup (self ) -> None :
433+ """Display startup information and log registered endpoints."""
434+ healthchain_ascii = r"""
435+
436+ __ __ ____ __ ________ _
437+ / / / /__ ____ _/ / /_/ /_ / ____/ /_ ____ _(_)___
438+ / /_/ / _ \/ __ `/ / __/ __ \/ / / __ \/ __ `/ / __ \
439+ / __ / __/ /_/ / / /_/ / / / /___/ / / / /_/ / / / / /
440+ /_/ /_/\___/\__,_/_/\__/_/ /_/\____/_/ /_/\__,_/_/_/ /_/
441+
442+ """ # noqa: E501
443+
444+ colors = ["red" , "yellow" , "green" , "cyan" , "blue" , "magenta" ]
445+ for i , line in enumerate (healthchain_ascii .split ("\n " )):
446+ color = colors [i % len (colors )]
447+ print (colored (line , color ))
448+
449+ # Log registered gateways and endpoints
450+ for name , gateway in self .gateways .items ():
451+ endpoints = self .gateway_endpoints .get (name , set ())
452+ for endpoint in endpoints :
453+ print (f"{ colored ('HEALTHCHAIN' , 'green' )} : { endpoint } " )
454+
455+ print (
456+ f"{ colored ('HEALTHCHAIN' , 'green' )} : See more details at { colored (self .docs_url , 'magenta' )} "
457+ )
458+
459+ def _shutdown (self ):
460+ """
461+ Shuts down server by sending a SIGTERM signal.
462+ """
463+ os .kill (os .getpid (), signal .SIGTERM )
464+ return JSONResponse (content = {"message" : "Server is shutting down..." })
465+
406466
407467def create_app (
408468 config : Optional [Dict ] = None ,
0 commit comments