Search code examples
pythonhtmlcsvjinja2fastapi

How to upload a csv file using Jinja2 Templates and FastAPI , and return it after modifications?


I am using FastAPI to upload a csv file, perform some modifications on it and then return it to the HTML page. I am using Jinja2 as the template engine and HTML in frontend.

How can I upload the csv file using Jinja2 template, modify it and then return it to the client?

Python code

from fastapi.templating import Jinja2Templates
from fastapi import FastAPI, File, UploadFile, Request
from io import BytesIO
import pandas as pd
import uvicorn

app = FastAPI()
templates = Jinja2Templates(directory="templates")

@app.get("/")
def form_post(request: Request):
result = "upload file"
return templates.TemplateResponse('home.html', context={'request': request, 'result': result})

@app.post("/")
def upload(request: Request, file: UploadFile = File(...)):

    contents1 = file.file.read()
    buffer1 = BytesIO(contents1)
    test1 = pd.read_csv(buffer1)
    buffer1.close()
    file.file.close()
    test1 = dict(test1.values)
    
    return templates.TemplateResponse('home.html', context={'request': request, 'result': test1})

if __name__ == "__main__":
    uvicorn.run(app)

HTML code

\<!DOCTYPE html\>
\<html lang="en"\>
\<head\>
\<meta charset="UTF-8"\>
\<title\>RUL_PREDICTION\</title\>
\</head\>
\<body\>
\<h1\>RUL PREDICTION\</h1\>
\<form method="post"\>
\<input type="file" name="file" id="file"/\>
\<button type="submit"\>upload\</button\>
\</form\>
\<p\>{{ result }}\</p\>
\</body\>
\</html\>

Solution

  • The working example below is derived from the answers here, here, as well as here, here and here, at which I would suggest you have a look for more details and explanation.

    Sample data

    data.csv

    Id,name,age,height,weight
    1,Alice,20,62,120.6
    2,Freddie,21,74,190.6
    3,Bob,17,68,120.0
    

    Option 1 - Return modified data in a new CSV file

    app.py

    from fastapi import FastAPI, File, UploadFile, Request, Response, HTTPException
    from fastapi.templating import Jinja2Templates
    from io import BytesIO
    import pandas as pd
    
    app = FastAPI()
    templates = Jinja2Templates(directory='templates')
    
    @app.post('/upload')
    def upload(file: UploadFile = File(...)):
        try:
            contents = file.file.read()
            buffer = BytesIO(contents) 
            df = pd.read_csv(buffer)
        except:
            raise HTTPException(status_code=500, detail='Something went wrong')
        finally:
            buffer.close()
            file.file.close()
    
        # remove a column from the DataFrame
        df.drop('age', axis=1, inplace=True)
        
        headers = {'Content-Disposition': 'attachment; filename="modified_data.csv"'}
        return Response(df.to_csv(), headers=headers, media_type='text/csv')
        
    
    @app.get('/')
    def main(request: Request):
        return templates.TemplateResponse('index.html', {'request': request})
    

    templates/index.html

    <!DOCTYPE html>
    <html>
       <head>
          <meta charset="utf-8">
          <meta name="viewport" content="width=device-width, initial-scale=1">
       </head>
       <body>
          <form method="post" action="/upload"  enctype="multipart/form-data">
             <label for="csvFile">Choose a CSV file</label>
             <input type="file" id="csvFile" name="file" onchange="enableSubmitBtn();"><br><br>
             <input type="submit" id="submitBtn" value="submit" disabled>
          </form>
          <script>
             function enableSubmitBtn() {
                document.getElementById('submitBtn').removeAttribute("disabled");
             }
          </script>
       </body>
    </html>
    

    Option 2 - Return modified data in a new Jinja2 Template

    If you would rather like to return a new Jinja2 template with the modified data instead of a csv file as demonstrated above, you could use the below.

    Method 1

    Use pandas.DataFrame.to_html() to render the DataFrame as an HTML table. You could optionally use the classes parameter in to_html() function to pass a class name, or a list of names, that will be used in a style sheet in your frontend to style the table. Additionally, you could remove the border by specifying border=0 in to_html().

    app.py

    # ... (rest of code is same as in Option 1)
    
    @app.post('/upload')
    def upload(request: Request, file: UploadFile = File(...)):
        # ... (rest of code is same as in Option 1)
    
        context = {'request': request, 'table': df.to_html()}
        return templates.TemplateResponse('results.html', context)
    
    

    templates/results.html

    <!DOCTYPE html>
    <html>
        <body>{{ table | safe }}</body>
    </html>
    

    Method 2

    Use pandas.DataFrame.to_dict() to convert the DataFrame to a dictionary and return it.

    app.py

    # ... (rest of code is same as in Option 1)
    
    @app.post('/upload')
    def upload(request: Request, file: UploadFile = File(...)):
        # ... (rest of code is same as in Option 1)
    
        context = {'request': request, 'data': df.to_dict(orient='records'), 'columns': df.columns.values}
        return templates.TemplateResponse('results.html', context)
    
    

    templates/results.html

    <!DOCTYPE html>
    <html>
        <body>
            <table style="width:50%">
                <tr>
                    {% for c in columns %}<td>{{ c }}</td>{% endfor %}
                </tr>
                {% for d in data %}
                    <tr>
                        {% for v in d.values() %}
                            <td>{{ v }}</td>
                        {% endfor %}
                        <br>
                    </tr>
                {% endfor %}
            </table>
        </body>
    </html>