Signature.py: Aplicație premiată la Concursul Shiny 2024

URMĂREȘTE-NE
16,065FaniÎmi place
1,142CititoriConectați-vă

Pentru a crea această aplicație, am decis să o urmăm cele mai bune practici ale noastre pentru dezvoltarea aplicațiilor Shiny. Acestea sunt aceleași practici pe care le folosim și le predăm în atelierele noastre.

Crearea unei machete pentru aplicație

Am început prin a crea o machetă înainte de a codifica aplicația. Acest lucru ne-a permis să urmărim diferitele etape ale designului machetei:

Crearea unui joasă fidelitate (lo-fi) machetă urmată de a de înaltă fidelitate (hi-fi) machetă.

macheta hi-fi

Am scris o postare pe blog (numai în franceză) despre importanța machetelor în dezvoltarea aplicațiilor. Pentru această aplicație, am urmat același proces.

Pentru a construi macheta, am folosit instrumente de ultimă generație, iar macheta poate fi accesată aici.

Construirea machetei înainte de codificare ne ajută: înțelegeți mai bine nevoile utilizatorului (în acest caz, echipa ThinkR) și organizați mai bine codul aplicației.

Petrecerea timpului pe acest pas cu siguranță ajută la economisirea timpului în timpul dezvoltării.

Construirea aplicației strălucitoare cu Python

Pentru această aplicație, am folosit noul {lucios} bibliotecă pentru Python.

Iată o prezentare generală a structurii codului:

Structura codului

Nucleul aplicației este situat în signature pliant.

The app.py fișierul este fișierul principal al aplicației. Conține codul aplicației.

Similar cu aplicațiile noastre Shiny în R, aplicația este împărțită în două părți: interfața cu utilizatorul şi serverul.

Interfață cu utilizatorul

Interfața cu utilizatorul este definită în app_ui. Pitonul {shiny} biblioteca ne permite să definim interfața cu utilizatorul folosind ui clasa, care generează diferitele elemente ale UI.

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",
        ),
    ),
)

La fel ca aplicațiile Shiny în R, este posibil să împărțiți aplicația în mai multe module. Acest lucru ajută la organizarea codului și îl face mai lizibil.

Astfel, cel modules folderul conține diferite module ale aplicației. Fiecare modul este un fișier format din două funcții: una pentru interfața utilizator și una pentru server.

Server

Pe partea de server, aplicația este definită în app_server. Pitonul {shiny} biblioteca definește serverul folosind server funcţie.

La fel ca în R, putem folosi și strategia small-r în 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
    )

Stilizarea aplicației

Aici, nimic nu se schimbă între R și Python. Shiny include încă în mod nativ biblioteca CSS Bootstrap.

Astfel, putem folosi clasele oferite de Bootstrap pentru a stila aplicația.

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",
    )
)

Acest fragment de cod adaugă un banner verde în partea de sus a aplicației, cu o poziție lipicioasă care rămâne în partea de sus a paginii chiar și atunci când derulați. Banner-ul are, de asemenea, o umplutură și o culoare albă a textului.

Am adaptat culorile aplicației pentru a se potrivi cu marca ThinkR în scss folderul și signature.scss fişier.

În acest fișier, culorile sunt definite ca variabile Sass, care sunt reutilizate în toată foaia de stil:

$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;
  }
}

Copierea semnăturii de e-mail

Pentru a copia semnătura de e-mail, am folosit o bibliotecă JavaScript externă: {clipboard}. Această bibliotecă ne permite să copiem text în clipboard.

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

Pentru a ne asigura că acest fișier este inclus în aplicație, la fel ca fișierul CSS, trebuie să includem JS în UI:

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

Șablon de semnătură

Pentru a genera semnătura, am folosit un șablon HTML. Acest șablon este stocat în template folderul în template.html fişier.

În R, acest lucru ar fi echivalent cu utilizarea htmlTemplate functia de la {shiny}.

