DEV Community

Mario García
Mario García

Posted on • Edited on

A web app built with Rust and Python

When I started learning Rust I took a few of my Python projects and rewrote the code with this new language I was checking but then I found out that there wasn't a real alternative for some of the libraries I was using.

After searching on crates.io, two crates got my attention, CPython and PyO3, you can use both for writing Python native modules and running and interacting with Python.

While you can find some examples in their corresponding GitHub repository on how to use Python within your Rust projects, I didn't find so much information on how to call a Python function (in an external library, on a .py file) from Rust, that returns a value or a set of values to be processed.

An answer on a StackOverflow's post helped me preparing a practical example I presented at a local Google I/O Extended event, a Rust web app that connects to Firebase through Python, retrieve data and print it on a HTML document.

By the time I proposed the talk "Web Apps with Rust & Firebase" for Google I/O Extended last year, I didn't know that there isn't a crate to connect to Firebase that actually works. So I started to think about the possibility to build the web app with Rust and only use Python for connecting to Firebase.

Let me explain how I built the project.

Configuration

First of all I created a new Rust project using Cargo:

cargo new rust-python-demo
Enter fullscreen mode Exit fullscreen mode

The above command will create a new directory with the name rust-python-demo, inside this directory you will find a src dir that contains a main.rs file that will later be modified for writing the code of the app, and the file Cargo.toml that is the manifest of the project.

Don't forget to change to the new directory:

cd rust-python-demo
Enter fullscreen mode Exit fullscreen mode

Cargo.toml

Cargo.toml must be modified to look as follows:

[package]
name = "rust-python-demo"
version = "0.1.0"
authors = ["mattdark"]
edition = "2018"

[dependencies]
serde = "1.0.99"
serde_derive = "1.0.99"
serde_json = "1.0.40"
rocket = "0.4.2"

[dependencies.cpython]
version = "0.4"
features = ["python-3-7"]

[dependencies.rocket_contrib]
version = "0.4"
features = ["handlebars_templates"]

[[bin]]
name = "rust-python-demo"
path = "src/main.rs"
Enter fullscreen mode Exit fullscreen mode

The section [package] contains name and version of the app, information of the developer and edition of Rust being used, 2015 or 2018.

[dependencies] section has the list of dependencies required for the project, I'm using Rocket, a web framework for Rust, and Serde, a framework for serializing and deserializing Rust data structures. For CPython you can specify the version of Python you'll be using, and as Rocket has support for both Handlebars and Tera (template engines), you must indicate the one being used for the project, in this case Handlebars.

To assign a specific name to the binary of the app, you can do it in the [[bin]] section, indicating the name of the source code file.

pyproject.toml

pyproject.toml is the file that Poetry uses to manage a Python project and its dependencies. It contains information about the project like name, version, description and authors, in the [tool.poetry] section. The version of Python and list of dependencies must be specified in the [tool.poetry.dependencies] section, for this project I'm using any Python version greater than or equal to 3.7.3 and less than 3.8. To connect to Firebase I'm using the firebase library and the rest of packages are dependencies of it.

The file should look as follows:

[tool.poetry]
name = "rust-python-demo"
version = "0.1.0"
description = ""
authors = ["Mario Garcia <iscmariog@gmail.com>"]

[tool.poetry.dependencies]
python = "^3.7.3"
firebase = "*"
python-jwt = "*"
gcloud = "*"
sseclient = "*"
pycrypto = "*"
requests-toolbelt = "*"

[tool.poetry.dev-dependencies]
pytest = "^5.2"

[build-system]
requires = ["poetry>=0.12"]
build-backend = "poetry.masonry.api"
Enter fullscreen mode Exit fullscreen mode

Python

Why using a specific Python version other than the one available on your system or the one that you installed from the repositories? Some Linux distributions, specially those rolling release ones, will offer the latest stable version of Python available, 3.8.2 at the moment of writing this article.

Other distros will have the latest release of Python 3.6 or Python 3.7 but you will probably need a different version depending on the compatibility and support of the libraries you're using for the project.

So this is the moment when pyenv can help, as it makes possible to have other versions of Python installed on your system in a friendly way.

To list the versions of Python available through pyenv, run the following command:

pyenv install --list
Enter fullscreen mode Exit fullscreen mode

For this project I'm using Python 3.7.7. Before installing the version of Python you'll be using for the project bear in mind that if you want to embed Python in Rust, shared libraries must be built for Python. Run the following command to install Python properly:

