How can I capture fastapi websocket messages without html/js? - python

Fastapi docs include a websocket example that receives data via html/javascript. Saving the script as main.py and running uvicorn main:app --reload, the example works as expected:
from fastapi import FastAPI, WebSocket
from fastapi.responses import HTMLResponse
app = FastAPI()
html = """
<!DOCTYPE html>
<html>
<head>
<title>Chat</title>
</head>
<body>
<h1>WebSocket Chat</h1>
<form action="" onsubmit="sendMessage(event)">
<input type="text" id="messageText" autocomplete="off"/>
<button>Send</button>
</form>
<ul id='messages'>
</ul>
<script>
var ws = new WebSocket("ws://localhost:8000/ws");
ws.onmessage = function(event) {
var messages = document.getElementById('messages')
var message = document.createElement('li')
var content = document.createTextNode(event.data)
message.appendChild(content)
messages.appendChild(message)
};
function sendMessage(event) {
var input = document.getElementById("messageText")
ws.send(input.value)
input.value = ''
event.preventDefault()
}
</script>
</body>
</html>
"""
#app.get("/")
async def get():
return HTMLResponse(html)
#app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
while True:
data = await websocket.receive_text()
await websocket.send_text(f"Message text was: {data}")
How can I modify this example to write websocket messages to file without using any html/js? I'd like direct access to the incoming data (text/json) with python and I'm unable to capture it directly. Any additional info/clarity is appreciated.

So, probably in the comments I didn't explain myself well.
From what I understood you want to connect via python to your webserver with a websocket and log to file the messages the server sends to your python script on the client. In simpler terms, you need to mimic the html/js part via python.
TL;DR
The server is already there, you just need to connect to it.
Here's the code snippet that you have to copy and paste in a different file and run when the webserver is already running. Note that the webserver doesn't need to be changed, if not for the two line within the while True loop. These can go away and you may change them with something like await websocket.send_text("text")
import asyncio
import websockets
async def hello():
uri = "ws://localhost:8000/ws"
async with websockets.connect(uri) as websocket:
await websocket.send("Hello world!")
res = await websocket.recv()
print(res)
asyncio.get_event_loop().run_until_complete(hello())
I'm not sure what messages you need to write to file, but the code snippet above is working and is the basis for what you need.

Related

How to Download a File after POSTing data using FastAPI?

