Podrías usar esto si quieres hacer stream de strings puros, por ejemplo directamente desde la salida de un servicio de AI LLM.
También podrías usarlo para hacer stream de archivos binarios grandes, donde haces stream de cada chunk de datos a medida que lo lees, sin tener que leerlo todo a la memoria de una sola vez.
También podrías hacer stream de video o audio de esta manera, incluso podría ser generado a medida que lo procesas y envías.
Si declaras response_class=StreamingResponse en tu función path operation, puedes usar yield para enviar cada chunk de datos por turno.
fromcollections.abcimportAsyncIterable,IterablefromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseapp=FastAPI()message="""Rick: (stumbles in drunkenly, and turns on the lights) Morty! You gotta come on. You got--... you gotta come with me.Morty: (rubs his eyes) What, Rick? What's going on?Rick: I got a surprise for you, Morty.Morty: It's the middle of the night. What are you talking about?Rick: (spills alcohol on Morty's bed) Come on, I got a surprise for you. (drags Morty by the ankle) Come on, hurry up. (pulls Morty out of his bed and into the hall)Morty: Ow! Ow! You're tugging me too hard!Rick: We gotta go, gotta get outta here, come on. Got a surprise for you Morty."""@app.get("/story/stream",response_class=StreamingResponse)asyncdefstream_story()->AsyncIterable[str]:forlineinmessage.splitlines():yieldline# Code below omitted 👇
👀 Vista previa del archivo completo
fromcollections.abcimportAsyncIterable,IterablefromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseapp=FastAPI()message="""Rick: (stumbles in drunkenly, and turns on the lights) Morty! You gotta come on. You got--... you gotta come with me.Morty: (rubs his eyes) What, Rick? What's going on?Rick: I got a surprise for you, Morty.Morty: It's the middle of the night. What are you talking about?Rick: (spills alcohol on Morty's bed) Come on, I got a surprise for you. (drags Morty by the ankle) Come on, hurry up. (pulls Morty out of his bed and into the hall)Morty: Ow! Ow! You're tugging me too hard!Rick: We gotta go, gotta get outta here, come on. Got a surprise for you Morty."""@app.get("/story/stream",response_class=StreamingResponse)asyncdefstream_story()->AsyncIterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async",response_class=StreamingResponse)defstream_story_no_async()->Iterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-annotation",response_class=StreamingResponse)asyncdefstream_story_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async-no-annotation",response_class=StreamingResponse)defstream_story_no_async_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-bytes",response_class=StreamingResponse)asyncdefstream_story_bytes()->AsyncIterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-bytes",response_class=StreamingResponse)defstream_story_no_async_bytes()->Iterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-annotation-bytes",response_class=StreamingResponse)asyncdefstream_story_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-no-annotation-bytes",response_class=StreamingResponse)defstream_story_no_async_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")
FastAPI le dará cada chunk de datos al StreamingResponse tal cual, no intentará convertirlo a JSON ni nada similar.
fromcollections.abcimportAsyncIterable,IterablefromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseapp=FastAPI()message="""Rick: (stumbles in drunkenly, and turns on the lights) Morty! You gotta come on. You got--... you gotta come with me.Morty: (rubs his eyes) What, Rick? What's going on?Rick: I got a surprise for you, Morty.Morty: It's the middle of the night. What are you talking about?Rick: (spills alcohol on Morty's bed) Come on, I got a surprise for you. (drags Morty by the ankle) Come on, hurry up. (pulls Morty out of his bed and into the hall)Morty: Ow! Ow! You're tugging me too hard!Rick: We gotta go, gotta get outta here, come on. Got a surprise for you Morty."""@app.get("/story/stream",response_class=StreamingResponse)asyncdefstream_story()->AsyncIterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async",response_class=StreamingResponse)defstream_story_no_async()->Iterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-annotation",response_class=StreamingResponse)asyncdefstream_story_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async-no-annotation",response_class=StreamingResponse)defstream_story_no_async_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-bytes",response_class=StreamingResponse)asyncdefstream_story_bytes()->AsyncIterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-bytes",response_class=StreamingResponse)defstream_story_no_async_bytes()->Iterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-annotation-bytes",response_class=StreamingResponse)asyncdefstream_story_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-no-annotation-bytes",response_class=StreamingResponse)defstream_story_no_async_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")
Realmente no necesitas declarar la anotación de tipo de retorno para hacer stream de datos binarios.
Ya que FastAPI no intentará convertir los datos a JSON con Pydantic ni serializarlos de ninguna manera, en este caso, la anotación de tipo es solo para que la usen tu editor y herramientas, no será usada por FastAPI.
fromcollections.abcimportAsyncIterable,IterablefromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseapp=FastAPI()message="""Rick: (stumbles in drunkenly, and turns on the lights) Morty! You gotta come on. You got--... you gotta come with me.Morty: (rubs his eyes) What, Rick? What's going on?Rick: I got a surprise for you, Morty.Morty: It's the middle of the night. What are you talking about?Rick: (spills alcohol on Morty's bed) Come on, I got a surprise for you. (drags Morty by the ankle) Come on, hurry up. (pulls Morty out of his bed and into the hall)Morty: Ow! Ow! You're tugging me too hard!Rick: We gotta go, gotta get outta here, come on. Got a surprise for you Morty."""@app.get("/story/stream",response_class=StreamingResponse)asyncdefstream_story()->AsyncIterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async",response_class=StreamingResponse)defstream_story_no_async()->Iterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-annotation",response_class=StreamingResponse)asyncdefstream_story_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async-no-annotation",response_class=StreamingResponse)defstream_story_no_async_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-bytes",response_class=StreamingResponse)asyncdefstream_story_bytes()->AsyncIterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-bytes",response_class=StreamingResponse)defstream_story_no_async_bytes()->Iterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-annotation-bytes",response_class=StreamingResponse)asyncdefstream_story_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-no-annotation-bytes",response_class=StreamingResponse)defstream_story_no_async_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")
Esto también significa que con StreamingResponse tienes la libertad y la responsabilidad de producir y codificar los bytes de datos exactamente como necesitas que se envíen, independientemente de las anotaciones de tipo. 🤓
fromcollections.abcimportAsyncIterable,IterablefromfastapiimportFastAPIfromfastapi.responsesimportStreamingResponseapp=FastAPI()message="""Rick: (stumbles in drunkenly, and turns on the lights) Morty! You gotta come on. You got--... you gotta come with me.Morty: (rubs his eyes) What, Rick? What's going on?Rick: I got a surprise for you, Morty.Morty: It's the middle of the night. What are you talking about?Rick: (spills alcohol on Morty's bed) Come on, I got a surprise for you. (drags Morty by the ankle) Come on, hurry up. (pulls Morty out of his bed and into the hall)Morty: Ow! Ow! You're tugging me too hard!Rick: We gotta go, gotta get outta here, come on. Got a surprise for you Morty."""@app.get("/story/stream",response_class=StreamingResponse)asyncdefstream_story()->AsyncIterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async",response_class=StreamingResponse)defstream_story_no_async()->Iterable[str]:forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-annotation",response_class=StreamingResponse)asyncdefstream_story_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-no-async-no-annotation",response_class=StreamingResponse)defstream_story_no_async_no_annotation():forlineinmessage.splitlines():yieldline@app.get("/story/stream-bytes",response_class=StreamingResponse)asyncdefstream_story_bytes()->AsyncIterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-bytes",response_class=StreamingResponse)defstream_story_no_async_bytes()->Iterable[bytes]:forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-annotation-bytes",response_class=StreamingResponse)asyncdefstream_story_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")@app.get("/story/stream-no-async-no-annotation-bytes",response_class=StreamingResponse)defstream_story_no_async_no_annotation_bytes():forlineinmessage.splitlines():yieldline.encode("utf-8")
En los ejemplos anteriores, los bytes de datos se transmitieron en stream, pero la respuesta no tenía un header Content-Type, así que el cliente no sabía qué tipo de datos estaba recibiendo.
Puedes crear una sub-clase personalizada de StreamingResponse que establezca el header Content-Type al tipo de datos que estás transmitiendo.
Por ejemplo, puedes crear un PNGStreamingResponse que establezca el header Content-Type a image/png usando el atributo media_type:
En este ejemplo, estamos simulando un archivo con io.BytesIO, que es un objeto tipo archivo que vive solo en memoria, pero nos permite usar la misma interfaz.
Por ejemplo, podemos iterar sobre él para consumir su contenido, como podríamos hacer con un archivo.
Las otras dos variables, image_base64 y binary_image, son una imagen codificada en Base64, y luego convertida a bytes, para luego pasarla a io.BytesIO.
Solo para que pueda vivir en el mismo archivo para este ejemplo y puedas copiarlo y ejecutarlo tal cual. 🥚
Al usar un bloque with, nos aseguramos de que el objeto tipo archivo se cierre después de que la función generadora (la función con yield) termine. Es decir, después de que termine de enviar la respuesta.
No sería tan importante en este ejemplo específico porque es un archivo falso en memoria (con io.BytesIO), pero con un archivo real, sería importante asegurarse de que el archivo se cierre después de terminar el trabajo con él.
En la mayoría de los casos, los objetos tipo archivo no son compatibles con async y await por defecto.
Por ejemplo, no tienen un await file.read(), ni async for chunk in file.
Y en muchos casos, leerlos sería una operación bloqueante (que podría bloquear el event loop), porque se leen del disco o desde la red.
Nota
El ejemplo anterior es en realidad una excepción, porque el objeto io.BytesIO ya está en memoria, así que leerlo no bloqueará nada.
Pero en muchos casos leer un archivo o un objeto tipo archivo sería bloqueante.
Para evitar bloquear el event loop, simplemente puedes declarar la función path operation con def regular en lugar de async def, de esa manera FastAPI la ejecutará en un worker del threadpool, para evitar bloquear el loop principal.
Si necesitas llamar código bloqueante desde dentro de una función async, o una función async desde dentro de una función bloqueante, podrías usar Asyncer, una librería hermana de FastAPI.
Cuando estás iterando sobre algo, como un objeto tipo archivo, y luego estás haciendo yield por cada item, también podrías usar yield from para hacer yield de cada item directamente y omitir el bucle for.
Esto no es particular de FastAPI, es simplemente Python, pero es un buen truco para conocer. 😎