env PYTHON_CONFIGURE_OPTS="--enable-shared" pyenv install 3.7.7
Enter fullscreen mode Exit fullscreen mode

After running the above command add the shared libraries directory to the LD_LIBRARY_PATH environment variable as follows:

export LD_LIBRARY_PATH=~/.pyenv/versions/3.7.7/lib/
Enter fullscreen mode Exit fullscreen mode

Poetry

Before installing the dependencies of the project you must specify the version of Python that will be used for Poetry to create the virtual environment of the project, run the following command:

poetry env use 3.7.7
Enter fullscreen mode Exit fullscreen mode

Then install the dependencies of the project by running:

poetry install
Enter fullscreen mode Exit fullscreen mode

This command will also create a virtual environment.

Note: If you don't require a virtual environment for your project, you can run the following command before poetry install:

poetry config virtualenvs.create false
Enter fullscreen mode Exit fullscreen mode

Rust

CPython and PyO3 works with the Nightly version of Rust, so before writing the code of your app change the toolchain of the project by running:

rustup override set nightly
Enter fullscreen mode Exit fullscreen mode

Understanding the code

Directory structure

After configuring the app you will have Cargo.toml, pyproject.toml and the main.rs file in the src directory that it will be modified later to put the code of the app.

In the src directory a python dir must be created to put the Python module that will make the connection to Firebase and retrieve the data from it, named pyrebase.py.

In the root directory the templates and static dirs should be added. The Handlebars templates and the static files (e.g. CSS, JavaScript, Pictures) will be stored in those dirs.

Firebase

Go to console.firebase.google.com and add a new project, I will name it Rust Python Demo.

Then add Firebase to a web app, you will get the following configuration details that you will need to configure the Python module:

var firebaseConfig = {
    apiKey: "APIKEY",
    authDomain: "rust-python-demo.firebaseapp.com",
    databaseURL: "https://rust-python-demo.firebaseio.com",
    projectId: "rust-python-demo",
    storageBucket: "rust-python-demo.appspot.com",
    messagingSenderId: "SENDERID",
    appId: "APPID"
  };
Enter fullscreen mode Exit fullscreen mode

And finally go to Database from the left sidebar and Create database. For this demo I will store a list of random names generated through listofrandomnames.com.

The structure of the database will look as follows, you can import the following JSON to the database for testing:

firebase.json