I am creating a web application that receives some text, converts the text into speech, and returns an mp3 file, which is saved to a temporary directory.
I want to be able to download the file from the html page (i.e., the frontend), but I don't know how to do that properly.
I know with Flask you can do this:
from app import app
from flask import Flask, send_file, render_template
#app.route('/')
def upload_form():
return render_template('download.html')
#app.route('/download')
def download_file():
path = "html2pdf.pdf"
return send_file(path, as_attachment=True)
if __name__ == "__main__":
app.run()
HTML Example:
<!doctype html>
<title>Python Flask File Download Example</title>
<h2>Download a file</h2>
<p>Download</p>
So how do I replicate this with FastAPI?
FastAPI Code:
from fastapi import FastAPI, File, Request, Response, UploadFile
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
from fastapi.templating import Jinja2Templates
from gtts import gTTS
templates = Jinja2Templates(directory="templates")
def text_to_speech(language:str, text: str) -> str:
tts = gTTS(text=text, lang=language, slow=False)
tts.save("./temp/welcome.mp3")
#os.system("mpg321 /temp/welcome.mp3")
return "Text to speech conversion successful"
#app.get("/")
def home(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
#app.get("/text2speech")
async def home(request: Request):
if request.method == "POST":
form = await request.form()
if form["message"] and form["language"]:
language = form["language"]
text = form["message"]
translate = text_to_speech(language, text)
path = './temp/welcome.mp3'
value = FileResponse("./temp/welcome.mp3", media_type="audio/mp3")
return value
# return templates.TemplateResponse(
# "index.html",
# {"request": request, "message": text, "language": language, "download": value},
# )
Sample HTML File:
<!doctype html>
<title>Download MP3 File</title>
<h2>Download a file</h2>
<p>Download</p>
Use the Form keyword to define Form-data in your endpoint, and more specifically, use Form(...) to make a parameter required, instead of using await request.form() and manually checking if the user submitted the required parameters. After processing the received data and generating the audio file, you can use FileResponse to return the file to the user. Note: use the headers argument in FileResponse to set the Content-Disposition header using the attachment parameter—as described in this answer—to have the file downloaded to your device. Failing to set the headers, or using the inline parameter isntead, would lead to 405 Method Not Allowed error, as the browser attempts to access the file using a GET request (however, only POST requests are allowed to the /text2speech endpoint). Have a look at Option 1 in the examples below.
If you wanted the /text2speech endpoint supporting both GET and POST requests (as shown in your question), you could either use #app.api_route("/text2speech", methods=["GET", "POST"]) and use request.method to check which one has been called, or define two different endpoints e.g., #app.post('/text2speech') and #app.get('/text2speech'). However, you don't necessarily need to do that in this case. Additionally, you have added a Download hyperlink to your template for the user to download the file. However, you haven't provided any information as to how you expect this to work. This wouldn't work in a scenario where you don't have static files, but dynamically generated audio files (as in your case), as well as multiple users accessing the API at the same time; unless, for example, you generated random UUIDs for the filenames and saved the files in a StaticFiles directory—or added that unique identifier as a query/path parameter (you could also use cookies instead, see here and here) to the URL in order to identify the file to be downloaded—and sent the URL back to the user. In that case, you would need a Javascript interface/library, such as Fetch API, to make an asynchronous HTTP request—as described in this answer—in order to get the URL to the file and display it in the Download hyperlink. Have a look at Option 2 below. Note: The example in Option 2 uses a simple dict to map the filepaths to UUIDs, for demo purposes. In a real-world scenario, where multiple users access the API and several workers might be used, you may consider using a database storage, or Key-Value stores (Caches), as described here and here. You would also need to have a mechanism for deleting the files from the database and disk, once they have been downloaded, as well as make sure that users do not have unauthorised access to other users' audio files.
Option 1
app.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import os
app = FastAPI()
templates = Jinja2Templates(directory="templates")
#app.get('/')
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
#app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
# do some processing here
filepath = './temp/welcome.mp3'
filename = os.path.basename(filepath)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return FileResponse(filepath, headers=headers, media_type="audio/mp3")
An alternative to the above would be to read the file data inside your endpoint—or, in case the data were fully loaded into memory beforehand, such as here, here and here—and return a custom Response directly, as shown below:
from fastapi import Response
#app.post('/text2speech')
...
with open(filepath, "rb") as f:
contents = f.read() # file contents could be already fully loaded into RAM
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return Response(contents, headers=headers, media_type='audio/mp3')
In case you had to return a file that is too large to fit into memory—e.g., if you have 8GB of RAM, you can’t load a 50GB file—you could use StreamingResponse, which would load the file into memory in chunks and process the data one chunk at a time (If you find yield from f being rather slow, please have a look at this answer for faster alternatives):
from fastapi.responses import StreamingResponse
#app.post('/text2speech')
...
def iterfile():
with open(filepath, "rb") as f:
yield from f
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return StreamingResponse(iterfile(), headers=headers, media_type="audio/mp3")
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Convert Text to Speech</title>
</head>
<body>
<form method="post" action="http://127.0.0.1:8000/text2speech">
message : <input type="text" name="message" value="This is a sample message"><br>
language : <input type="text" name="language" value="en"><br>
<input type="submit" value="submit">
</form>
</body>
</html>
Using JavaScript to download the file
In case you used a JavaScript interface, such as Fetch API, in the frontend to issue the file-download request—instead of using an HTML <form>, as demonstrated above—please have a look at this answer, as well as this answer and this answer on how to download the file in the frontend through JavaScript.
Option 2
app.py
from fastapi import FastAPI, Request, Form
from fastapi.templating import Jinja2Templates
from fastapi.responses import FileResponse
import uuid
import os
app = FastAPI()
templates = Jinja2Templates(directory="templates")
files = {}
#app.get('/')
def main(request: Request):
return templates.TemplateResponse("index.html", {"request": request})
#app.get('/download')
def download_file(request: Request, fileId: str):
filepath = files.get(fileId)
if filepath:
filename = os.path.basename(filepath)
headers = {'Content-Disposition': f'attachment; filename="{filename}"'}
return FileResponse(filepath, headers=headers, media_type='audio/mp3')
#app.post('/text2speech')
def convert(request: Request, message: str = Form(...), language: str = Form(...)):
# do some processing here
filepath = './temp/welcome.mp3'
file_id = str(uuid.uuid4())
files[file_id] = filepath
file_url = f'/download?fileId={file_id}'
return {"fileURL": file_url}
templates/index.html
<!DOCTYPE html>
<html>
<head>
<title>Convert Text to Speech</title>
</head>
<body>
<form method="post" id="myForm">
message : <input type="text" name="message" value="This is a sample message"><br>
language : <input type="text" name="language" value="en"><br>
<input type="button" value="Submit" onclick="submitForm()">
</form>
<a id="downloadLink" href=""></a>
<script type="text/javascript">
function submitForm() {
var formElement = document.getElementById('myForm');
var data = new FormData(formElement);
fetch('/text2speech', {
method: 'POST',
body: data,
})
.then(response => response.json())
.then(data => {
document.getElementById("downloadLink").href = data.fileURL;
document.getElementById("downloadLink").innerHTML = "Download";
})
.catch(error => {
console.error(error);
});
}
</script>
</body>
</html>
Removing a File after it's been downloaded
For Option 1 above, to remove a file after it has been downloaded by the user, you can simply define a BackgroundTask to be run after returning the response. For example:
from fastapi import BackgroundTasks
import os
#app.post('/text2speech')
def convert(request: Request, background_tasks: BackgroundTasks, ...):
filepath = 'welcome.mp3'
# ...
background_tasks.add_task(os.remove, path=filepath)
return FileResponse(filepath, headers=headers, media_type="audio/mp3")
For Option 2, however, you would have to make sure to delete the key (i.e., file_id) pointing to the given filepath from the cache as well. Hence, you should create a background task function, as shown below:
from fastapi import BackgroundTasks
import os
files = {}
def remove_file(filepath, fileId):
os.remove(filepath)
del files[fileId]
#app.get('/download')
def download_file(request: Request, fileId: str, background_tasks: BackgroundTasks):
filepath = files.get(fileId)
if filepath:
# ...
background_tasks.add_task(remove_file, filepath=filepath, fileId=fileId)
return FileResponse(filepath, headers=headers, media_type='audio/mp3')
More details and examples on background tasks can be found here, as well as here.

How to send a file (docx, doc, pdf or json) to fastapi and predict on it without UI (i.e., HTML)?

If you know how to send a file to FastAPI server and access it in /predict endpoint for prediction using my models please help me out.
I have deployed the model using /predict endpoint and done uvicorn main:app and it's deployed but the only thing is input that is a document is in my local pc so how can I sent it to FastAPI?
I have went through the documentation of FastAPI and I have found this example code there, but the challenge is that this code creates an UI for uploading file which is not what I"m looking for.
from typing import Optional
from fastapi import FastAPI
from fastapi import FastAPI, File, UploadFile
from pydantic import BaseModel
from typing import List
from fastapi.responses import HTMLResponse
app = FastAPI()
class User(BaseModel):
user_name: dict
#app.post("/files/")
async def create_files(files: List[bytes] = File(...)):
return {"file_sizes": [len(file) for file in files]}
#app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
return {"filenames": [file.filename for file in files]}
#app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
FASTAPI CODE
This will be your endpoint.
from fastapi import FastAPI, UploadFile, File
app = FastAPI()
#app.post("/file")
async def upload_file(file: UploadFile = File(...)):
# Do here your stuff with the file
return {"filename": file.filename}
JAVASCRIPT CODE
This is your javascript code (assuming you are using javascript for uploading the file)
form = new FormData();
form.append("file", myFile);
let response = await fetch('/file', {
method: 'POST',
body: form
});
let result = await response.json();
EDIT: Python file upload
I'm using httpx, but technically it should be fully compatible with requests.
import httpx
# Create a dict with a key that has the same name as your file parameter and the file in binary form (the "b" in "rb")
f = {'file': open('foo.png', 'rb')}
r = httpx.post("your_url/file", files=f)
You can see more configurations/examples on the official documentation of httpx at https://www.python-httpx.org/quickstart/#sending-multipart-file-uploads.
Again, I did not test the code, since I'm tight with time at the moment.
END OF EDIT
Be aware that the parameter name of the file, MUST match the one used for sending the file.
In case, there is also another answer from me on how to test it with POSTMAN. See How to send file to fastapi endpoint using postman
NOTE
I did not test the code as I don't have time right now. In case, there is also a link with a previous answer of mine that works (unless FASTAPI introduced breaking changes).

Send a csv file to fastAPI and get a new file back

I have this very simple code that gets a dictionary as imput and returns back a dictionary as well:
app = FastAPI()
class User(BaseModel):
user_name: dict
#app.post("/")
def main(user: User):
## some processing
return user
I'm calling this with the following python code:
import requests
import json
import pandas as pd
df = pd.read_csv("myfile.csv")
data = {}
data["user_name"] = df.to_dict()
headers = {'content-type': 'application/json'}
url = 'http://localhost:8000/'
resp = requests.post(url,data=json.dumps(data), headers=headers )
resp
Now, I want to do something similar but instead of sending the data with a request from python
I want to upload a local file send it to the API and get a processed .csv file back.
Now I have the following code to upload a file:
from typing import Optional
from fastapi import FastAPI
from fastapi import FastAPI, File, UploadFile
from pydantic import BaseModel
from typing import List
from fastapi.responses import HTMLResponse
app = FastAPI()
class User(BaseModel):
user_name: dict
#app.post("/files/")
async def create_files(files: List[bytes] = File(...)):
return {"file_sizes": [len(file) for file in files]}
#app.post("/uploadfiles/")
async def create_upload_files(files: List[UploadFile] = File(...)):
return {"filenames": [file.filename for file in files]}
#app.get("/")
async def main():
content = """
<body>
<form action="/files/" enctype="multipart/form-data" method="post">
<input name="files" type="file" multiple>
<input type="submit">
</form>
</body>
"""
return HTMLResponse(content=content)
This allows me to select and load a file:
But when I click on send, I'm redirected to: http://localhost:8000/uploadfiles/ and get the following error:
{"detail":"Method Not Allowed"}
How can I send the file for procesing to the API and get a file back as a response?
I copied your code and I got the same error. After going through the FastAPI documentation for requesting files, it is mentioned that
To receive uploaded files, first install python-multipart.
E.g. pip install python-multipart.
This is because uploaded files are sent as "form data".
After installing the library python-multipart, the code worked perfectly.
References:
https://andrew-d.github.io/python-multipart/

how does aiohttp handle onclick function

I am a newbie for the aiohttp, and I have a question below,
First, I have a button in the page as below,
<body>
<button class="button" name="click_me" onclick="click_me">Click Me</button>
</body>
and in the python code, I define the function as below,
async def click_me(request):
logger.info(f'test on click_me')
async def handler(request):
... ... default code here ... ...
However, when I open the browser and click the "click_me" button, the debugging page shows the error,
Uncaught ReferenceError: click_me is not defined at HTMLButtonElement.onclick
How to define the function in the python script to catch click_me event?
No, the onclick attribute refers to a Javascript function, that must be loaded in your page. If you want to call a Python function from that click event, you must perform an HTTP request (or whatever, there are many other ways), for instance with AJAX or fetch, to your corresponing server's url. e.g.
<body>
<button onclick="doFoo()"></button>
<script>
async function doFoo() {
const response = await fetch('http://YOURSERVER/foo')
// do whatever you want with your response
}
</script>
</body>
Will call the Python foo function defined here
from aiohttp import web
async def foo(request):
logger.info("button clicked")
return web.Response()
app = web.Application()
app.add_routes([web.get('/foo', foo)])
if __name__ == '__main__':
web.run_app(app)

why request.body is empty?

I try to get data from a user:
class EchoWebSocket(tornado.websocket.WebSocketHandler):
def open(self):
print("WebSocket opened")
def on_message(self, message):
nom = self.get_argument("nom") # this will return an error, because nom is not found
# I then tried to retrieve the result of the body,
print(self.request.body) # nothing to show!
def on_close(self):
print("WebSocket closed")
client side:
$(document).ready(function() {
$("#frm").on("submit", function(e){
var formdata = $("#frm").serialize()
console.log(formdata) // gives _xsrf=2%7C0fc414f0%7Cf5e0bd645c867be5879aa239b5ce0dfe%7C1456505450&nom=sdfsdf
var ws = new WebSocket("ws://localhost:8000/websocket");
ws.onopen = function() {
ws.send(formdata);
};
ws.onmessage = function (evt) {
alert(evt.data);
};
e.preventDefault();
})
})
</script>
<form action="/websocket" method="post" id="frm">
{% raw xsrf_form_html() %}
<input type="text" name="nom" autofocus>
<button class="ui primary button">Envoyer</button>
</form>
I have tried a simple ajax way, and got:
class AjaxHandler(tornado.web.RequestHandler):
def post(self):
print self.request.body
#gives: _xsrf=2%7C0d466237%7Cf762cba35e040d228518d4feb74c7b39%7C1456505450&nom=hello+there
My question: How to get the user input using websocket?
A websocket message is not a new HTTP request. self.request (and related methods like self.get_argument()) refer to the HTTP request that opened the websocket and do not change when new messages arrive. Instead, the only thing you get with a websocket message is the message argument to on_message(). This contains the data that was sent by the javascript, and you must parse it yourself.
def on_message(self, message):
args = {}
tornado.httputil.parse_body_arguments("application/x-www-form-urlencoded", message, args, {})
print(args["nom"][0])
You may want to use JSON instead of form encoding for this; it is generally easier to work with when you don't need backwards-compatibility with plain HTML form submission.

Categories