Search code examples
pythonflaskjinja2

"AssertionError: applications must write bytes" when streaming data using Python Flask


I wrote the following Python code in the app_consumer.py and tasks_consumer.py files to stream data to the consumer.html Jinja template.

app_consumer.py

from flask import render_template, Response, request
from flask_socketio import join_room
from init_consumer import app, socketio
import tasks_consumer
import uuid

def render_template_stream(template_name, **context): 
    app.update_template_context(context) 
    t = app.jinja_env.get_template(template_name) 
    rv = t.stream(context)
    rv.enable_buffering(5)
    return rv

@app.before_request
def initialize_params():
    if not hasattr(app.config,'uid'):
        sid = str(uuid.uuid4())
        app.config['uid'] = sid
        print("initialize_params - Session ID stored =", sid)

@app.route("/", methods=['GET'])
def index():
    return render_template('consumer.html', stockInfo = {})
       
@app.route('/consumetasks', methods=['GET','POST'])
def getStockStatus():
    if request.method == 'POST':
        print("Retrieving stock status")
        return Response(render_template_stream('consumer.html', stockInfo = tasks_consumer.sendStockStatus()))     
    elif request.method == 'GET':
        return '''
         <!doctype html>
        <html>
            <head>
                <title>Stock Sheet</title>
                <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
            </head>

            <body class="container">
                <h1>Stock Sheet</h1>
                <div>
                    <button id="consumeTasks">Check stock status</button>
                </div>
            </body>
        </html>
        '''

# Run using port 5001
if __name__ == "__main__":
    socketio.run(app,host='localhost', port=5001,debug=True)

tasks_consumer.py

import csv
from flask import request, stream_with_context
from init_consumer import app, socketio
import json

# Receive the webhook requests and emit a SocketIO event back to the client
def send_message(data):
    status_code = 0
    if request.method == 'POST':
        roomid = app.config['uid']
        msg = json.dumps(data)
        event = "Send_stock_status"
        socketio.emit(event, msg, namespace = '/collectHooks', room = roomid)
        status_code = 200
    else:
        status_code = 405 # Method not allowed
    return status_code

# Retrieve the stock status of the products sent through the webhook requests and return them back to the client. 
@app.route('/consumetasks', methods=['POST'])
def sendStockStatus():
    stockList = [] # List of products in stock
    with open("NZ_NVJ_Apparel_SKUs_sheet.csv", newline='') as csvFile:
        stockReader = csv.reader(csvFile, delimiter=',', quotechar='"')
        for row in stockReader:
            stockList.append(row[0])
    
    stockSheet = {} # Dictionary of products sent in the request and their stock status

    def generateStockStatus():
        request_data = request.get_json()
        if request_data:
            if 'SKU' in request_data:
                stockRequest = request_data['SKU'] # List of products sent in the request
                for stock in stockRequest:
                    if stock in stockList:
                        stockStatus = "In Stock"
                        stockSheet.update({str(stock):stockStatus})
                        send_message(stockSheet)
                        yield stock, stockStatus
                    else:
                        stockStatus = "Out of Stock"
                        stockSheet.update({str(stock):stockStatus})
                        send_message(stockSheet)
                        yield stock, stockStatus
    return stream_with_context(generateStockStatus())

When I ran the app_consumer.py file, I got the following output:

127.0.0.1 - - [02/Jul/2023 00:28:02] "GET / HTTP/1.1" 200 -
initialize_params - Session ID stored = 69b0e5e8-d5ea-4279-88d1-9653007662d5
emitting event "Send_stock_status" to 69b0e5e8-d5ea-4279-88d1-9653007662d5 [/collectHooks]
127.0.0.1 - - [02/Jul/2023 00:28:05] "POST /consumetasks HTTP/1.1" 200 -

followed by the error:

Error on request:
Traceback (most recent call last):
  File "C:\Users\yuanl\AppData\Local\Programs\Python\Python311\Lib\site-packages\werkzeug\serving.py", line 364, in run_wsgi
    execute(self.server.app)
  File "C:\Users\yuanl\AppData\Local\Programs\Python\Python311\Lib\site-packages\werkzeug\serving.py", line 328, in execute
    write(data)
  File "C:\Users\yuanl\AppData\Local\Programs\Python\Python311\Lib\site-packages\werkzeug\serving.py", line 296, in write
    assert isinstance(data, bytes), "applications must write bytes"
AssertionError: applications must write bytes

Note that both the stock and stockStatus variables are of type string. A sample stockSheet looks like this: {'PFMTSHIRT_CN_WM_CLS_L_OLIVEDRAB': 'Out of Stock'}

I initially thought the error was generated by the yield statements in the generateStockStatus() function in tasks_consumer.py. Therefore I tried to change the yield statements to yield str(stock), str(stockStatus), yield stock.encode('utf-8'), stockStatus.encode('utf-8') and yield bytes(stock, 'utf-8'), bytes(stockStatus, 'utf-8'), however the error persisted.

I then thought the error was generated by the return statement after the elif request.method == 'GET': statement in the app_consumer.py file, therefore I added the .encode() method on the string returned, however I got a new error:

TypeError: Object of type bytes is not JSON serializable.

Could anyone point me in the right direction in regards to fixing the error?


Solution

  • You should yield only one value at a time. Such as this;

    yield stock
    yield stockStatus
    

    For more information, you can check this answer.