"""Utitilities to make development easier.""" from functools import wraps from typing import Any, Callable, Union import flask from .html import Page sign_in_snippet = ( '' ) def signin(title: str = "Please Sign In") -> Page: """Return a sign-in page. Args: title (str): The title of the sign in page. Defaults to "Please Sign In". Returns: Page: The sign-in page. """ return Page(title=title, body=sign_in_snippet) def needs_signin(func: Callable = None, login_html: str = sign_in_snippet) -> Callable: """A decorator that enforces that the user is signed in before accessing the page. Args: func (Callable): The function passed in if used as a decorator. Defaults to None. login_html (str): The HTML to show when the user needs to sign in. Defaults to sign_in_snippet. Returns: Callable: The new handler. """ def decorator(func: Callable) -> Callable: @wraps(func) def handler(*args: Any, **kwargs: Any) -> flask.Response: if flask.request.signed_in: return func(*args, **kwargs) else: return login_html return handler if func is not None: # called with no options @needs_signin return decorator(func) else: # called with options, eg @needs_signin(login_html='...') return decorator def needs_params( *param_names: str, src: Union[str, dict], onerror: Callable[[str], flask.Response] = None, ) -> Callable: """Require paramaters before a handler can be activated. Args: param_names (str): The paramaters that must be in the request. src (Union[str, dict]): The source to get the paramaters from. Can be "form" to use flask.request.form (POST requests), "query" for flask.request.query (GET requests), or a custom dictionary. onerror (Callable): A function to handle when a paramater is missing. It will be passed the parameter that is missing. If no function is specified a handler that returns a descriptive error and 400 Bad Request status code will be used. Raises: TypeError: No paramaters were provided or an invalid one was provided. Returns: Callable: The new handler. """ if len(param_names) < 1: raise TypeError("You must specify at least one required paramater name") # If function is used as a decorator with no arguments, the first argument will be # a function, so type check all of the param names to catch mistakes if not all(isinstance(p, str) for p in param_names): raise TypeError("All paramater names should be strings.") def default_onerror(missing_param: str) -> flask.Response: return flask.make_response( f"Parameter {missing_param!r} is required but is missing", 400, mimetype="text/plain", ) if src in ["form", "query"]: src = getattr(flask.request, src) onerror = default_onerror if onerror is None else onerror def decorator(func: Callable) -> Callable: @wraps(func) def handler(*args: Any, **ignoredkwargs: Any) -> flask.Response: param_kwargs = {} for p in param_names: if p not in src: return onerror(p) param_kwargs[p] = src[p] return func(*args, **param_kwargs) return handler return decorator