Tutorial: Usar sinalizadores de recursos variantes em um aplicativo Python

Neste tutorial, utiliza uma funcionalidade de variantes para gerir experiências para diferentes segmentos de utilizadores numa aplicação de exemplo, Citação do Dia. Utiliza a flag de funcionalidade variante criada em Como criar flags de funcionalidade variantes. Antes de continuar, certifique-se de criar o sinalizador de recurso variante chamado Saudação na sua loja de configuração de aplicativos.

Pré-requisitos

Configurar um aplicativo Web Python Flask

Se já tiver uma aplicação web Python Flask, pode saltar para a secção Utilizar o sinalizador de funcionalidade de variante.

  1. Crie uma nova pasta de projeto chamada QuoteOfTheDay.

  2. Crie um ambiente virtual na pasta QuoteOfTheDay .

    python -m venv venv
    
  3. Ative o ambiente virtual.

    .\venv\Scripts\Activate
    
  4. Instale as versões mais recentes dos seguintes pacotes.

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

Criar a aplicação Citação do Dia

  1. Crie um novo ficheiro chamado app.py na pasta QuoteOfTheDay com o seguinte conteúdo. Ele configura um aplicativo web Flask básico com autenticação de usuário.

    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)
    
    import routes
    app.register_blueprint(routes.bp)
    
  2. Crie um novo arquivo chamado model.py na pasta QuoteOfTheDay com o seguinte conteúdo. Ele define uma classe de Quote dados e um modelo de usuário para o aplicativo web Flask.

    from dataclasses import dataclass
    from flask_login import UserMixin
    from app 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. Crie um novo arquivo chamado routes.py na pasta QuoteOfTheDay com o seguinte conteúdo. Ele define rotas para a aplicação web Flask, manipulando a autenticação do usuário e exibindo uma página inicial com uma cotação aleatória.

    import random
    
    from flask import Blueprint, render_template, request, flash, redirect, url_for
    from flask_login import current_user, login_user, logout_user
    from app import db, bcrypt
    from model import Quote, Users
    
    bp = Blueprint("pages", __name__)
    
    @bp.route("/", methods=["GET"])
    def index():
        context = {}
        user = ""
        if current_user.is_authenticated:
            user = current_user.username
            context["user"] = user
        else:
            context["user"] = "Guest"
    
        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. Crie uma nova pasta chamada templates na pasta QuoteOfTheDay e adicione um novo arquivo chamado base.html nela com o seguinte conteúdo. Ele define a página de layout para o aplicativo 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. Crie um novo arquivo chamado index.html na pasta de modelos com o seguinte conteúdo. Ele estende o modelo base e adiciona o bloco de conteúdo.

    {% 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. Crie um novo arquivo chamado sign_up.html na pasta de modelos com o seguinte conteúdo. Ele define o modelo para a página de registro do usuário.

    {% 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. Crie um novo arquivo chamado login.html na pasta de modelos com o seguinte conteúdo. Ele define o modelo para a página de login do usuário.

    {% 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. Crie uma nova pasta chamada static na pasta QuoteOfTheDay e adicione um novo arquivo chamado site.css nela com o seguinte conteúdo. Ele adiciona estilos CSS para o aplicativo 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 */
    }
    

Usar o sinalizador de recurso variante

  1. Instale as versões mais recentes dos seguintes pacotes.

    pip install azure-identity 
    pip install azure-appconfiguration-provider
    pip install featuremanagement[AzureMonitor]
    
  2. Abre o ficheiro app.py . Adicione o seguinte código entre bcrypt = Bcrypt(app) e db = SQLAlchemy(). Ele se conecta à Configuração do aplicativo e configura o gerenciamento de recursos.

    Utiliza o DefaultAzureCredential para se autenticar no seu arquivo de App Configuration. Siga as instruções para atribuir à sua credencial a função de Leitor de Dados de Configuração do Aplicativo. Certifique-se de dar tempo suficiente para que a permissão se propague antes de executar seu aplicativo.

    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. Abra routes.py e atualize o seguinte código para greeting_message de modo a obter a variante da funcionalidade.

    from app import feature_manager
    
    # Update greeting_message to variant
    greeting = feature_manager.get_variant("Greeting", user)
    greeting_message = ""
    if greeting:
        greeting_message = greeting.configuration
    
  4. No portal Azure, vá à loja de Configuração de Aplicações que criou anteriormente e adicione os seguintes valores-chave. Crie uma chave-valor

Chave valor Description
"SQLALCHEMY_DATABASE_URI" "sqlite:///db.sqlite" Usado por Flask-SQLAlchemy para saber a que base de dados se ligar
"SECRET_KEY" "chave secreta falsa" Usado pelo Flask para assinar cookies de sessão (proteção de integridade) para efeitos de autenticação

Em app.py, a load(...) função recupera dados de configuração de Azure App Configuration e armazena-os em azure_app_config. A app.config.update(azure_app_config) chamada aplica então as definições de configuração recuperadas à aplicação Flask.

Compilar e executar a aplicação

  1. Defina uma variável de ambiente chamada AzureAppConfigurationEndpoint para o ponto de extremidade do seu repositório de Configuração de Aplicativo encontrado em Visão geral da sua loja no portal do Azure.

    Se você usar o prompt de comando do Windows, execute o seguinte comando e reinicie o prompt de comando para permitir que a alteração entre em vigor:

    setx AzureAppConfigurationEndpoint "<AppConfigurationEndpoint>"
    

    Se você usar o PowerShell, execute o seguinte comando:

    $Env:AzureAppConfigurationEndpoint = "<AppConfigurationEndpoint>"
    

    Se você usa macOS ou Linux, execute o seguinte comando:

    export AzureAppConfigurationEndpoint='<AppConfigurationEndpoint>'
    
  2. Na linha de comandos, na pasta QuoteOfTheDay, execute o seguinte comando: flask run.

  3. Aguarde até que a aplicação seja iniciada e, em seguida, abra um browser e navegue até http://localhost:5000/.

  4. Depois de visualizar o aplicativo em execução, selecione Registrar no canto superior direito para registrar um novo usuário.

    Captura de ecrã da aplicação Citação do dia, a mostrar Registo.

  5. Registre um novo usuário chamado usera@contoso.com.

    Nota

    É importante para o propósito deste tutorial usar esses nomes exatamente. Desde que o recurso tenha sido configurado conforme o esperado, os dois usuários devem ver variantes diferentes.

  6. Selecione o botão Enviar depois de inserir as informações do usuário.

  7. Você está automaticamente conectado. Você deve ver que usera@contoso.com vê a mensagem longa ao visualizar o aplicativo.

    Captura de ecrã da aplicação Citação do dia, mostrando uma mensagem especial para o utilizador.

  8. Saia usando o botão Logout no canto superior direito.

  9. Registre um segundo usuário chamado userb@contoso.com.

  10. Você está automaticamente conectado. Você deve ver que userb@contoso.com vê a mensagem curta ao visualizar o aplicativo.

    Captura de ecrã da aplicação Citação do dia, mostrando uma mensagem para o utilizador.

Próximos passos

Para obter o resumo completo dos recursos da biblioteca de gerenciamento de recursos do Python, consulte o documento a seguir.