次の方法で共有


Python アプリケーションでバリアント機能フラグを使用する

このチュートリアルでは、バリアント機能フラグを使用して、Quote of the Day というサンプル アプリケーションでさまざまなユーザー セグメントのエクスペリエンスを管理します。 「バリアント機能フラグを使用する方法」で作成したバリアント機能フラグを利用します。 作業を始める前に、App Configuration ストアに Greeting というバリアント機能フラグを作成していることを確認してください。

前提条件

Python Flask Web アプリを設定する

すでに Python Flask Web アプリをお持ちの場合は、「バリアント機能フラグを使用する」セクションに進むことができます。

  1. QuoteOfTheDay という新しいプロジェクト フォルダーを作成します。

  2. QuoteOfTheDay フォルダー内に仮想環境を作成します。

    python -m venv venv
    
  3. 仮想環境をアクティブにします。

    .\venv\Scripts\Activate
    
  4. 次のパッケージの最新バージョンをインストールします。

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

Quote of the Day アプリを作成する

  1. 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)
    
  2. 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
    
  3. 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"))
    
  4. 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">
            &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. 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 %}
    
  6. 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 %}
    
  7. 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 %}
    
  8. 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 */
    }
    

バリアント機能フラグを使用する

  1. 次のパッケージの最新バージョンをインストールします。

    pip install azure-identity 
    pip install azure-appconfiguration-provider
    pip install featuremanagement[AzureMonitor]
    
  2. app.py ファイルを開き、ファイルの末尾に次のコードを追加します。 App Configuration に接続し、機能の管理を設定します。

    DefaultAzureCredential を使って、App Configuration ストアに対する認証を行います。 手順に従って、資格情報に App Configuration データ閲覧者ロールを割り当てます。 アプリケーションを実行する前に、アクセス許可が伝わるのに十分な時間をおいてください。

    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. 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
    

アプリをビルドして実行する

  1. AzureAppConfigurationEndpoint という環境変数を、Azure portal でストアの [概要] にある App Configuration ストアのエンドポイントに設定します。

    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'
    
  2. コマンド プロンプトで、QuoteOfTheDay 内で flask run を実行します。

  3. アプリが起動するまで待ち、ブラウザーを開いて http://localhost:5000/ に移動します。

  4. 実行中のアプリケーションを表示したら、右上にある [登録] を選択して新しいユーザーを登録します。

    今日の名言アプリの [登録] を示すスクリーンショット。

  5. usera@contoso.com という名前の新しいユーザーを登録します。

    Note

    このチュートリアルの目的上、これらの名前を正確に使用することが重要です。 機能が想定どおりに構成されている限り、この 2 人のユーザーには異なるバリアントが表示されます。

  6. ユーザー情報を入力した後、[送信] ボタンを選択します。

  7. 自動的にログインします。 usera@contoso.com がアプリを表示したときに長いメッセージが表示されることを確認できます。

    今日の名言アプリでユーザーに表示される特別なメッセージを示す今日の名言アプリのスクリーンショット。

  8. 右上の [ログアウト] ボタンを使用してログアウトします。

  9. userb@contoso.com という名前の 2 番目のユーザーを登録します。

  10. 自動的にログインします。 userb@contoso.com がアプリを表示したときに短いメッセージが表示されることを確認できます。

    Quote of the day アプリのスクリーンショット。ユーザー向けにメッセージが表示されます。

次のステップ

Python 機能管理ライブラリの詳細な機能の説明については、次のドキュメントを参照してください。