Signature.py: application lauréate du Shiny Contest 2024

Author : Arthur Bréant
Categories : développement, shiny
Tags : posit, python, scss, shiny, Tests
Date :

🏆 Nous avons le plaisir d’annoncer que {signature.py} est la grande gagnante du Shiny Contest 2024 dans la catégorie ‘Meilleure application Shiny avec Python’ !

Cette année, Posit a relancé le Shiny Contest, un concours dédié au développement d’applications Shiny. Les participants doivent créer une application personelle ou professionelle, qui répond à un problème spécifique. Les applications sont ensuite jugées par un jury de professionnels.

Les applications sont évaluées selon plusieurs critères : la qualité de l’interface utilisateur, la qualité du code et l’aspect technique général de l’application. L’usage de l’application et la narration sont également pris en compte.

Le concours est ouvert à tous les développeurs, qu’ils soient débutants ou expérimentés. Ce concours, ThinkR le connaît bien. En effet, les applications {hexmake} et {wedding} furent les gagnantes de leurs éditions respectives.

Cette année, ThinkR a décidé de participer à nouveau au concours. Cette fois, avec une application Shiny avec Python.

{Signature.py} : une application Shiny avec Python

Nous avons le plaisir d’annoncer que notre application, {signature.py} a été sélectionnée comme l’application gagnante du concours dans la catégorie “Meilleure application Shiny avec Python”. Les résultats et les autres applications sont disponibles directement depuis la page de Posit.

L’objectif de {signature.py} est de générer des signatures de mails pour l’équipe de ThinkR. Elle permet de créer une signature d’email rapidement avec les images et les éléments de l’entreprise. Enfin, une fois tous les éléments renseignés, la signature peut être collée directement dans les paramètres du client mail.

Autre point positif de cette application, les membres de l’équipe n’ont plus besoin de mettre à jour l’image de la bannière car celle-ci est automatiquement mise à jour à partir de l’URL où elle est stockée. Il suffit de mettre à jour l’image sur le serveur pour que toutes les signatures soient actualisées. C’est un gain de temps considérable pour l’équipe.

Consulter l’application en ligne : Signature.py

Création de l’application

Pour créer cette application, nous avons décidé de suivre nos meilleures pratiques de développement d’applications Shiny. Celles que nous utilisons et que nous enseignons dans nos formations.

Création d’une maquette pour cette application

Nous avons commencé par créer une maquette avant de coder l’application. Ainsi, nous avons respecté les différentes étapes de conception d’une maquette :

La création d’une maquette basse définition (lo-fi) jusqu’à la création d’une maquette haute définition (hi-fi).

Lofi

maquette lo-fi

maquette hi-fi

Nous avons écrit un article sur notre blog (en français uniquement) sur l’importance des maquettes dans le développement d’applications. Pour cette application, nous n’avons pas dérogé à la règle.

Pour construire la maquette, nous avons utilisé Figma. La maquette est disponible ici.

Construire la maquette avant de coder l’application permet de : mieux comprendre les besoins de l’utilisateur (ici celui de l’équipe de ThinkR) et de mieux organiser le code de l’application.

Passer un peu de temps à cette étape permet à coup sûr de gagner du temps lors de la phase de développement.

Création de l’application Shiny avec Python

Pour créer cette application, nous avons utilisé la nouvelle librairie {shiny} de Python.

Tout le code de cette application est disponible en open-source sur notre dépôt GitHub : ThinkR-open/signature.py.

Voici comment se présente le code de cette application :

Structure du code

Le coeur de l’application est dans le dossier signature.

Le fichier app.py est le fichier principal de l’application. Il contient le code de l’application.

A l’image de nos applications Shiny en R, l’application est divisée en deux parties : l’interface utilisateur et le serveur.

Interface utilisateur

L’interface utilisateur est définie dans app_ui. La librairie {shiny} de Python permet de définir l’interface utilisateur à l’aide de la classe ui qui permet de générer les différents éléments de l’interface utilisateur.

