Condividi tramite


Usare flag di funzionalità varianti in un'applicazione Python

In questa esercitazione si usa un flag di funzionalità variante per gestire le esperienze per segmenti utente diversi in un'applicazione di esempio, Offerta del giorno. Usare il flag di funzionalità variant creato in Come variantare i flag di funzionalità. Prima di procedere, assicurarsi di creare il flag di funzionalità variante denominato Greeting nell'archivio Configurazione app.

Prerequisiti

  • Python 3.8 o versione successiva: per informazioni sulla configurazione di Python in Windows, vedere la documentazione di Python in Windows
  • Seguire l'esercitazione Come variantare i flag di funzionalità e creare il flag di funzionalità variante denominato Greeting.

Configurare un'app Web Python Flask

Se si dispone già di un'app Web Python Flask, è possibile passare alla sezione Usare il flag di funzionalità variant.

  1. Creare una nuova cartella di progetto denominata QuoteOfTheDay.

  2. Creare un ambiente virtuale nella cartella QuoteOfTheDay .

    python -m venv venv
    
  3. Attivare l'ambiente virtuale.

    .\venv\Scripts\Activate
    
  4. Installare le versioni più recenti dei pacchetti seguenti.

    pip install flask
    pip install flask-login
    pip install flask_sqlalchemy
    pip install flask_bcrypt
    