Bine documentată în R, această caracteristică lipsește în prezent din Python{shiny} documentare.

Cu toate acestea, iată cum signature.py folosește șablonul HTML:

Previzualizarea semnăturii este generată din șablonul HTML. Șablonul este citit, iar valorile sunt înlocuite cu cele introduse în valoarea reactivă a aplicației reactive_values. Această valoare reactivă este inițializată în app.py și a trecut la module mod_form şi mod_preview.

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

Pe măsură ce utilizatorul completează formularul, valoarea reactivă este actualizată.

@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()}",
            }
        )

În cele din urmă, șablonul este citit, iar valorile sunt înlocuite cu valorile introduse. Pentru a face acest lucru, folosim Python {jinja2} bibliotecă. Preluăm șablonul și valorile introduse, apoi le transmitem șablonului.

Șablonul este apoi redat în aplicație.

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

Teste

Continuăm să urmăm cele mai bune practici de dezvoltare pe care le cunoaștem în R și le reutilizam în Python. Noi am folosit {pytest} bibliotecă pentru a scrie teste unitare.

Testele sunt stocate în tests/pytest-unit folder, cu fișierele test_accordion_panels.py, test_one_plus_one.py (trebuie să încep de undeva).

Aici, efectuăm teste unitare. Aceasta înseamnă că ne concentrăm pe testarea unei anumite funcții sau modul. Scopul este de a testa mai degrabă comportamentul unei funcții decât comportamentul întregii aplicații. Aceste teste sunt în primul rând teste legate de afaceri/domeniu.

Un test Python arată astfel:

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

În paralel, am scris și noi De la capăt la capăt (E2E) teste. Aceste teste ne permit să testăm întreaga aplicație. Acestea ajută la asigurarea faptului că aplicația funcționează corect ca întreg. Obiectivul aici este de a simula comportamentul utilizatorului. Pentru a face acest lucru, folosim {playwright} bibliotecă, care ne permite să simulăm interacțiunile utilizatorului.

Aceste teste asigură că aplicația funcționează corect ca întreg prin testarea integrării diferitelor module. Spre deosebire de testele unitare, care se concentrează pe funcții sau componente izolate, Testele E2E simulează un scenariu complet de utilizator. De exemplu, ei pot verifica dacă completarea unui formular actualizează corect datele și generează o semnătură corectă. Acest lucru ajută la detectarea erorilor care ar putea apărea în timpul interacțiunilor dintre module, îmbunătățind astfel fiabilitatea generală și experiența utilizatorului aplicației.

Testele E2E sunt stocate în tests/pytest-playwright folder, în fișier test_e2e.py.

Un test E2E în Python arată astfel:

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")

Integrare continuă

Pe tot parcursul, aderăm la cele mai bune practici de dezvoltare. De asemenea, am creat o conductă de integrare continuă pentru această aplicație.

Conducta de integrare continuă este stocată în .github/workflows/run-pytest.yaml fişier. Acest fișier conține diferiții pași ai conductei.

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

Aceasta este o modalitate excelentă de a vă asigura că aplicația funcționează corect înainte de a o implementa în producție. Aici, aplicația este testată pe trei browsere: Webkit, Chromium și Firefox.

Actualizarea Banner Imager

Odată actualizată imaginea, aceasta va afecta direct aplicația și toate semnăturile generate cu aplicația.

Implementarea aplicației pe serverele noastre

Această aplicație este implementată pe serverele noastre. Îl puteți vizualiza online: signature.py.

Această aplicație Python trăiește alături de celelalte aplicații R de pe serverele noastre. Nu ezitați să ne contactați dacă doriți să implementați aplicațiile dvs. Python sau R în producție.

Dominic Botezariu
Dominic Botezariuhttps://www.noobz.ro/
Creator de site și redactor-șef.

Cele mai noi știri

Pe același subiect

LĂSAȚI UN MESAJ

Vă rugăm să introduceți comentariul dvs.!
Introduceți aici numele dvs.