Search code examples
pythonflaskjinja2flask-socketio

“RuntimeError: working outside of request context" when using a generator to stream data with Flask


I encountered the RuntimeError: working outside of request context error while trying to build a webhook using the Python Flask framework.

This is the app_producer.py file that sends HTTP POST requests:

from flask import Response, render_template
from init_producer import app
import tasks_producer

# Render a template with a given context as a stream and return a TemplateStream
def render_template_stream(template_name, **context):
    app.update_template_context(context) # Update the template context with some commonly used variables. 
    t = app.jinja_env.get_template(template_name) # jinja2.Environment.get_template() # Load a template by name with loader and return a Template.
    rv = t.stream(context) # jinja2.Template.stream # Return a TemplateStream that returns one function after another as strings
    rv.enable_buffering(5) # jinja2.environment.TemplateStream.enable_buffering # Buffer 5 items before yielding them
    return rv # Return a TemplateStream

@app.route("/", methods=['GET'])
def index():
    return render_template('producer.html')

@app.route('/producetasks', methods=['POST'])
def producetasks():
    print("Producing tasks")
    return Response(render_template_stream('producer.html', data = tasks_producer.produce_bunch_tasks()))

# Stop the app.run() function from being automatically executed when the app_producer.py file is imported as a module to another file.
if __name__ == "__main__":
   app.run(host="localhost",port=5000, debug=True)

This is the app_consumer.py file that processes requests sent by the app_producer.py file.

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

# Render a template with a given context as a stream and return a TemplateStream
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

# Registers a function to be run before the first request to this instance of the application
# Create a unique session ID and store it within the application configuration file
@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)

# Render the assigned template file
@app.route("/", methods=['GET'])
def index():
    return render_template('consumer.html', stockSheet = {})
       
@app.route('/consumetasks', methods=['GET','POST'])
def get_stock_status():
    # Handle the POST request
    if request.method == 'POST':
        print("Retrieving stock status")
        return Response(render_template_stream('consumer.html', stockSheet = tasks_consumer.sendStockStatus()))     
    # Handle the GET request
    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)

The tasks_consumer.py file that the app_consumer.py file imports:

import csv
from flask import request
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():
    request_data = request.get_json()
    stockList = [] # List of products in stock
    stockInfo = [] # List of products sent in the request
    stockSheet = {} # Dictionary of products sent in the request and their stock status
    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])
    
    if request_data:
        if 'SKU' in request_data:
            stockInfo = request_data['SKU']
            for stock in stockInfo:
                if stock in stockList:
                    stockStatus = "In Stock"
                    stockSheet.update({str(stock):stockStatus})
                    yield stock, stockStatus
                else:
                    stockStatus = "Out of Stock"
                    stockSheet.update({str(stock):stockStatus})
                    yield stock, stockStatus
    send_message(stockSheet)
    print(stockSheet)

When I ran both the app_producer and app_consumer files to render the consumer.html template file, the line request_data = request.get_json() in the sendStockStatus() function in the tasks_consumer.py file raised the error: RuntimeError: Working outside of request context.

What puzzles me is that I have put the sendStockStatus() function in a view function, but the body of the function still gave me the error Working outside of request context. Could anyone point me in the right direction as to how to fix the error?


Solution

  • When you use a generator to implement a streaming response with Flask, the request context is not available by default. To ensure the context stays around while the generator runs, you have to wrap your generator function with the stream_with_context().