Creare l'app Quote of the Day

  1. Creare un nuovo file denominato app.py nella QuoteOfTheDay cartella con il contenuto seguente. Configura un'applicazione Web Flask di base con l'autenticazione utente.

    from flask_bcrypt import Bcrypt
    from flask_sqlalchemy import SQLAlchemy
    from flask_login import LoginManager
    from flask import Flask
    
    app = Flask(__name__, template_folder="../templates", static_folder="../static")
    bcrypt = Bcrypt(app)
    
    db = SQLAlchemy()
    db.init_app(app)
    
    login_manager = LoginManager()
    login_manager.init_app(app)
    
    from .model import Users
    
    @login_manager.user_loader
    def loader_user(user_id):
        return Users.query.get(user_id)
    
    with app.app_context():
        db.create_all()
    
    if __name__ == "__main__":
        app.run(debug=True)
    
    from . import routes
    app.register_blueprint(routes.bp)
    
  2. Creare un nuovo file denominato model.py nella cartella QuoteOfTheDay con il contenuto seguente. Definisce una Quote classe di dati e un modello utente per l'applicazione Web Flask.

    from dataclasses import dataclass
    from flask_login import UserMixin
    from . import db
    
    @dataclass
    class Quote:
        message: str
        author: str
    
    # Create user model
    class Users(UserMixin, db.Model):
    
        id = db.Column(db.Integer, primary_key=True)
        username = db.Column(db.String(250), unique=True, nullable=False)
        password_hash = db.Column(db.String(250), nullable=False)
    
        def __init__(self, username, password):
            self.username = username
            self.password_hash = password
    
  3. Creare un nuovo file denominato routes.py nella cartella QuoteOfTheDay con il contenuto seguente. Definisce le route per l'applicazione Web Flask, la gestione dell'autenticazione utente e la visualizzazione di una home page con un'offerta casuale.

    import random
    
    from flask import Blueprint, render_template, request, flash, redirect, url_for
    from flask_login import current_user, login_user, logout_user
    from . import db, bcrypt
    from .model import Quote, Users
    
    bp = Blueprint("pages", __name__)
    
    @bp.route("/", methods=["GET", "POST"])
    def index():
        context = {}
        user = ""
        if current_user.is_authenticated:
            user = current_user.username
            context["user"] = user
        else:
            context["user"] = "Guest"
        if request.method == "POST":
            return redirect(url_for("pages.index"))
    
        quotes = [
            Quote("You cannot change what you are, only what you do.", "Philip Pullman"),
        ]
    
        greeting_message = "Hi"
    
        context["model"] = {}
        context["model"]["greeting_message"] = greeting_message
        context["model"]["quote"] = {}
        context["model"]["quote"] = random.choice(quotes)
        context["isAuthenticated"] = current_user.is_authenticated
    
        return render_template("index.html", **context)
    
    @bp.route("/register", methods=["GET", "POST"])
    def register():
        if request.method == "POST":
            password = request.form.get("password")
            hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
            user = Users(request.form.get("username"), hashed_password)
            try:
                db.session.add(user)
                db.session.commit()
            except Exception as e:
                flash("Username already exists")
                return redirect(url_for("pages.register"))
            login_user(user)
    
            return redirect(url_for("pages.index"))
        return render_template("sign_up.html")
    
    
    @bp.route("/login", methods=["GET", "POST"])
    def login():
        if request.method == "POST":
            user = Users.query.filter_by(username=request.form.get("username")).first()
            password = request.form.get("password")
            if user and bcrypt.check_password_hash(user.password_hash, password):
                login_user(user)
                return redirect(url_for("pages.index"))
        return render_template("login.html")
    
    @bp.route("/logout")
    def logout():
        logout_user()
        return redirect(url_for("pages.index"))
    
  4. Creare una nuova cartella denominata templates nella cartella QuoteOfTheDay e aggiungere un nuovo file denominato base.html al suo interno con il contenuto seguente. Definisce la pagina di layout per l'applicazione Web.

    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>QuoteOfTheDay</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
        <link rel="stylesheet" href="{{ url_for('static', filename='site.css') }}">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.3/css/all.min.css">
    </head>
    <body>
        <header>
            <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
                <div class="container">
                    <a class="navbar-brand"  href="/">QuoteOfTheDay</a>
                    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
                            aria-expanded="false" aria-label="Toggle navigation">
                        <span class="navbar-toggler-icon"></span>
                    </button>
                    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
    
                        <ul class="navbar-nav flex-grow-1">
                            <li class="nav-item">
                                <a class="nav-link text-dark" href="/">Home</a>
                            </li>
                        </ul>
                        {% block login_partial %}
                        <ul class="navbar-nav">
                        {% if isAuthenticated %}
                            <li class="nav-item">
                                <a  class="nav-link text-dark">Hello {{user}}!</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark"  href="/logout">Logout</a>
                            </li>
                        {% else %}
                            <li class="nav-item">
                                <a class="nav-link text-dark"  href="/register">Register</a>
                            </li>
                            <li class="nav-item">
                                <a class="nav-link text-dark"  href="/login">Login</a>
                            </li>
                        {% endif %}
                        </ul>
                        {% endblock %}
                    </div>
                </div>
            </nav>
        </header>
        <div class="container">
            <main role="main" class="pb-3">
                {% block content %}
                {% endblock %}
            </main>
        </div>
    </body>
    
    <footer class="border-top footer text-muted">
        <div class="container">
            &copy; 2024 - QuoteOfTheDay
        </div>
    </footer>
    
    <script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz" crossorigin="anonymous"></script>
    </body>
    </html>
    
  5. Creare un nuovo file denominato index.html nella cartella templates con il contenuto seguente. Estende il modello di base e aggiunge il blocco di contenuto.

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="quote-container">
        <div class="quote-content">
           {% if model.greeting_message %}
                <h3 class="greeting-content">{{model.greeting_message}}</h3>
            {% endif %}
            <br />
            <p class="quote">“{{model.quote.message}}”</p>
            <p>- <b>{{model.quote.author}}</b></p>
        </div>
    
        <div class="vote-container">
            <button class="btn btn-primary" onclick="heartClicked(this)">
                <i class="far fa-heart"></i> <!-- Heart icon -->
            </button>
        </div>
    
        <form action="/" method="post">
        </form>
    </div>
    <script>
        function heartClicked(button) {
            var icon = button.querySelector('i');
            icon.classList.toggle('far');
            icon.classList.toggle('fas');
        }
    </script>
    {% endblock %}
    
  6. Creare un nuovo file denominato sign_up.html nella cartella templates con il contenuto seguente. Definisce il modello per la pagina di registrazione dell'utente.

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="login-container">
      <h1>Create an account</h1>
      <form action="#" method="post">
        <label for="username">Username:</label>
        <input type="text" name="username" />
        <label for="password">Password:</label>
        <input type="password" name="password" />
        <button type="submit">Submit</button>
      </form>
    </div>
    {% endblock %}
    
  7. Creare un nuovo file denominato login.html nella cartella templates con il contenuto seguente. Definisce il modello per la pagina di accesso dell'utente.

    {% extends 'base.html' %}
    
    {% block content %}
    <div class="login-container">
      <h1>Login to your account</h1>
      <form action="#" method="post">
        <label for="username">Username:</label>
        <input type="text" name="username" />
        <label for="password">Password:</label>
        <input type="password" name="password" />
        <button type="submit">Submit</button>
      </form>
    </div>
    {% endblock %}
    
  8. Creare una nuova cartella denominata static nella cartella QuoteOfTheDay e aggiungere un nuovo file denominato site.css in esso con il contenuto seguente. Aggiunge stili CSS per l'applicazione Web.

    html {
        font-size: 14px;
      }
    
      @media (min-width: 768px) {
        html {
          font-size: 16px;
        }
      }
    
      .btn:focus, .btn:active:focus, .btn-link.nav-link:focus, .form-control:focus, .form-check-input:focus {
        box-shadow: 0 0 0 0.1rem white, 0 0 0 0.25rem #258cfb;
      }
    
      html {
        position: relative;
        min-height: 100%;
      }
    
      body {
        margin-bottom: 60px;
      }
    
      body {
        font-family: Arial, sans-serif;
        background-color: #f4f4f4;
        color: #333;
    }
    
    .quote-container {
        background-color: #fff;
        margin: 2em auto;
        padding: 2em;
        border-radius: 8px;
        max-width: 750px;
        box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
        display: flex;
        justify-content: space-between;
        align-items: start;
        position: relative;
    }
    
    .login-container {
      background-color: #fff;
      margin: 2em auto;
      padding: 2em;
      border-radius: 8px;
      max-width: 750px;
      box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
      justify-content: space-between;
      align-items: start;
      position: relative;
    }
    
    .vote-container {
        position: absolute;
        top: 10px;
        right: 10px;
        display: flex;
        gap: 0em;
    }
    
        .vote-container .btn {
            background-color: #ffffff; /* White background */
            border-color: #ffffff; /* Light blue border */
            color: #333
        }
    
            .vote-container .btn:focus {
                outline: none;
                box-shadow: none;
            }
    
            .vote-container .btn:hover {
                background-color: #F0F0F0; /* Light gray background */
            }
    
    .greeting-content {
        font-family: 'Georgia', serif; /* More artistic font */
    }
    
    .quote-content p.quote {
        font-size: 2em; /* Bigger font size */
        font-family: 'Georgia', serif; /* More artistic font */
        font-style: italic; /* Italic font */
        color: #4EC2F7; /* Medium-light blue color */
    }
    

