Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to get client to read variable from server with flask

Tags:

python

flask

I am in the process of making my first ever flask/python web app. The app initially displays a form which the user is invited to fill in, then they click on a "submit" button, then the server runs the simulation and creates a PNG file with a graph showing the results, then finally the page is redrawn with the graph displayed. My python code is roughly of this form:

# flask_app.py

@app.route("/", methods=["POST", "GET"])
def home():

    if request.method == 'POST':
        # bunch of request.form things to scoop the contents of the form

    if form_answers_all_good:
        for i in range(huge_number):
            # some maths
        # create png file with results

    return render_template("index.htm", foo=bar)

The program is working just fine, but the huge_number loop can take several tens of seconds. So what I would like is some sort of progress indicator - it doesn't have to be a slick animation - even a string readout of the percentage progress would be fine.

Presumably I can change my for loop to something like...

    for i in range(huge_number):
        # some maths
        percentage_done = str(i * 100/huge_number)

and then somehow arrange on the client side to read (poll?) percentage_done so that I put something like:

Completed {% percentage_done %}% so far.

in my index.htm. BTW, my knowledge of things like Javascript, AJAX or come to think of it, almost anything on the client side (apart from HTML) is beginner level.

I have seen a lot of explanations of similar sounding problems but generally they are doing far more complex things than I actually need and I fail to understand them because of my lack of client side knowledge. So for example some solutions might include a snippet of code and I won't actually know where to put it, or I won't know that something else needs to be loaded first in order for the snippet to work.

EDIT: I am hosting my web app on pythonanywhere.com. The list of included modules is here.

