Coverage for /opt/conda/envs/apienv/lib/python3.10/site-packages/daiquiri/core/__init__.py: 93%
141 statements
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-15 02:12 +0000
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-15 02:12 +0000
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3import inspect
4from abc import ABC, abstractmethod
6from flask import Blueprint, request, jsonify, g
7from flask_restful import Api, abort
8from flask_apispec import MethodResource, doc as apispec_doc, marshal_with, use_kwargs
10from marshmallow import fields
12import logging
14logger = logging.getLogger(__name__)
17def require_valid_session(fn):
18 """Require Valid Session
20 Decorate a route class http method with this to enforce that there is a valid
21 session token with the request
22 """
23 fn._require_valid_session = True
24 return fn
27def require_staff(fn):
28 """Require User to be Staff
30 Decorate a route class http method with this to enforce that there is a staff
31 member making the request
32 """
33 fn._require_staff = True
34 return fn
37def no_blsession_required(fn):
38 """Indicate that a blsession is not required"""
39 fn._no_blsession_required = True
40 return fn
43def require_control(fn):
44 """Require Control Decorator
46 Decorate a route class http method with this to enforce that the current session
47 has control for the request to be served
48 """
49 fn._require_control = True
50 return fn
53# @doc(description="Get list of current sessions", tags=["session"])
54def doc(description, tags=[]):
55 def wrap(f):
56 return apispec_doc(description=description)(f)
58 return wrap
61# @marshal_with(SessionListSchema(many=True), code=200)
62# @marshal_with(ErrorSchema(), code=401)
63def marshal(
64 inp=None, out=[], paged=False, filtered=False, ordered=False, inp_location=None
65):
66 """Marshal Decorator
68 Marshals this request with input and output schemas, and appends any
69 input schema
71 Args:
72 inp (schema): Input schema to validate request with
73 out (list[list[code, schema, description]]): A list of response codes, schemas, and descriptions
74 paged (bool): Enable paging inputs: per_page, page
75 filtered (bool): Enable filtering inputs: s
76 ordered (bool): Enable ordering inputs: order, order_by
77 """
78 # Circular import :(
79 from daiquiri.core.schema.validators import OneOf
81 def wrap(f):
82 inp_copy = inp
83 if (inp_copy is None) and (paged or filtered or ordered):
84 inp_copy = {}
86 if inp_copy is not None:
87 if filtered:
88 inp_copy["s"] = fields.Str()
90 if paged:
91 inp_copy["page"] = fields.Int()
92 inp_copy["per_page"] = fields.Int()
94 if ordered:
95 inp_copy["order"] = OneOf(["asc", "desc"], dump_default="asc")
96 inp_copy["order_by"] = fields.Str()
98 location = inp_location
99 if f.__name__ == "get" and inp_location is None:
100 location = "query"
102 f = use_kwargs(inp_copy, location=location)(f)
103 if inspect.isclass(inp_copy):
104 f.__schemas__ = [inp_copy()]
106 for i in out:
107 desc = None
108 if len(i) > 2:
109 desc = i[2]
110 f = marshal_with(i[1], code=i[0], description=desc)(f)
112 return f
114 return wrap
117class CoreResource(MethodResource):
118 """CoreResource is the base resource class that all flask Resources inherit from
120 kwargs are mapped to _(kwargs) for convenince
121 """
123 def __init__(self, *args, **kwargs):
124 # logger.debug('Loaded {c}'.format(c=self.__class__.__name__))
125 self.__dict__.update(("_" + k, v) for k, v in kwargs.items())
128class CoreBase(ABC):
129 """CoreBase is the class that all application module inherit from
131 The CoreBase maps kwargs to _(kwargs) for convenience, sets up a blueprint if
132 _blueprint = True (default), provides a convenience method to register flask route
133 and registers swagger documentation. The blueprint is registered with a prefix of
134 the current class name
136 """
138 _blueprint = True
139 _base_url = None
140 _namespace = None
141 _require_session = False
142 _require_blsession = False
144 def __init__(self, *args, **kwargs):
145 self.initkwargs = kwargs
146 self.__dict__.update(("_" + k, v) for k, v in kwargs.items())
148 self._bp = self.__class__.__name__
149 if self._blueprint:
150 self._api_bp = Blueprint(self._bp, __name__)
151 self._api = Api(self._api_bp)
152 self._docs_to_reg = []
153 self._api_bp.before_request(self.before_request)
155 self.setup()
157 if self._blueprint:
158 if self._base_url is None:
159 logger.debug(
160 f"base_url is empty, defaulting to class name: {self._bp.lower()}"
161 )
162 self._base_url = self._bp.lower()
164 kwargs["app"].register_blueprint(
165 self._api_bp, url_prefix=f"/api/{self._base_url}"
166 )
167 for d in self._docs_to_reg:
168 kwargs["docs"].register(d, blueprint=self._bp)
170 self.after_setup()
172 def after_setup(self):
173 """Called after the setup of this component"""
174 pass
176 def after_all_setup(self, components):
177 """Called after the setup of the whole components"""
178 pass
180 def before_request(self):
181 """Flask before_request handler
183 Checks the current session is valid (delegates to Session.require_valid_session),
184 then check if the route class function has a _require control property as set by
185 the above decorator.
187 Returns:
188 None if everything is valid, otherwise return a json error to the request
189 """
190 logger.debug(f"Core:before_request {request.method}")
191 if request.method == "OPTIONS":
192 return
194 # This is delving pretty deep into the internals of flask
195 # May be a better way to achieve this
196 if request.endpoint in self._app.view_functions:
197 view_func = self._app.view_functions[request.endpoint]
198 cls = view_func.view_class
199 method = getattr(cls, request.method.lower())
201 if self._require_session or hasattr(method, "_require_valid_session"):
202 require_blsession = self._require_blsession
203 if hasattr(method, "_no_blsession_required"):
204 require_blsession = False
206 invalid = self._session.require_valid_session(
207 require_blsession=require_blsession
208 )
209 if invalid:
210 return jsonify(invalid[0]), invalid[1]
212 req_staff = hasattr(method, "_require_staff")
213 if req_staff:
214 staff = g.user.staff()
215 if not staff:
216 # The best response is to return 405 rather than give
217 # away that this resource might exist
218 abort(405)
220 req_control = hasattr(method, "_require_control")
221 if req_control:
222 nocontrol = self._session.require_control()
223 # print("before_request: no control", nocontrol)
224 if nocontrol:
225 return jsonify(nocontrol), 400
227 def register_route(self, route_class, url, route_keywords={}):
228 """Register a flask route with a route class
230 By default the parent (this class) is passed into the route class for convenience
231 The doc string from each http method on the class is registered into the swagger
232 docs. Any input schema are also registered into the schema resource for use by the
233 client
235 Args:
236 route_class (obj:CoreResource): A CoreResource class to register
237 url (str): The slug to register the route class under
238 route_keywors (dict): Any other keywords to pass to the route class
239 """
240 kwargs = {"parent": self}
241 self._api.add_resource(
242 route_class,
243 url,
244 resource_class_kwargs={**self.initkwargs, **kwargs, **route_keywords},
245 )
247 for k in ["post", "get", "put", "patch", "delete"]:
248 fn = getattr(route_class, k, None)
249 if fn:
250 security = []
251 if self._require_session or hasattr(fn, "_require_valid_session"):
252 security.append({"bearer": []})
254 fn = apispec_doc(
255 description=fn.__doc__, tags=[self._bp], security=security
256 )(fn)
257 if hasattr(fn, "__schemas__") and self._schema:
258 for sch in fn.__schemas__:
259 path = self._base_url if self._base_url else self._bp.lower()
260 self._schema.register(sch, f"/{path}{url}", k)
262 self._docs_to_reg.append(route_class)
264 def emit(self, *args, **kwargs):
265 """Convenience method to emit a socketio event.
267 If the emit fails, a log is emitted but the method do not raise any
268 exception.
269 """
270 if "namespace" not in kwargs:
271 kwargs["namespace"] = f"/{self._namespace}"
273 try:
274 self._socketio.emit(*args, **kwargs)
275 except Exception:
276 logger.error("Error while emitting socketio", exc_info=True)
278 def on(self, *args, **kwargs):
279 """Convenience method to register a callback for a socketio event"""
280 if "namespace" not in kwargs:
281 kwargs["namespace"] = f"/{self._namespace}"
283 if (
284 args[0] in ["connect", "disconnect"]
285 and self.__class__.__name__ != "Session"
286 ):
287 raise KeyError(
288 "Connect and disconnect events cannot be overridden by components"
289 )
291 return self._socketio.on(*args, **kwargs)
293 @abstractmethod
294 def setup(self):
295 """Setup Initialiser
297 Abstract method for any setup for the child class
298 """
299 pass
301 def reload(self):
302 """Component Reloader
304 A function that can reload the component once the config file
305 has been reloaded. Each component must decide how much of its
306 internal state to reload
307 """
308 pass