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

1#!/usr/bin/env python 

2# -*- coding: utf-8 -*- 

3import inspect 

4from abc import ABC, abstractmethod 

5 

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 

9 

10from marshmallow import fields 

11 

12import logging 

13 

14logger = logging.getLogger(__name__) 

15 

16 

17def require_valid_session(fn): 

18 """Require Valid Session 

19 

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 

25 

26 

27def require_staff(fn): 

28 """Require User to be Staff 

29 

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 

35 

36 

37def no_blsession_required(fn): 

38 """Indicate that a blsession is not required""" 

39 fn._no_blsession_required = True 

40 return fn 

41 

42 

43def require_control(fn): 

44 """Require Control Decorator 

45 

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 

51 

52 

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) 

57 

58 return wrap 

59 

60 

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 

67 

68 Marshals this request with input and output schemas, and appends any 

69 input schema 

70 

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 

80 

81 def wrap(f): 

82 inp_copy = inp 

83 if (inp_copy is None) and (paged or filtered or ordered): 

84 inp_copy = {} 

85 

86 if inp_copy is not None: 

87 if filtered: 

88 inp_copy["s"] = fields.Str() 

89 

90 if paged: 

91 inp_copy["page"] = fields.Int() 

92 inp_copy["per_page"] = fields.Int() 

93 

94 if ordered: 

95 inp_copy["order"] = OneOf(["asc", "desc"], dump_default="asc") 

96 inp_copy["order_by"] = fields.Str() 

97 

98 location = inp_location 

99 if f.__name__ == "get" and inp_location is None: 

100 location = "query" 

101 

102 f = use_kwargs(inp_copy, location=location)(f) 

103 if inspect.isclass(inp_copy): 

104 f.__schemas__ = [inp_copy()] 

105 

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) 

111 

112 return f 

113 

114 return wrap 

115 

116 

117class CoreResource(MethodResource): 

118 """CoreResource is the base resource class that all flask Resources inherit from 

119 

120 kwargs are mapped to _(kwargs) for convenince 

121 """ 

122 

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()) 

126 

127 

128class CoreBase(ABC): 

129 """CoreBase is the class that all application module inherit from 

130 

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 

135 

136 """ 

137 

138 _blueprint = True 

139 _base_url = None 

140 _namespace = None 

141 _require_session = False 

142 _require_blsession = False 

143 

144 def __init__(self, *args, **kwargs): 

145 self.initkwargs = kwargs 

146 self.__dict__.update(("_" + k, v) for k, v in kwargs.items()) 

147 

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) 

154 

155 self.setup() 

156 

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() 

163 

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) 

169 

170 self.after_setup() 

171 

172 def after_setup(self): 

173 """Called after the setup of this component""" 

174 pass 

175 

176 def after_all_setup(self, components): 

177 """Called after the setup of the whole components""" 

178 pass 

179 

180 def before_request(self): 

181 """Flask before_request handler 

182 

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. 

186 

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 

193 

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()) 

200 

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 

205 

206 invalid = self._session.require_valid_session( 

207 require_blsession=require_blsession 

208 ) 

209 if invalid: 

210 return jsonify(invalid[0]), invalid[1] 

211 

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) 

219 

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 

226 

227 def register_route(self, route_class, url, route_keywords={}): 

228 """Register a flask route with a route class 

229 

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 

234 

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 ) 

246 

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": []}) 

253 

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) 

261 

262 self._docs_to_reg.append(route_class) 

263 

264 def emit(self, *args, **kwargs): 

265 """Convenience method to emit a socketio event. 

266 

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}" 

272 

273 try: 

274 self._socketio.emit(*args, **kwargs) 

275 except Exception: 

276 logger.error("Error while emitting socketio", exc_info=True) 

277 

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}" 

282 

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 ) 

290 

291 return self._socketio.on(*args, **kwargs) 

292 

293 @abstractmethod 

294 def setup(self): 

295 """Setup Initialiser 

296 

297 Abstract method for any setup for the child class 

298 """ 

299 pass 

300 

301 def reload(self): 

302 """Component Reloader 

303 

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