EDIT: pythonanywhere.com does not allow streaming :-(

like image 892
Mick Avatar asked Dec 18 '22 13:12

Mick


1 Answers

You mention you're new to flask, so I am assuming you're new to flask but comfortable with python and also you are very constrained on what you can use because you're using pythonanywhere. One of the main things is it is single threaded so it makes really hard to scale anything. Also it would be better sticking with pure python as managing the dependencies in python anywhere would be an extra problem to care about, and in the end it's doable to do it using only python builtins.

I focused to show a working solution that you simple copy and paste in pythonanywhere or in your local, rather then showing snippets of code. I will try to:

  1. show the working solution
  2. describe how to replicate it
  3. breakdown the main components and briefly explain

(1) The Working solution

you can access it here(I made a lot of restrictions to try to avoid people from breaking it). The solution just involves two files, flask_app.py and index.html

How it looks like

(1.1) solution code

"./home/{username}/{flask_foldername}/flask_app.py"

from queue import Queue
import time
import random
import threading
from PIL import Image
import flask
from flask import request
import json
import io
import uuid
import base64

### You create a Queue and start a scheduler, Start flask after that
def run_scheduler(app):
    sleep_time = 5
    while True:
        time.sleep(sleep_time)
        print("\n"*5)
        print(f'images Completed:{app.images_completed}')
        print('-----'*20)
        if(app.images_toBe_processed.qsize() > 0):
            next_image_name = app.images_toBe_processed.get()
            print(f"No Images being processed so scheduler will start processing the next image {next_image_name} from the queue")
            app.function_to_process_image(next_image_name, app)
        else:
            pass

def function_to_process_image(image_name, app):
    huge_number = 5
    R = random.randint(0,256)
    G = random.randint(0,256)
    B = random.randint(0,256)
    for i in range(huge_number):
        # some maths
        percentage_done = str((i+1)*100/huge_number)
        app.images_processing_status[image_name] = percentage_done
        time.sleep(1)
    app.images_processing_status[image_name] = str(100.0)
    img = Image.new('RGB', (60, 30), color =(R,G,B))
    b=io.BytesIO()
    img.save(b, "jpeg")
    app.images_completed[image_name] = {"status":1,"file": b}
    print(f"IC from function: {app.images_completed} **************************")
    if app.images_processing_status.get("!!total!!",False): app.images_processing_status["!!total!!"]+= 1
    else: app.images_processing_status["!!total!!"] = 1
    del app.images_processing_status[image_name]
    return 0 #process sucessful

class Webserver(flask.Flask):
    def __init__(self,*args,**kwargs):
        scheduler_func = kwargs["scheduler_func"]
        function_to_process_image = kwargs["function_to_process_image"]
        queue_MAXSIZE = kwargs["queue_MAXSIZE"]
        del kwargs["function_to_process_image"], kwargs["scheduler_func"], kwargs["queue_MAXSIZE"]
        super(Webserver, self).__init__(*args, **kwargs)
        self.start_time = time.strftime("%d/%m/%Y %H:%M")
        self.queue_MAXSIZE = queue_MAXSIZE
        self.active_processing_threads = []
        self.images_processing_status = {}
        self.images_completed = {}
        self.images_toBe_processed = Queue(maxsize=queue_MAXSIZE)
        self.function_to_process_image = function_to_process_image
        self.scheduler_thread = threading.Thread(target=scheduler_func, args=(self,))


app = Webserver(__name__,
                  template_folder="./templates",
                  static_folder="./",
                  static_url_path='',
                  scheduler_func = run_scheduler,
                  function_to_process_image = function_to_process_image,
                  queue_MAXSIZE = 20,
                 )


### You define a bunch of views
@app.route("/",methods=["GET"])
def send_index_view():
    if not flask.current_app.scheduler_thread.isAlive():
        flask.current_app.scheduler_thread.start()
    return flask.render_template('index.html',queue_size = flask.current_app.images_toBe_processed.qsize(),
                                max_queue_size =flask.current_app.queue_MAXSIZE , being_processed=len(flask.current_app.active_processing_threads),
                                total=flask.current_app.images_processing_status.get("!!total!!",0), start_time=flask.current_app.start_time )

@app.route("/process_image",methods=["POST"])
def receive_imageProcessing_request_view():
    image_name = json.loads(request.data)["image_name"]
    if(flask.current_app.images_toBe_processed.qsize() >= flask.current_app.queue_MAXSIZE ):
        while(not flask.current_app.images_toBe_processed.empty()):
            flask.current_app.images_toBe_processed.get()
    requestedImage_status = {"name":image_name, "id":uuid.uuid1()}
    flask.current_app.images_toBe_processed.put(image_name)
    return flask.jsonify(requestedImage_status)

@app.route("/check_image_progress",methods=["POST"])
def check_image_progress():
    print(f'Current Image being processed: {flask.current_app.images_processing_status}')
    print(f'Current Images completed: {flask.current_app.images_completed}')
    image_name = json.loads(request.data)["image_name"]
    is_finished = flask.current_app.images_completed \
                                   .get(image_name,{"status":0,"file": ''})["status"]
    requestedImage_status = {
            "is_finished": is_finished,
            "progress":    flask.current_app.images_processing_status.get(image_name,"0")
            }
    return flask.jsonify(requestedImage_status) #images_processing_status[image_name]})

@app.route("/get_image",methods=["POST"])
def get_processed_image():
    image_name = json.loads(request.data)["image_name"]
    file_bytes = flask.current_app.images_completed[image_name]["file"] #open("binary_image.jpeg", 'rb').read()
    file_bytes = base64.b64encode(file_bytes.getvalue()).decode()
    flask.current_app.images_completed.clear()
    return flask.jsonify({image_name:file_bytes}) #images_processing_status[image_name]})

"./home/{username}/{flask_foldername}/templates/index.html"

<html>
<head>
</head>
<body>
    <h5> welcome to the index page, give some inputs and get a random RGB image back after some time</h5>
    <h5> Wait 10 seconds to be able to send an image request to the server </h5>
    <h5>When the page was loaded there were {{queue_size}} images on the queue to be processed, and {{being_processed}} images being processed</h5>
    <h5> The max size of the queue is {{max_queue_size}}, and it will be reseted when reaches it</h5>
    <h5>A total of {{total}} images were processed since the server was started at {{start_time}}</h5>
    <form>
      <label for="name">Image name:</label><br>
      <input type="text" id="name" name="name" value="ImageName" required><br>
    </form>
    <button onclick="send();" disabled>Send request to process image </button>
    <progress id="progressBar" value="0" max="100"></progress>
    <img style="display:block" />
    <script>
       window.image_name = "";
       window.requests = "";
       function send(){
           var formEl = document.getElementsByTagName("form")[0];
           var input = formEl.getElementsByTagName("input")[0];
           var RegEx = /^[a-zA-Z0-9]+$/;
           var Valid = RegEx.test(input.value);
           if(Valid){
               window.image_name = input.value;
               var xhttp = new XMLHttpRequest();
               xhttp.onload = function() {
                       result=JSON.parse(xhttp.response)
                       window.requests = setTimeout(check_image_progress, 3000);
               };
               xhttp.open("POST", "/process_image", true);
               xhttp.send(JSON.stringify({"image_name":input.value}));
               var buttonEl = document.getElementsByTagName("button")[0];
               buttonEl.disabled = true;
               buttonEl.innerHTML = "Image sent to process;only one image per session allowed";
           }
           else{
               alert("input not valid, only alphanumeric characters");
           }
        }

       function check_image_progress(){
           var xhttp = new XMLHttpRequest();
           xhttp.onload = function() {
                   result=JSON.parse(xhttp.response)
                   var progressBarEl = document.getElementsByTagName("progress")[0];
                   if(progressBarEl.value < result["progress"]){
                       progressBarEl.value=result["progress"];
                   } else {}
                   if(result["is_finished"] == true){
                       clearTimeout(window.requests);
                       window.requests = setTimeout(get_image,5);
                   }
                   else {
                       window.requests = setTimeout(check_image_progress, 3000);
                   }
           };
           xhttp.open("POST", "/check_image_progress", true);
           xhttp.send(JSON.stringify({"image_name":window.image_name}));
        }

       function get_image(){
           var xhttp = new XMLHttpRequest();
           xhttp.onload = function() {
                   result=JSON.parse(xhttp.response)
                   img_base64 = result[window.image_name];
                   var progressBarEl = document.getElementsByTagName("progress")[0];
                   progressBarEl.value=100;
                   clearTimeout(window.requests);
                   var imgEl = document.getElementsByTagName("img")[0];
                   console.log(result)
                   imgEl.src = 'data:image/jpeg;base64,'+img_base64;
           };
           xhttp.open("POST", "/get_image", true);
           xhttp.send(JSON.stringify({"image_name":window.image_name}));
        }
    setTimeout(function(){document.getElementsByTagName("button")[0].disabled=false;},100);
    function hexToBase64(str) {
        return btoa(String.fromCharCode.apply(null, str.replace(/\r|\n/g, "").replace(/([\da-fA-F]{2}) ?/g, "0x$1 ").replace(/ +$/, "").split(" ")));
    }
    </script>
</body>
</html>

(2) How to replicate, and create your own webapp

  1. go to your web app tab in web
  2. scroll down to find the source directory link
  3. click on the flask_app.py and include the flask_app.py code
  4. click on the templates directory, or create if doesn't exists
  5. click on the index.html file and include the index.html code
  6. go back to the web app tab and reload your app

walktrhough

(3) Main components of the app

Some details about pythonanywhere:

  1. Pythoanywhere run a wsgi to start your flask app, it will basically import your app from the flask_app.py and run it.

  2. by default wsgi is run in your /home/{username} folder and not in the /home/{username}/{flask_folder}, you can change this if you want.

  3. Pythonanywhere is single-threaded so you can't rely on sending jobs to background.

The main components to watch out for in the backend:

  • 1) Threads, Flask will be in the main Thread run by wsgi and we will run a child thread scheduler that will keep track of the Queue and schedule the next image to be processed.

  • 2) Flask class: app, the component which handles user requests and send processing requests to the Queue

  • 3) Queue, a Queue that stores in order the request from users to process images

  • 4) Scheduler, The component that decides if a new function process_image call can be run and if yes. It needs to be run in an independent Thread than flask.

  • 5) Encapsulate all those in a custom class Webserver to be able to easily access then (pythonanywhere uses wsgi which makes keeping track of variables created locally hard)

