Slide 1

Slide 1 text

Extending Flask using the Flask Plugins API

Slide 2

Slide 2 text

No content

Slide 3

Slide 3 text

About One of the organizer of Python Mauritius Usergroup (pymug) / FlaskCon / PyCon Africa OpenSource https://www.compileralchemy.com or Qr 3

Slide 4

Slide 4 text

so, about handles twitter @osdotsystem https://fosstodon.org/@osdotsystem linkedin.com/in/appinv/ 4

Slide 5

Slide 5 text

Extending Flask using the Flask Plugins API 5

Slide 6

Slide 6 text

Why this talk? 6

Slide 7

Slide 7 text

encourage people to maintain Flask projects Pallets Eco 7

Slide 8

Slide 8 text

encourage people to write extensions give users SUPER powers improve Flask dev experience contributes to make Flask a compelling choice saves time 8

Slide 9

Slide 9 text

understand extensions the seed to muster the courage to dive into codebases 9

Slide 10

Slide 10 text

Approaches to plugins in Python 10

Slide 11

Slide 11 text

The auto-load method python ---> find packages start with fruit_ | V iterate find attributes 11

Slide 12

Slide 12 text

Plugins list method python ---> developers specify plugins | V load iterate find attributes 12

Slide 13

Slide 13 text

Plugin manager python ---> plugin manager | V find plugin load extract info pass back 13

Slide 14

Slide 14 text

Flask app = create_app() | app ---. | V .---------. | plugin1 | plugin1.init_app(app) .---------. | app <--. // modified | app ---. | V .---------. | plugin2 | plugin2.init_app(app) .---------. | app <--. // modified | 14

Slide 15

Slide 15 text

Order of plugins matter sometimes 15

Slide 16

Slide 16 text

Not always need to be named .init_app 16

Slide 17

Slide 17 text

Really? 17

Slide 18

Slide 18 text

Yes, docs says The most common pattern is to create a class that represents the extension’s configuration and behavior, with an init_app method to apply the extension instance to the given application instance. 18

Slide 19

Slide 19 text

Also The only time the extension should have direct access to an app is during init_app, otherwise it should use current_app. 19

Slide 20

Slide 20 text

class HelloExtension: def __init__(self, app=None): if app is not None: self.init_app(app) def init_app(self, app): app.before_request(...) 20

Slide 21

Slide 21 text

before_request(f) Register a function to run before each request. @app.before_request def load_user(): if "user_id" in session: g.user = db.session.get(session["user_id"]) # ... def init_app(self, app): app.before_request(greet_user) def greet_user(self): print('Welcome to FlaskCon') 21

Slide 22

Slide 22 text

So, building extensions is about 22

Slide 23

Slide 23 text

knowing Flask internals well ... 23

Slide 24

Slide 24 text

Useful tips 24

Slide 25

Slide 25 text

current_app g global variables, during request, per thread 25

Slide 26

Slide 26 text

Real app current_app._get_current_object() 26

Slide 27

Slide 27 text

blueprint also has cli.group 27

Slide 28

Slide 28 text

Common ideas 28

Slide 29

Slide 29 text

Configs -> Flask app config 29

Slide 30

Slide 30 text

flask-tabler-icons 30

Slide 31

Slide 31 text

from typing import Any from flask import Blueprint, Flask # version is same as tabler-icons __version__ = "3.3.0" class TablerIcons: def __init__(self, app: Any = None) -> None: if app is not None: self.init_app(app) def init_app(self, app: Flask) -> None: if not hasattr(app, "extensions"): app.extensions = {} app.extensions["tabler_icons"] = self bp = Blueprint( "tabler_icons", __name__, static_folder="static", static_url_path=f"/tabler-icons{app.static_url_path}", template_folder="templates", ) app.register_blueprint(bp) app.jinja_env.globals["tabler_icons"] = self app.config.setdefault("TABLER_ICON_SIZE", 24) 31

Slide 32

Slide 32 text

{% macro fill_css(value="", prefix="", yes_value="", postfix="") %} ... {% endmacro %} {% macro render_icon(icon, color="", bold=false, class="", style="", animation="", css_color="", size=0) %} {% if css_color %} {% set css_color = "color: %s;"|format(css_color) %} {% endif %} {% set bold = " font-weight: bold;" if bold else "" %} {% set animation_css = fill_css(animation, ' icon-') %} {% if color or class %} {% set class = "%s %s"|format(class, ("text-" + color) if color else "") %} {% endif %} {% endmacro %} 32

Slide 33

Slide 33 text

static/tabler-icon.svg 33

Slide 34

Slide 34 text

flask-MonitoringDashboard 34

Slide 35

Slide 35 text

from flask import Flask import flask_monitoringdashboard as dashboard app = Flask(__name__) @app.route("/test") def test(): return 'ok' dashboard.bind(app) // here app.run() 35

Slide 36

Slide 36 text

Flask-sqlalchemy 36

Slide 37

Slide 37 text

def init_app(self, app: Flask) -> None: if "sqlalchemy" in app.extensions: raise RuntimeError( "A 'SQLAlchemy' instance has already been registered on this Flask app." " Import and use that instance instead." ) app.extensions["sqlalchemy"] = self app.teardown_appcontext(self._teardown_session) if self._add_models_to_shell: from .cli import add_models_to_shell app.shell_context_processor(add_models_to_shell) basic_uri: str | sa.engine.URL | None = app.config.setdefault( "SQLALCHEMY_DATABASE_URI", None ) basic_engine_options = self._engine_options.copy() basic_engine_options.update( app.config.setdefault("SQLALCHEMY_ENGINE_OPTIONS", {}) ) echo: bool = app.config.setdefault("SQLALCHEMY_ECHO", False) config_binds: dict[str | None, str | sa.engine.URL | dict[str, t.Any]] = ( app.config.setdefault("SQLALCHEMY_BINDS", {}) ) engine_options: dict[str | None, dict[str, t.Any]] = {} 37

Slide 38

Slide 38 text

# Build the engine config for each bind key. for key, value in config_binds.items(): engine_options[key] = self._engine_options.copy() if isinstance(value, (str, sa.engine.URL)): engine_options[key]["url"] = value else: engine_options[key].update(value) # Build the engine config for the default bind key. if basic_uri is not None: basic_engine_options["url"] = basic_uri if "url" in basic_engine_options: engine_options.setdefault(None, {}).update(basic_engine_options) if not engine_options: raise RuntimeError( "Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set." ) 38

Slide 39

Slide 39 text

engines = self._app_engines.setdefault(app, {}) # Dispose existing engines in case init_app is called again. if engines: for engine in engines.values(): engine.dispose() engines.clear() # Create the metadata and engine for each bind key. for key, options in engine_options.items(): self._make_metadata(key) options.setdefault("echo", echo) options.setdefault("echo_pool", echo) self._apply_driver_defaults(options, app) engines[key] = self._make_engine(key, options, app) if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False): from . import record_queries for engine in engines.values(): record_queries._listen(engine) if app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False): from . import track_modifications track_modifications._listen(self.session) 39

Slide 40

Slide 40 text

Plugins reaching EOL 40

Slide 41

Slide 41 text

See pallets-eco 41

Slide 42

Slide 42 text

That's all folks 42