app_ui = ui.div(
    ui.div(
    ui.page_fixed(
        ui.head_content(
            ui.tags.title("signature.py"),
        ui.include_css(current_dir / "css" / "signature.css"),
        ui.include_js(current_dir / "js" / "signature.js"),
        mod_navbar.navbar_ui("nav_signature"),
        ui.div(
            ui.div(
                mod_form.form_ui("form_signature"),
                mod_preview.preview_ui("preview_signature"),
                class_="row",
            ),
            class_="container",
        ),
    ),
)

Exactement comme les applications Shiny en R, il est possible de diviser son application en plusieurs modules. Cela permet de mieux organiser son code et de le rendre plus lisible.

Ainsi, le dossier modules contient les différents modules de l’application. Chaque module est un fichier composé de deux fonctions : une fonction pour l’interface utilisateur et une fonction pour le serveur.

Serveur

Côté serveur, l’application est définie dans app_server. La libraire {shiny} de Python permet de définir le serveur à la fonction server.

Comme pour nos best tips en R, on peut également utiliser la stratégie du petit r en Python.

def server(input: Inputs, output: Outputs, session: Session):
    reactive_values = reactive.Value(
        {
            "firstname",
            "lastname",
            "jobtitle",
            "email",
            "email_url",
            "phone",
            "phone_url",
        }
    )

    mod_form.form_server("form_signature", reactive_values=reactive_values)
    mod_preview.preview_server(
        "preview_signature", current_dir=current_dir, reactive_values=reactive_values
    )

Donner du style à l’application

Ici rien ne change entre R et Python, Shiny embarque toujours nativement la bibliothèque CSS Bootstrap.

On peut donc utiliser les classes offertes par Bootstrap pour donner du style à l’application.

app_ui = ui.div(
    ui.div(
        ui.div(
            ui.span("🏆 ", class_="fs-5"),
            ui.span(
                "This project won the 2024 Shiny Contest for the best Shiny application with Python. ",
                class_="fs-6",
            ),
            ui.a(
                "Click here for more information 🔗 ",
                href="https://posit.co/blog/winners-of-the-2024-shiny-contest/",
                target="_blank",
                class_="text-white",
            ),
            class_="container",
        ),
        class_="sticky-top bg-success text-white p-3",
    )
)

Ce morceau de code permet d’ajouter un bandeau vert en haut de l’application, position sticky qui signifie qu’il reste en haut de la page même si on scroll. Sur ce bandeau, on a ajouté un peu d’espace (padding) avec p-3 et du texte en blanc text-white.

On a modifié les couleurs de l’application pour les adapter à l’identité visuelle de ThinkR. Cela se passe dans le dossier scss et le fichier signature.scss.

Dans ce fichier, les couleurs sont définies dans des variables Sass. Ces variables sont ensuite réutilisées dans le fichier :

$primary: #b8b8dc;
$secondary: #f15522;
$info: #494955;
$close: #ff5f57;
$minimize: #febc2e;
$zoom: #27c840;

.navbar {
  padding: 1.5em 0;

  .navbar-brand {
    font-size: 1.5em;
    font-family: "Permanent Marker", cursive;
    pointer-events: none;
    color: $secondary;
  }
}

Copier la signature de mail

Pour copier la signature de mail, nous avons utilisé une librairie javascript externe : {clipboard}. Cette librairie permet de copier du texte dans le presse-papier.

$(document).ready(function () {
  $("#preview_signature-copy").click(function () {
    new Clipboard("#preview_signature-copy");
  });
});

Pour que ce fichier soit pris en compte dans l’application, comme pour le fichier CSS, il faut l’inclure l’UI de l’application :

ui.include_css(current_dir / "css" / "signature.css")
ui.include_js(current_dir / "js" / "signature.js")

Le template de signature

Pour générer la signature, nous avons utilisé un template HTML. Ce template est stocké dans le dossier template et le fichier template.html.

En R, ce serait l’équivalent de l’utilisatin de la fonction htmlTemplate de {shiny}.

Bien documenté en R, cette fonctionnalité est pour le moment absente de la documentation de {shiny} en Python.

Néanmoins, voici comment signature.py utilise le template HTML :

La prévisualisation de la signature est générée à partir du template HTML. Le template est lu et les valeurs sont remplacées par les valeurs renseignées dans l’application dans la reactive value reactive_values. Cette réactive value est initiée dans app.py et est passée aux modules mod_form et mod_preview.

reactive_values = reactive.Value(
  {
    "firstname",
    "lastname",
    "jobtitle",
    "email",
    "email_url",
    "phone",
    "phone_url",
  }
)

Lors de la saisie d’une information dans le formulaire, la reactive value est mise à jour.

@module.server
def form_server(input: Inputs, output: Outputs, session: Session, reactive_values):
    @reactive.effect
    @reactive.event(
        input.firstname, input.lastname, input.job_title, input.email, input.phone
    )
    def _():
        reactive_values.set(
            {
                "firstname": input.firstname(),
                "lastname": input.lastname(),
                "job_title": input.job_title(),
                "email": input.email(),
                "email_url": f"mailto:{input.email()}",
                "phone": input.phone(),
                "phone_url": f"tel:{input.phone()}",
            }
        )

A la fin, le template est lu et les valeurs sont remplacées par les valeurs renseignées dans l’application. Pour cela, nous utilisons la libraie Python {jinja2}. Nous récupérons le template ainsi que les valeurs renseignées dans l’application et nous les passons au template.

Le template est ensuite rendu dans l’application.

def preview_server(
    input: Inputs, output: Outputs, session: Session, current_dir, reactive_values
):
    env = Environment(loader=FileSystemLoader(current_dir))
    template = env.get_template("template/template.html")

    @render.text
    def render_template() -> str:
        print(reactive_values())

        first_name = reactive_values().get("firstname")
        last_name = reactive_values().get("lastname")
        job_title = reactive_values().get("job_title")
        email = reactive_values().get("email")
        email_url = reactive_values().get("email_url")
        phone = reactive_values().get("phone")
        phone_url = reactive_values().get("phone_url")

        rendered_template = template.render(
            firstname="{{firstname}}" if first_name == "" else first_name,
            lastname="{{lastname}}" if last_name == "" else last_name,
            job_title="{{job_title}}" if job_title == "" else job_title,
            email="{{email}}" if email == "" else email,
            phone="{{phone}}" if phone == "" else phone,
            email_url="{{email_url}}" if email_url == "" else email_url,
            phone_url="{{phone_url}}" if phone_url == "" else phone_url,
        )
        return rendered_template

Les tests

Nous continuons à suivre les bonnes pratiques de développement que nous connaissons en R, pour les réutiliser en Python. Nous avons utilisé la librairie {pytest} pour écrire des tests unitaires.

Les tests sont stockés dans le dossier tests/pytest-unit et les fichiers : test_accordion_panels.py, test_one_plus_one.py (il faut bien commencer quelque part).

Ici, il s’agit des tests unitaires. C’est à dire que nous cherchons à tester une fonction ou un module en particulier. Il s’agit de tester plutôt le comportement d’une fonction que le comportement de l’application dans son ensemble. Ce seront donc plutôt des tests busines / métiers.

Un test en Python ressemble à cela :

def test_one_plus_one():
    assert 1 + 1 == 2

En parallèle, nous avons également écrit des tests End-to-End (E2E). Ces tests permettent de tester l’application dans son ensemble. Ils permettent de vérifier que l’application fonctionne correctement. L’objectif ici est de simuler le comportement de l’utilisateur. Pour cela, nous utilisons la librairie {playwright} qui permet de simuler le comportement de l’utilisateur.

Ces tests permettent de garantir que l’application fonctionne correctement dans son ensemble en testant l’intégration des différents modules. Contrairement aux tests unitaires, qui se concentrent sur des fonctions ou des composants isolés, les tests E2E simulent un scénario utilisateur complet. Par exemple, ils peuvent vérifier que remplir un formulaire met bien à jour les données et génère une signature conforme. Cela permet de détecter des erreurs qui pourraient survenir lors des interactions entre les différents modules, renforçant ainsi la fiabilité et l’expérience utilisateur globale de l’application.

Les tests E2E sont stockés dans le dossier tests/pytest-playwright et le fichier test_e2e.py.

Un test E2E en Python ressemble à cela :

from shiny.run import ShinyAppProc
from playwright.sync_api import Page, expect
from shiny.pytest import create_app_fixture

app = create_app_fixture("../../app.py")


def test_signature(page: Page, app: ShinyAppProc):
    page.goto(app.url)
    response = page.request.get(app.url)
    expect(response).to_be_ok()
    expect(page).to_have_title("signature.py")

Intégration continue

Jusqu’au bout nous utiliserons les bonnes pratiques de développement. Nous avons également mis en place un pipeline d’intégration continue pour cette application.

Le pipeline d’intégration continue est stocké dans le fichier .github/workflows/run-pytest.yaml. Ce fichier contient les différentes étapes du pipeline d’intégration continue.

A chaque push sur le dépôt GitHub, le pipeline est déclenché. Il exécute les tests unitaires et les tests E2E. Si les tests passent, le pipeline est vert. Sinon, il est rouge.

name: Unit tests + E2E tests

jobs:
  pytest:
    runs-on: ubuntu-latest

    steps:

      ...

      - name: 🧪 Run tests
        run: poetry run pytest --github-report -vvv --browser webkit --browser chromium --browser firefox

Ceci est un excellent moyen de s’assurer que l’application fonctionne correctement avant de la déployer en production. Ici, l’application est testée sur trois navigateurs : Webkit, Chromium et Firefox.

Mise à jour de l’image de la bannière

Pour mettre à jour l’image de la bannière, il suffit de mettre à jour l’image dans le repo GitHub. L’image est stockée dans le dossier signature/assets et le fichier current_banner.png.

Une fois l’image mise à jour, cela impactera directement l’application et toutes les signatures générées avec l’application.

Déploiement de l’application sur nos serveurs

Cette application est déployée sur nos serveurs. Vous pouvez la consulter en ligne : signature.py.

Cette application Python vit sur nos serveurs à côté de nos autres applications R. N’hésitez pas à nous contacter pour déployer vos applications Python ou R en production.

Aller plus loin

R, Python le combat entre les deux langages de programmation est un débat sans fin. Pourquoi choisir entre l’un ou l’autre ? Pourquoi ne pas utiliser les deux ?

Cette application Python, nous l’avons également réalisée en R. Vous pouvez consulter l’application en ligne : signature.r.

Mais nous l’avons également réalisée en JavaScript Vanilla. Vous pouvez consulter l’application en ligne : signature.js.

Que ce soit pour l’application R, Python ou Javascript, les codes sources sont disponibles sur notre dépôt GitHub. Explorez le code de ThinkR-open/signature.py, de ThinkR-open/signature.r ou de ThinkR-open/signature.js.

Si vous souhaitez réutiliser cette application pour vous, clonez ces applications et personnalisez-les pour répondre à vos besoins ! Il vous faudra l’adapter à votre entreprise en modifiant les images et éventuellement les textes et couleurs.

Vous pouvez également nous contacter pour nous demander de l’aide pour adapter cette application à vos besoins. Nous serons ravis de vous aider.

Avec {signature.py}, nous avons voulu tester la nouvelle bibliothèque prometteuse {shiny} de Python. C’est chose faite et nous sommes ravis de cete expérience. A la fin, nous avons pu la reproduire en R et en JavaScript. Signe que les applications Shiny répondent à un besoin et que les langages de programmation ne sont que des outils pour y répondre.


Comments


Also read