{
  "speakers" : {
    "speaker1" : {
      "id" : "1",
      "last_name" : "Moore",
      "name" : "Madie"
    },
    "speaker2" : {
      "id" : "2",
      "last_name" : "Nathanson",
      "name" : "Norbert"
    },
    "speaker3" : {
      "id" : "3",
      "last_name" : "Mcconelle",
      "name" : "Major"
    },
    "speaker4" : {
      "id" : "4",
      "last_name" : "Schalk",
      "name" : "Sophia"
    },
    "speaker5" : {
      "id" : "5",
      "last_name" : "Bertrand",
      "name" : "Benny"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The name of the database is speakers as this demo is part of a web app that I'm working on.

Don't forget to configure the rules of your database as follows:

{
  "rules": {
    ".read": true,
    ".write": false
  }
}
Enter fullscreen mode Exit fullscreen mode

Python

src/python/pyrebase.py

import json
from firebase import Firebase
def read_data(self):
    config = {
        "apiKey": "APIKEY",
        "authDomain": "rust-python-demo.firebaseapp.com",
        "databaseURL": "https://rust-python-demo.firebaseio.com",
        "projectId": "rust-python-demo",
        "storageBucket": "rust-python-demo.appspot.com",
        "messagingSenderId": "MESSAGINGSENDERID"
    }
    firebase = Firebase(config)

    speaker = list()
    db = firebase.database()
    all_speakers = db.child("speakers").get()
    for x in all_speakers.each():
        speaker.append(x.val())
    s = json.dumps(speaker)
    return s
Enter fullscreen mode Exit fullscreen mode

For this module I'm using the json and firebase libraries. It only has a function named read_data() that will connect to Firebase and retrieve the data from the database.

Inside the function I defined the config variable that has the configuration details required to connect to Firebase.

After connecting to Firebase I get all the registries stored in the database and append it to the speaker list and convert that list to a JSON structure for Rust to import this data properly.

Rust

src/main.rs

#![feature(proc_macro_hygiene, decl_macro)]

#[macro_use] extern crate rocket;

extern crate rocket_contrib;
extern crate cpython;

use cpython::{Python, PyResult, PyModule};

#[macro_use]
extern crate serde_derive;
extern crate serde_json;

use std::collections::HashMap;
use std::path::{Path, PathBuf};
use crate::handlebars::{to_json};

use rocket::response::NamedFile;
use rocket_contrib::templates::{Template, handlebars};

#[derive(Serialize, Deserialize, Debug)]
pub struct Speakers {
    pub id: String,
    pub name: String,
    pub last_name: String,
}

const FIRE_PY: &'static str = include_str!("./python/pyrebase.py");

#[get("/")]
fn index() -> Template {
    let gil = Python::acquire_gil();
    let py = gil.python();
    let s = run_python(py).unwrap();
    let mut data = HashMap::new();
    data.insert("speakers".to_string(), to_json(&s));
    Template::render("index", &data)
}

fn run_python(py: Python<'_>) -> PyResult<Vec<Speakers>> {
    let m = module_from_str(py, "pyrebase", FIRE_PY)?;
    let out: String = m.call(py, "read_data", (2,), None)?.extract(py)?;
    let speakers: Vec<Speakers> = serde_json::from_str(&out).unwrap();
    Ok(speakers)
}

fn module_from_str(py: Python<'_>, name: &str, source: &str) -> PyResult<PyModule> {
    let m = PyModule::new(py, name)?;
    m.add(py, "__builtins__", py.import("builtins")?)?;

    let m_locals = m.get(py, "__dict__")?.extract(py)?;
    py.run(source, Some(&m_locals), None)?;
    Ok(m)
}

#[get("/<file..>", rank=3)]
fn files(file: PathBuf) -> Option<NamedFile> {
    NamedFile::open(Path::new("static/").join(file)).ok()
}

fn rocket() -> rocket::Rocket {
    rocket::ignite().mount("/", routes![index, files])
    .attach(Template::fairing())
}

fn main() {
    rocket().launch();
}
Enter fullscreen mode Exit fullscreen mode

At first, in the main() function the Rocket app is run through the rocket() function where the routes of the app are mounted and the template Fairing is attached to the app.

As it only loads a HTML document, it has two routes, index and files, the first one points to the index() function where the root of the app is loaded and the last one pointing to the files() function where the static files in the static directory is loaded.

In the index() function, the Python interpreter is embedded and the reference to the Python binary is assigned to the py variable, in the first two lines.

Then the function run_python() is called. In the first line the module_from_str() function is called, where the pyrebase module is loaded.

After that the read_data() Python method is called and the value returned by it is assign to the out variable. Then the out variable is deserialize and the values are converted into a vector using the struct Speakers.

The vector speakers is returned to the index() function where it was called and converted into a HashMap to pass the values to the index template.

Template

templates/index.html.hbs

<body>
    <div class="limiter">
        <div class="container-table100">
            <div class="wrap-table100">
                <div class="table100">
                    <table>
                        <thead>
                            <tr class="table100-head">
                                <th class="column1">ID</th>
                                <th class="column2">Name</th>
                                <th class="column3">Last Name</th>
                            </tr>
                        </thead>
                        <tbody>
                            {{ #each speakers }}
                                <tr>
                                    <td class="column1">{{ id }}</td>
                                    <td class="column2">{{ name }}</td>
                                    <td class="column3">{{ last_name }}</td>
                                </tr>
                            {{ /each }}
                        </tbody>
                    </table>
                </div>
            </div>
        </div>
    </div>
...
</body>
Enter fullscreen mode Exit fullscreen mode

The index HTML document will only display the data retrieved from the database in a table.

Running the app

The code of the app is written so it's time to run the demo built.

The project must be built first, run the following command from the terminal:

poetry run cargo build --release
Enter fullscreen mode Exit fullscreen mode

As the app is being run through a virtual env created by poetry, the poetry run command must be executed first and then the project is built for production with cargo build --release.

Cargo created a binary name rust-python-demo in the target/release directory. To run the app write the following command in the terminal:

poetry run ./target/release/rust-python-demo
Enter fullscreen mode Exit fullscreen mode

Now go to localhost:8000 in your browser. You will see the following screen.
Rust Python Demo

The demo of the web app built with Rust and Python is up and running, to get the code go to gitlab.com/mattdark/rust-python-demo.

Top comments (0)