Home Articles Categories Series
Pythonise Just now

Automated build tool & task runner with Flask & uWSGI

Prototype idea - An automated build tool using uWSGI to watch files & directories for modifications


Article Posted on by in Python
Julian Nash · 7 months ago in Python

Here's an idea and initial proof of concept - uWSGI and Flask as an automatic build tool, using uWSGI's filemon decorator to watch files for changes.

Any file (or directory) modifications trigger a function, minifying CSS, JavaScript, compiling SCSS to CSS, linting, code formatting etc..

The uWSGI server is started in a new terminal and runs in the background, functions are triggered when modifications are made to the desired files.

Scenario
  • You're already using Flask and uWSGI in your project
  • You want to minify assets, such as CSS & JavaScript
  • You want to use a CSS pre-processor
  • You want to automate linting & code formatting on the fly
  • You want to automate anything on a file/directory modification

Runner

In this initial proof of concept, tuples containing source and destination files are each accompanied by a function, using the source as the decorator argument.

The argument passed to the @filemon decorator is the file or directory to watch for changes.

Here's the quick and dirty prototype:

from flask import Flask, render_template, make_response
from uwsgidecorators import filemon
import click

import rcssmin
import rjsmin
import sass

from datetime import datetime

"""
To start thr runner:
uwsgi runner.ini
"""

app = Flask(__name__)

# compile SCSS to CSS: source > destination
sass_map = ("app/static/scss/style.scss", "app/static/css/style.css")

# Minify JavaScript: source > destination
js_map = ("app/static/js/app.js", "app/static/js/app.min.js")

# Minify CSS: source > destination
css_map = ("app/static/css/style.css", "app/static/css/style.min.css")

task_history = list()


@filemon(sass_map[0])
def scss_watcher(file):

    """ Compiles SCSS to CSS on file modification """

    click.secho(f"\nDetected change in {sass_map[0]}", fg="green")
    print("--------------------")
    print("Compiling SCSS to CSS:")

    source, dest = sass_map
    with open(dest, "w") as outfile:
        outfile.write(sass.compile(filename=source))

    print(f"{source} > {dest}", end="\n")

    task_history.append(
        {
            "type": "Compiled SCSS to CSS",
            "src": source,
            "dest": dest,
            "time": datetime.utcnow(),
        }
    )


@filemon(js_map[0])
def js_watcher(file):

    """ Minifies JavaScript on file modification """

    click.secho(f"\nDetected change in {js_map[0]}", fg="yellow")
    print("--------------------")

    print("Minifying Javascript files:")

    source, dest = js_map
    with open(source, "r") as infile:
        with open(dest, "w") as outfile:
            outfile.write(rjsmin.jsmin(infile.read()))

    print(f"{source} > {dest}", end="\n")

    task_history.append(
        {
            "type": "Minified JavaScript file",
            "src": source,
            "dest": dest,
            "time": datetime.utcnow(),
        }
    )


@filemon(css_map[0])
def css_watcher(file):

    """ Minifies CSS on file modification """

    click.secho(f"\nDetected change in {css_map[0]}", fg="cyan")
    print("--------------------")
    print("Minifying CSS files:")

    source, dest = css_map
    with open(source, "r") as infile:
        with open(dest, "w") as outfile:
            outfile.write(rcssmin.cssmin(infile.read()))

    print(f"{source} > {dest}", end="\n")

    task_history.append(
        {"type": "Minified CSS", "src": source, "dest": dest, "time": datetime.utcnow()}
    )



@app.route("/")
def runner_web():

    """ A simple route that displays tasks run since reload """

    if len(task_history) == 0:
        res = "No tasks run"
    else:
        res = "Tasks run since reload:\n"
        for t in task_history:
            res += "[ runner.py ]"
            res += f" task: {t['type']}"
            res += f" src: {t['src']}"
            res += f" dest: {t['dest']}"
            res += f" time: {str(t['time'])}\n"

    response = make_response(res)
    response.headers["Content-Type"] = "text"

    return response

uWSGI config

Here's the uWSGI config file:

[uwsgi]
strict = true
http = :8080

wsgi-file = runner.py
callable = app

master = true
processes = 1
py-autoreload = 2

Starting the runner

uwsgi runner.ini

Testing

Initial testing works as expected. SCSS is compiled and CSS/JS files are minified. Here's the text logged to the browser after a few file modifications:

Tasks run since reload:
[ runner.py ] task: Compiled SCSS to CSS src: app/static/scss/style.scss dest: app/static/css/style.css time: 2019-04-18 21:30:36.448970
[ runner.py ] task: Compiled SCSS to CSS src: app/static/scss/style.scss dest: app/static/css/style.css time: 2019-04-18 21:30:36.619307
[ runner.py ] task: Minified CSS src: app/static/css/style.css dest: app/static/css/style.min.css time: 2019-04-18 21:30:36.625491
[ runner.py ] task: Minified CSS src: app/static/css/style.css dest: app/static/css/style.min.css time: 2019-04-18 21:30:36.629975
[ runner.py ] task: Minified CSS src: app/static/css/style.css dest: app/static/css/style.min.css time: 2019-04-18 21:30:36.635917
[ runner.py ] task: Minified CSS src: app/static/css/style.css dest: app/static/css/style.min.css time: 2019-04-18 21:30:36.641563

Wrapping up

I know there's lot's of very good task runners/build tools out there, this was a curiosity and some fun. After experimenting with some of the uWSGI decorators in a previous article, I thought this kind of project might have some potential for projects already using Flask and uWSGI (without Node/NPM).

:TODO - Revisit this.

Last modified · 18 Apr 2019
Did you find this article useful?
This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License
Contents
Loading...