在本教程中,你将使用变体功能标志来管理示例应用程序中不同用户段的体验,今日名言。 利用在《变体功能标志指南》中创建的变体功能标志。 在继续操作之前,请确保在应用程序配置存储中创建名为 Greeting 的变体功能标志。
先决条件
- Python 3.8 或更高版本 - 有关在 Windows 上设置 Python 的信息,请参阅 Windows 上的 Python 文档
- 按照《变体功能标志指南》教程创建名为 Greeting 的变体功能标志。
设置 Python Flask Web 应用
如果已有 Python Flask Web 应用,则可以跳到“使用变体功能标志”部分。
创建名为 QuoteOfTheDay 的新项目文件夹。
在 QuoteOfTheDay 文件夹中创建虚拟环境。
python -m venv venv
激活虚拟环境。
.\venv\Scripts\Activate
安装以下包的最新版本。
pip install flask pip install flask-login pip install flask_sqlalchemy pip install flask_bcrypt
创建“日报价”应用
在
QuoteOfTheDay
文件夹中创建以下内容的新文件app.py
。 该文件设置一个拥有用户身份验证的基本 Flask Web 应用程序。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)
在 QuoteOfTheDay 文件夹中创建包含以下内容的新文件 model.py。 它会为 Flask Web 应用程序定义
Quote
数据类和用户模型。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
在 QuoteOfTheDay 文件夹中创建包含以下内容的新文件 routes.py。 它会定义 Flask Web 应用程序的路由、处理用户身份验证以及显示带有随机报价的主页。
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"))
在 QuoteOfTheDay 文件夹中创建一个名为 templates 的新文件夹,并在其中添加包含以下内容的 base.html 新文件。 它会定义 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"> © 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>
在 templates 文件夹中创建包含以下内容的新文件 index.html。 它会扩展基本模板并添加内容块。
{% 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 %}
在 templates 文件夹中创建包含以下内容的新文件 sign_up.html。 它会定义用户注册页面的模板。
{% 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 %}
在 templates 文件夹中创建包含以下内容的新文件 login.html。 它会定义用户登录页面的模板。
{% 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 %}
在 QuoteOfTheDay 文件夹中创建一个名为 static 的新文件夹,并在其中添加包含以下内容的新文件 site.css。 该文件为 Web 应用程序添加 CSS 样式。
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 */ }
使用变体功能标志
安装以下包的最新版本。
pip install azure-identity pip install azure-appconfiguration-provider pip install featuremanagement[AzureMonitor]
打开
app.py
文件,并在文件末尾添加以下代码。 它会连接到应用配置并设置功能管理。可以使用
DefaultAzureCredential
向应用程序配置存储区进行身份验证。 按照说明为凭据分配应用程序配置数据读取者角色。 在运行应用程序之前,请务必留出足够的时间来传播权限。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)
打开
routes.py
并在其末尾添加以下代码,以刷新配置并获取功能变体。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
生成并运行应用
将名为 AzureAppConfigurationEndpoint 的环境变量设置到应用程序配置存储区的终结点中,存储区位于 Azure 门户中的存储概述下。
如果使用 Windows 命令提示符,则请运行以下命令并重启命令提示符,这样更改才会生效:
setx AzureAppConfigurationEndpoint "<endpoint-of-your-app-configuration-store>"
如果使用 PowerShell,请运行以下命令:
$Env:AzureAppConfigurationEndpoint = "<endpoint-of-your-app-configuration-store>"
如果使用 macOS 或 Linux,则请运行以下命令:
export AzureAppConfigurationEndpoint='<endpoint-of-your-app-configuration-store'
在命令提示符下,在 QuoteOfTheDay 文件夹中,运行:
flask run
。等待应用启动,然后打开浏览器并导航到
http://localhost:5000/
。查看正在运行的应用程序后,选择右上角的“注册”以注册新用户。
注册名为 usera@contoso.com 的新用户。
注意
对于本教程而言,准确使用这些名称非常重要。 只要该功能已按预期进行配置,两个用户应会看到不同的变体。
输入用户信息后,单击“提交”按钮。
系统会自动登录。 查看应用时,应会看到 usera@contoso.com 显示长消息。
使用右上角的“登出”按钮退出登录。
注册第二个名为 userb@contoso.com 的用户。
系统会自动登录。 查看应用时,应会看到 userb@contoso.com 显示短消息。
后续步骤
有关 Python 功能管理库的完整功能运行,请参阅以下文档。