So taking look in the big picture of the code

#lot of imports
+-- 14 lines: from queue import Queue-----------------------------------------------------------------------------------------

# this function will check periodically if there's no images being processed at the moment. 
# if no images are being processed check in the queue if there's more images to be processd
# and start the first one in the queue 
def run_scheduler(app):
+-- 12 lines: sleep_time = 5 -------------------------------------------------------------------------------------------------

# this function do the math and creates an random RGB image in the end.
def function_to_process_image(image_name, app):
+-- 21 lines: {---------------------------------------------------------------------------------------------------------------

# This class encapsulates all the data structures("state") from our application
# in order to easily access the progress and images information 
class Webserver(flask.Flask):
    def __init__(self,*args,**kwargs):
+-- 13 lines: scheduler_func = kwargs["scheduler_func"]-----------------------------------------------------------------------

# Here we're instatiating the class
app = Webserver(__name__,
+--  5 lines: template_folder="./templates",----------------------------------------------------------------------------------
                  queue_MAXSIZE = 20,
                 )


### You define a bunch of views
+-- 39 lines: @app.route("/",methods=["GET"]) --------------------------------------------------------------------------------

the main components of the frontend:

  1. send function which is triggered when user clicks the send request to process image button
  2. check_progress function which is triggered by send function to recurrently request the check_progress view in flask to get info about progress. When processing is over we remove the recurrence.
  3. get_image function which is triggered by check_progress when processing is over ('is_finished' = 1)

big picture of the frontend:

<html>
<head>
</head>
<body>
    <!-- JUST THE INITIAL HTML elements -->
+-- 12 lines: <h5> welcome to the index page, give some inputs and get a random RGB image back after some time</h5>-----------


    <script>
       window.image_name = "";
       window.requests = "";
       function send(){
           // SEND image process request when click button and set a timer to call periodically check_image_process
+-- 20 lines: var formEl = document.getElementsByTagName("form")[0];----------------------------------------------------------
        }

       function check_image_progress(){
           // SEND a request to get processing status for a certain image_name
+-- 18 lines: var xhttp = new XMLHttpRequest();-------------------------------------------------------------------------------
        }

       function get_image(){
           // SEND a request to get the image when image_status 'is_processed' = 1
+--- 13 lines: var xhttp = new XMLHttpRequest();------------------------------------------------------------------------------
        }
    setTimeout(function(){document.getElementsByTagName("button")[0].disabled=false;},100);
    </script>
</body>
</html>
like image 91
Bernardo stearns reisen Avatar answered Mar 02 '23 15:03

Bernardo stearns reisen