Source code for flask_mab.__init__

"""
    flask_mab
    ~~~~~~~~~

    This module implements all the extension logic for
    Multi-armed bandit experiments on Flask apps.

    :copyright: (c) 2013 by `Mark Grey <http://www.deacondesperado.com>`_.

    :license: BSD, see LICENSE for more details.
"""

from flask import current_app, g, request
import json
import flask_mab.storage
import types
from bunch import Bunch
from flask import _request_ctx_stack
from functools import wraps

try:
    from flask import _app_ctx_stack as stack
except ImportError:
    from flask import _request_ctx_stack as stack

__version__ = "1.1.1"

[docs]def choose_arm(bandit): """Route decorator for registering an impression conveinently :param bandit: The bandit/experiment to register for :type bandit: string """ def decorator(func): #runs @ service init if not hasattr(func, 'bandits'): func.bandits = [] func.bandits.append(bandit) @wraps(func) def wrapper(*args, **kwargs): #runs at endpoint hit add_args = [] for bandit in func.bandits: #Fetch from request first here? arm_id, arm_value = suggest_arm_for(bandit) add_args.append((bandit, arm_value)) kwargs.update(add_args) return func(*args, **kwargs) return wrapper return decorator
[docs]def reward_endpt(bandit, reward_val=1): """Route decorator for rewards. :param bandit: The bandit/experiment to register rewards for using arm found in cookie. :type bandit: string :param reward: The amount of reward this endpoint should give its winning arm :type reward: float """ def decorator(func): if not hasattr(func, 'rewards'): func.rewards = [] func.rewards.append((bandit, reward_val)) @wraps(func) def wrapper(*args, **kwargs): for bandit, reward_amt in func.rewards: if bandit in request.bandits.keys(): request.bandits_reward.add((bandit, request.bandits[bandit], reward_amt)) return func(*args, **kwargs) return wrapper return decorator
[docs]class BanditMiddleware(object): """The main flask extension. Sets up all the necessary tracking for the bandit experiments """ def __init__(self, app=None): """Attach MAB logic to a Flask application :param app: An optional Flask application """ if app is not None: self.init_app(app) def _register_storage(self, app): storage_engine = getattr( flask_mab.storage, app.config.get('MAB_STORAGE_ENGINE', 'BanditStorage')) storage_opts = app.config.get('MAB_STORAGE_OPTS', tuple()) storage_backend = storage_engine(*storage_opts) app.extensions['mab'].bandit_storage = storage_backend
[docs] def init_app(self, app): """Attach Multi Armed Bandit to application and configure :param app: A flask application instance """ app.config.setdefault('MAB_COOKIE_NAME', 'MAB') app.config.setdefault('MAB_COOKIE_PATH', '/') app.config.setdefault('MAB_COOKIE_TTL', None) app.config.setdefault('MAB_DEBUG_HEADERS', True) if not hasattr(app, 'extensions'): app.extensions = {} app.extensions['mab'] = Bunch() self._register_storage(app) if hasattr(app, 'teardown_appcontext'): app.teardown_appcontext(self.teardown) else: app.teardown_request(self.teardown) app.extensions['mab'].bandits = {} app.extensions['mab'].reward_endpts = [] app.extensions['mab'].pull_endpts = [] app.extensions['mab'].debug_headers = app.config.get('MAB_DEBUG_HEADERS', True) app.extensions['mab'].cookie_name = app.config.get('MAB_COOKIE_NAME', "MAB") self._init_detection(app)
[docs] def teardown(self, *args, **kwargs): """Stub for old flask versions """ pass
def _init_detection(self, app): """ Attaches all request before/after handlers for bandits. Nested functions are as follows * detect_last_bandits: Loads any arms already assigned to this user from the cookie. * persist_bandits: Saves bandits down to storage engine at the end of the request * remember_bandit_arms: Sets the cookie for all requests that pulled an arm * send_debug_header: Attaches a header for the MAB to the HTTP response for easier debugging """ @app.before_request def detect_last_bandits(): bandits = request.cookies.get(app.extensions['mab'].cookie_name) request.bandits_save = False request.bandits_reward = set() if bandits: request.bandits = json.loads(bandits) else: request.bandits = {} @app.after_request def persist_bandits(response): app.extensions['mab'].bandit_storage.save(app.extensions['mab'].bandits) return response @app.after_request def remember_bandit_arms(response): if request.bandits_save: for bandit_id,arm in request.bandits.items(): #hook event for saving an impression here app.extensions['mab'].bandits[bandit_id].pull_arm(arm) for bandit_id, arm, reward_amt in request.bandits_reward: try: app.extensions['mab'].bandits[bandit_id].reward_arm(arm, reward_amt) #hook event for saving a reward line here except KeyError: raise MABConfigException("Bandit %s not found" % bandit_id) response.set_cookie( app.extensions['mab'].cookie_name, json.dumps(request.bandits)) return response @app.after_request def send_debug_header(response): if app.extensions['mab'].debug_headers and request.bandits_save: response.headers['X-MAB-Debug'] = "STORE; "+';'.join( ['%s:%s' % (key, val) for key, val in request.bandits.items()]) elif app.extensions['mab'].debug_headers: response.headers['X-MAB-Debug'] = "SAVED; "+';'.join(['%s:%s' % (key, val) for key, val in request.bandits.items()]) return response app.add_bandit = types.MethodType(add_bandit, app)
[docs]def add_bandit(app, name, bandit=None): """Attach a bandit for an experiment :param name: The name of the experiment, will be used for lookups :param bandit: The bandit to use for this experiment :type bandit: Bandit """ saved_bandits = app.extensions['mab'].bandit_storage.load() if name in saved_bandits.keys(): app.extensions['mab'].bandits[name] = saved_bandits[name] else: app.extensions['mab'].bandits[name] = bandit
[docs]def suggest_arm_for(key): """Get an experimental outcome by id. The primary way the implementor interfaces with their experiments. Suggests arms if not in cookie, using cookie val if present :param key: The bandit/experiment to get a suggested arm for :type key: string :param also_pull: Should we register a pull/impression at the same time as suggesting :raises KeyError: in case requested experiment does not exist """ app = current_app try: #Try to get the selected bandits from cookie arm = app.extensions['mab'].bandits[key][request.bandits[key]] return arm["id"], arm["value"] except (AttributeError, TypeError, KeyError) as err: #Assign an arm for a new client try: arm = app.extensions['mab'].bandits[key].suggest_arm() request.bandits[key] = arm["id"] request.bandits_save = True return arm["id"], arm["value"] except KeyError: raise MABConfigException("Bandit %s not found" % key)
[docs]class MABConfigException(Exception): """Raised when internal state in MAB setup is invalid""" pass
Fork me on GitHub