fix: uploaded file were closed automatically before the model could infer their data

This commit is contained in:
faraphel 2025-01-10 11:05:41 +01:00
parent 639425ad7d
commit b89fafdc96
6 changed files with 62 additions and 12 deletions

View file

@ -2,5 +2,7 @@
"type": "python", "type": "python",
"file": "model.py", "file": "model.py",
"inputs": {} "inputs": {
"file": {"type": "file"}
}
} }

View file

@ -8,5 +8,5 @@ def load(model) -> None:
def unload(model) -> None: def unload(model) -> None:
pass pass
def infer(model) -> typing.Iterator[bytes]: def infer(model, file) -> typing.Iterator[bytes]:
yield json.dumps({"hello": "world!"}).encode("utf-8") yield json.dumps({"hello": "world!"}).encode("utf-8")

View file

@ -11,6 +11,7 @@ import fastapi
from source import utils from source import utils
from source.manager import ModelManager from source.manager import ModelManager
from source.model import base from source.model import base
from source.utils.fastapi import UploadFileFix
class PythonModel(base.BaseModel): class PythonModel(base.BaseModel):
@ -49,9 +50,18 @@ class PythonModel(base.BaseModel):
parameters = utils.parameters.load(configuration.get("inputs", {})) parameters = utils.parameters.load(configuration.get("inputs", {}))
# create an endpoint wrapping the inference inside a fastapi call # create an endpoint wrapping the inference inside a fastapi call
async def infer_api(*args, **kwargs): async def infer_api(**kwargs):
# NOTE: fix an issue where it is not possible to give an UploadFile to a StreamingResponse
# NOTE: perform a naive type(value).__name__ == "type_name" because fastapi do not use it own
# fastapi.UploadFile class, but instead the starlette UploadFile class that is more of an implementation
# curiosity that may change in the future
kwargs = {
key: UploadFileFix(value) if type(value).__name__ == "UploadFile" else value
for key, value in kwargs.items()
}
return fastapi.responses.StreamingResponse( return fastapi.responses.StreamingResponse(
content=self.infer(*args, **kwargs), content=self.infer(**kwargs),
media_type=self.output_type, media_type=self.output_type,
) )
@ -66,5 +76,5 @@ class PythonModel(base.BaseModel):
def _unload(self) -> None: def _unload(self) -> None:
return self.module.unload(self) return self.module.unload(self)
def _infer(self, *args, **kwargs) -> typing.Iterator[bytes]: def _infer(self, **kwargs) -> typing.Iterator[bytes]:
return self.module.infer(self, *args, **kwargs) return self.module.infer(self, **kwargs)

View file

@ -104,7 +104,7 @@ class BaseModel(abc.ABC):
Do not call manually, use `unload` instead. Do not call manually, use `unload` instead.
""" """
def infer(self, *args, **kwargs) -> typing.Iterator[bytes]: def infer(self, **kwargs) -> typing.Iterator[bytes]:
""" """
Infer our payload through the model within the model manager Infer our payload through the model within the model manager
:return: the response of the model :return: the response of the model
@ -114,10 +114,10 @@ class BaseModel(abc.ABC):
self.load() self.load()
# model specific inference part # model specific inference part
return self._infer(*args, **kwargs) return self._infer(**kwargs)
@abc.abstractmethod @abc.abstractmethod
def _infer(self, *args, **kwargs) -> typing.Iterator[bytes]: def _infer(self, **kwargs) -> typing.Iterator[bytes]:
""" """
Infer our payload through the model Infer our payload through the model
:return: the response of the model :return: the response of the model

39
source/utils/fastapi.py Normal file
View file

@ -0,0 +1,39 @@
"""
fastapi.UploadFile currently have an issue where using an UploadFile and giving it to a StreamingResponse close it.
Fix from this comment :
https://github.com/fastapi/fastapi/issues/10857#issuecomment-2079878117
"""
from fastapi import UploadFile
class UploadFileFix(UploadFile):
"""Patches `fastapi.UploadFile` due to buffer close issue.
See the related github issue:
https://github.com/tiangolo/fastapi/issues/10857
"""
def __init__(self, upload_file: UploadFile) -> None:
"""Wraps and mutates input `fastapi.UploadFile`.
Swaps `close` method on the input instance so it's a no-op when called
by the framework. Adds `close` method of input as `_close` here, to be
called later with overridden `close` method.
"""
self.filename = upload_file.filename
self.file = upload_file.file
self.size = upload_file.size
self.headers = upload_file.headers
_close = upload_file.close
setattr(upload_file, "close", self._close)
setattr(self, "_close", _close)
async def _close(self) -> None:
pass
async def close(self) -> None: # noqa: D102
await self._close()

View file

@ -1,8 +1,7 @@
import inspect import inspect
from datetime import datetime from datetime import datetime
import fastapi from fastapi import UploadFile
# the list of types and their name that can be used by the API # the list of types and their name that can be used by the API
types: dict[str, type] = { types: dict[str, type] = {
@ -16,7 +15,7 @@ types: dict[str, type] = {
"set": set, "set": set,
"dict": dict, "dict": dict,
"datetime": datetime, "datetime": datetime,
"file": fastapi.UploadFile, "file": UploadFile,
} }