def decorator(cls): if not inspect.isclass(cls): raise ValueError("@serve.ingress must be used with a class.") if issubclass(cls, collections.abc.Callable): raise ValueError( "Class passed to @serve.ingress may not have __call__ method.") # Sometimes there are decorators on the methods. We want to fix # the fast api routes here. if isinstance(app, (FastAPI, APIRouter)): make_fastapi_class_based_view(app, cls) # Free the state of the app so subsequent modification won't affect # this ingress deployment. We don't use copy.copy here to avoid # recursion issue. ensure_serialization_context() frozen_app = cloudpickle.loads(cloudpickle.dumps(app)) class ASGIAppWrapper(cls): async def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._serve_app = frozen_app # Use uvicorn's lifespan handling code to properly deal with # startup and shutdown event. self._serve_asgi_lifespan = LifespanOn( Config(self._serve_app, lifespan="on")) # Replace uvicorn logger with our own. self._serve_asgi_lifespan.logger = logger # LifespanOn's logger logs in INFO level thus becomes spammy # Within this block we temporarily uplevel for cleaner logging with LoggingContext(self._serve_asgi_lifespan.logger, level=logging.WARNING): await self._serve_asgi_lifespan.startup() async def __call__(self, request: Request): sender = ASGIHTTPSender() await self._serve_app( request.scope, request._receive, sender, ) return sender.build_starlette_response() def __del__(self): # LifespanOn's logger logs in INFO level thus becomes spammy # Within this block we temporarily uplevel for cleaner logging with LoggingContext(self._serve_asgi_lifespan.logger, level=logging.WARNING): asyncio.get_event_loop().run_until_complete( self._serve_asgi_lifespan.shutdown()) ASGIAppWrapper.__name__ = cls.__name__ return ASGIAppWrapper
def test_make_fastapi_cbv_util(): app = FastAPI() class A: @app.get("/{i}") def b(self, i: int): pass # before, "self" is treated as a query params assert app.routes[-1].endpoint == A.b assert app.routes[-1].dependant.query_params[0].name == "self" assert len(app.routes[-1].dependant.dependencies) == 0 make_fastapi_class_based_view(app, A) # after, "self" is treated as a dependency instead of query params assert app.routes[-1].endpoint == A.b assert len(app.routes[-1].dependant.query_params) == 0 assert len(app.routes[-1].dependant.dependencies) == 1 self_dep = app.routes[-1].dependant.dependencies[0] assert self_dep.name == "self" assert inspect.isfunction(self_dep.call) assert "get_current_servable" in str(self_dep.call)