Usare il flag di funzionalità variant

  1. Installare le versioni più recenti dei pacchetti seguenti.

    pip install azure-identity 
    pip install azure-appconfiguration-provider
    pip install featuremanagement[AzureMonitor]
    
  2. Aprire il app.py file e aggiungere il codice seguente alla fine del file. Si connette a Configurazione app e configura la gestione delle funzionalità.

    Usare per eseguire l'autenticazione DefaultAzureCredential nell'archivio Configurazione app. Seguire le istruzioni per assegnare le credenziali al ruolo lettore dati Configurazione app. Assicurarsi di consentire tempo sufficiente per la propagazione dell'autorizzazione prima di eseguire l'applicazione.

    import os
    from azure.appconfiguration.provider import load
    from featuremanagement import FeatureManager
    from azure.identity import DefaultAzureCredential
    
    ENDPOINT = os.getenv("AzureAppConfigurationEndpoint")
    
    # Updates the flask app configuration with the Azure App Configuration settings whenever a refresh happens
    def callback():
        app.config.update(azure_app_config)
    
    # Connect to App Configuration
    global azure_app_config
    azure_app_config = load(
        endpoint=ENDPOINT,
        credential=DefaultAzureCredential(),
        on_refresh_success=callback,
        feature_flag_enabled=True,
        feature_flag_refresh_enabled=True,
    )
    app.config.update(azure_app_config)
    
    # Create a FeatureManager
    feature_manager = FeatureManager(azure_app_config)
    
  3. Aprire routes.py e aggiungere il codice seguente alla fine di esso per aggiornare la configurazione e ottenere la variante della funzionalità.

    from featuremanagement.azuremonitor import track_event
    from . import azure_app_config, feature_manager
    
    ...
    # Update the post request to track liked events
    if request.method == "POST":
        track_event("Liked", user)
        return redirect(url_for("pages.index"))
    
    ...
    # Update greeting_message to variant
    greeting = feature_manager.get_variant("Greeting", user)
    greeting_message = ""
    if greeting:
        greeting_message = greeting.configuration
    

Compilare ed eseguire l'app

  1. Impostare una variabile di ambiente denominata AzureAppConfigurationEndpoint sull'endpoint dell'archivio Configurazione app disponibile in Panoramica dell'archivio nel portale di Azure.

    Se si usa il prompt dei comandi di Windows, eseguire il comando seguente e riavviare il prompt per rendere effettiva la modifica:

    setx AzureAppConfigurationEndpoint "<endpoint-of-your-app-configuration-store>"
    

    Se si usa PowerShell, eseguire il comando seguente:

    $Env:AzureAppConfigurationEndpoint = "<endpoint-of-your-app-configuration-store>"
    

    Se si usa macOS o Linux, eseguire il comando seguente:

    export AzureAppConfigurationEndpoint='<endpoint-of-your-app-configuration-store'
    
  2. Nel prompt dei comandi, nella cartella QuoteOfTheDay eseguire: flask run.

  3. Attendere l'avvio dell'app e quindi aprire un browser e passare a http://localhost:5000/.

  4. Una volta che si sta visualizzando l'applicazione in esecuzione, selezionare Registra in alto a destra per registrare un nuovo utente.

    Screenshot dell'app Citazione del giorno, che mostra Registra.

  5. Registrare un nuovo utente denominato usera@contoso.com.

    Nota

    Ai fini di questa esercitazione è importante usare esattamente questi nomi. Se la funzionalità è stata configurata come previsto, i due utenti dovrebbero visualizzare varianti diverse.

  6. Selezionare il pulsante Invia dopo aver immesso le informazioni utente.

  7. L'accesso viene eseguito automaticamente. Verrà visualizzato usera@contoso.com il messaggio lungo quando si visualizza l'app.

    Screenshot dell'app Citazione del giorno, che mostra un messaggio speciale per l'utente.

  8. Disconnettersi con il pulsante Disconnessione in alto a destra.

  9. Registrare un secondo utente denominato userb@contoso.com.

  10. L'accesso viene eseguito automaticamente. Si noterà che userb@contoso.com viene visualizzato il breve messaggio quando si visualizza l'app.

    Screenshot dell'app Offerta del giorno, che mostra un messaggio per l'utente.

Passaggi successivi

Per il rundown completo delle funzionalità della libreria di gestione delle funzionalità Python, vedere il documento seguente.