Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/flask_restx/namespace.py: 28%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

163 statements  

1import inspect 

2import warnings 

3import logging 

4from collections import namedtuple, OrderedDict 

5 

6from flask import request 

7from flask.views import http_method_funcs 

8 

9from ._http import HTTPStatus 

10from .errors import abort 

11from .marshalling import marshal, marshal_with 

12from .model import Model, OrderedModel, SchemaModel 

13from .reqparse import RequestParser 

14from .utils import merge 

15 

16# Container for each route applied to a Resource using @ns.route decorator 

17ResourceRoute = namedtuple("ResourceRoute", "resource urls route_doc kwargs") 

18 

19 

20class Namespace(object): 

21 """ 

22 Group resources together. 

23 

24 Namespace is to API what :class:`flask:flask.Blueprint` is for :class:`flask:flask.Flask`. 

25 

26 :param str name: The namespace name 

27 :param str description: An optional short description 

28 :param str path: An optional prefix path. If not provided, prefix is ``/+name`` 

29 :param list decorators: A list of decorators to apply to each resources 

30 :param bool validate: Whether or not to perform validation on this namespace 

31 :param bool ordered: Whether or not to preserve order on models and marshalling 

32 :param Api api: an optional API to attache to the namespace 

33 """ 

34 

35 def __init__( 

36 self, 

37 name, 

38 description=None, 

39 path=None, 

40 decorators=None, 

41 validate=None, 

42 authorizations=None, 

43 ordered=False, 

44 **kwargs 

45 ): 

46 self.name = name 

47 self.description = description 

48 self._path = path 

49 

50 self._schema = None 

51 self._validate = validate 

52 self.models = {} 

53 self.urls = {} 

54 self.decorators = decorators if decorators else [] 

55 self.resources = [] # List[ResourceRoute] 

56 self.error_handlers = OrderedDict() 

57 self.default_error_handler = None 

58 self.authorizations = authorizations 

59 self.ordered = ordered 

60 self.apis = [] 

61 if "api" in kwargs: 

62 self.apis.append(kwargs["api"]) 

63 self.logger = logging.getLogger(__name__ + "." + self.name) 

64 

65 @property 

66 def path(self): 

67 return (self._path or ("/" + self.name)).rstrip("/") 

68 

69 def add_resource(self, resource, *urls, **kwargs): 

70 """ 

71 Register a Resource for a given API Namespace 

72 

73 :param Resource resource: the resource ro register 

74 :param str urls: one or more url routes to match for the resource, 

75 standard flask routing rules apply. 

76 Any url variables will be passed to the resource method as args. 

77 :param str endpoint: endpoint name (defaults to :meth:`Resource.__name__.lower` 

78 Can be used to reference this route in :class:`fields.Url` fields 

79 :param list|tuple resource_class_args: args to be forwarded to the constructor of the resource. 

80 :param dict resource_class_kwargs: kwargs to be forwarded to the constructor of the resource. 

81 

82 Additional keyword arguments not specified above will be passed as-is 

83 to :meth:`flask.Flask.add_url_rule`. 

84 

85 Examples:: 

86 

87 namespace.add_resource(HelloWorld, '/', '/hello') 

88 namespace.add_resource(Foo, '/foo', endpoint="foo") 

89 namespace.add_resource(FooSpecial, '/special/foo', endpoint="foo") 

90 """ 

91 route_doc = kwargs.pop("route_doc", {}) 

92 self.resources.append(ResourceRoute(resource, urls, route_doc, kwargs)) 

93 for api in self.apis: 

94 ns_urls = api.ns_urls(self, urls) 

95 api.register_resource(self, resource, *ns_urls, **kwargs) 

96 

97 def route(self, *urls, **kwargs): 

98 """ 

99 A decorator to route resources. 

100 """ 

101 

102 def wrapper(cls): 

103 doc = kwargs.pop("doc", None) 

104 if doc is not None: 

105 # build api doc intended only for this route 

106 kwargs["route_doc"] = self._build_doc(cls, doc) 

107 self.add_resource(cls, *urls, **kwargs) 

108 return cls 

109 

110 return wrapper 

111 

112 def _build_doc(self, cls, doc): 

113 if doc is False: 

114 return False 

115 unshortcut_params_description(doc) 

116 handle_deprecations(doc) 

117 for http_method in http_method_funcs: 

118 if http_method in doc: 

119 if doc[http_method] is False: 

120 continue 

121 unshortcut_params_description(doc[http_method]) 

122 handle_deprecations(doc[http_method]) 

123 if "expect" in doc[http_method] and not isinstance( 

124 doc[http_method]["expect"], (list, tuple) 

125 ): 

126 doc[http_method]["expect"] = [doc[http_method]["expect"]] 

127 return merge(getattr(cls, "__apidoc__", {}), doc) 

128 

129 def doc(self, shortcut=None, **kwargs): 

130 """A decorator to add some api documentation to the decorated object""" 

131 if isinstance(shortcut, str): 

132 kwargs["id"] = shortcut 

133 show = shortcut if isinstance(shortcut, bool) else True 

134 

135 def wrapper(documented): 

136 documented.__apidoc__ = self._build_doc( 

137 documented, kwargs if show else False 

138 ) 

139 return documented 

140 

141 return wrapper 

142 

143 def hide(self, func): 

144 """A decorator to hide a resource or a method from specifications""" 

145 return self.doc(False)(func) 

146 

147 def abort(self, *args, **kwargs): 

148 """ 

149 Properly abort the current request 

150 

151 See: :func:`~flask_restx.errors.abort` 

152 """ 

153 abort(*args, **kwargs) 

154 

155 def add_model(self, name, definition): 

156 self.models[name] = definition 

157 for api in self.apis: 

158 api.models[name] = definition 

159 return definition 

160 

161 def model(self, name=None, model=None, mask=None, strict=False, **kwargs): 

162 """ 

163 Register a model 

164 

165 :param bool strict - should model validation raise error when non-specified param 

166 is provided? 

167 

168 .. seealso:: :class:`Model` 

169 """ 

170 cls = OrderedModel if self.ordered else Model 

171 model = cls(name, model, mask=mask, strict=strict) 

172 model.__apidoc__.update(kwargs) 

173 return self.add_model(name, model) 

174 

175 def schema_model(self, name=None, schema=None): 

176 """ 

177 Register a model 

178 

179 .. seealso:: :class:`Model` 

180 """ 

181 model = SchemaModel(name, schema) 

182 return self.add_model(name, model) 

183 

184 def extend(self, name, parent, fields): 

185 """ 

186 Extend a model (Duplicate all fields) 

187 

188 :deprecated: since 0.9. Use :meth:`clone` instead 

189 """ 

190 if isinstance(parent, list): 

191 parents = parent + [fields] 

192 model = Model.extend(name, *parents) 

193 else: 

194 model = Model.extend(name, parent, fields) 

195 return self.add_model(name, model) 

196 

197 def clone(self, name, *specs): 

198 """ 

199 Clone a model (Duplicate all fields) 

200 

201 :param str name: the resulting model name 

202 :param specs: a list of models from which to clone the fields 

203 

204 .. seealso:: :meth:`Model.clone` 

205 

206 """ 

207 model = Model.clone(name, *specs) 

208 return self.add_model(name, model) 

209 

210 def inherit(self, name, *specs): 

211 """ 

212 Inherit a model (use the Swagger composition pattern aka. allOf) 

213 

214 .. seealso:: :meth:`Model.inherit` 

215 """ 

216 model = Model.inherit(name, *specs) 

217 return self.add_model(name, model) 

218 

219 def expect(self, *inputs, **kwargs): 

220 """ 

221 A decorator to Specify the expected input model 

222 

223 :param ModelBase|Parse inputs: An expect model or request parser 

224 :param bool validate: whether to perform validation or not 

225 

226 """ 

227 expect = [] 

228 params = {"validate": kwargs.get("validate", self._validate), "expect": expect} 

229 for param in inputs: 

230 expect.append(param) 

231 return self.doc(**params) 

232 

233 def parser(self): 

234 """Instanciate a :class:`~RequestParser`""" 

235 return RequestParser() 

236 

237 def as_list(self, field): 

238 """Allow to specify nested lists for documentation""" 

239 field.__apidoc__ = merge(getattr(field, "__apidoc__", {}), {"as_list": True}) 

240 return field 

241 

242 def marshal_with( 

243 self, fields, as_list=False, code=HTTPStatus.OK, description=None, **kwargs 

244 ): 

245 """ 

246 A decorator specifying the fields to use for serialization. 

247 

248 :param bool as_list: Indicate that the return type is a list (for the documentation) 

249 :param int code: Optionally give the expected HTTP response code if its different from 200 

250 

251 """ 

252 

253 def wrapper(func): 

254 doc = { 

255 "responses": { 

256 str(code): ( 

257 (description, [fields], kwargs) 

258 if as_list 

259 else (description, fields, kwargs) 

260 ) 

261 }, 

262 "__mask__": kwargs.get( 

263 "mask", True 

264 ), # Mask values can't be determined outside app context 

265 } 

266 func.__apidoc__ = merge(getattr(func, "__apidoc__", {}), doc) 

267 return marshal_with(fields, ordered=self.ordered, **kwargs)(func) 

268 

269 return wrapper 

270 

271 def marshal_list_with(self, fields, **kwargs): 

272 """A shortcut decorator for :meth:`~Api.marshal_with` with ``as_list=True``""" 

273 return self.marshal_with(fields, True, **kwargs) 

274 

275 def marshal(self, *args, **kwargs): 

276 """A shortcut to the :func:`marshal` helper""" 

277 return marshal(*args, **kwargs) 

278 

279 def errorhandler(self, exception): 

280 """A decorator to register an error handler for a given exception""" 

281 if inspect.isclass(exception) and issubclass(exception, Exception): 

282 # Register an error handler for a given exception 

283 def wrapper(func): 

284 self.error_handlers[exception] = func 

285 return func 

286 

287 return wrapper 

288 else: 

289 # Register the default error handler 

290 self.default_error_handler = exception 

291 return exception 

292 

293 def param(self, name, description=None, _in="query", **kwargs): 

294 """ 

295 A decorator to specify one of the expected parameters 

296 

297 :param str name: the parameter name 

298 :param str description: a small description 

299 :param str _in: the parameter location `(query|header|formData|body|cookie)` 

300 """ 

301 param = kwargs 

302 param["in"] = _in 

303 param["description"] = description 

304 return self.doc(params={name: param}) 

305 

306 def response(self, code, description, model=None, **kwargs): 

307 """ 

308 A decorator to specify one of the expected responses 

309 

310 :param int code: the HTTP status code 

311 :param str description: a small description about the response 

312 :param ModelBase model: an optional response model 

313 

314 """ 

315 return self.doc(responses={str(code): (description, model, kwargs)}) 

316 

317 def header(self, name, description=None, **kwargs): 

318 """ 

319 A decorator to specify one of the expected headers 

320 

321 :param str name: the HTTP header name 

322 :param str description: a description about the header 

323 

324 """ 

325 header = {"description": description} 

326 header.update(kwargs) 

327 return self.doc(headers={name: header}) 

328 

329 def produces(self, mimetypes): 

330 """A decorator to specify the MIME types the API can produce""" 

331 return self.doc(produces=mimetypes) 

332 

333 def deprecated(self, func): 

334 """A decorator to mark a resource or a method as deprecated""" 

335 return self.doc(deprecated=True)(func) 

336 

337 def vendor(self, *args, **kwargs): 

338 """ 

339 A decorator to expose vendor extensions. 

340 

341 Extensions can be submitted as dict or kwargs. 

342 The ``x-`` prefix is optionnal and will be added if missing. 

343 

344 See: http://swagger.io/specification/#specification-extensions-128 

345 """ 

346 for arg in args: 

347 kwargs.update(arg) 

348 return self.doc(vendor=kwargs) 

349 

350 @property 

351 def payload(self): 

352 """Store the input payload in the current request context""" 

353 return request.get_json() 

354 

355 

356def unshortcut_params_description(data): 

357 if "params" in data: 

358 for name, description in data["params"].items(): 

359 if isinstance(description, str): 

360 data["params"][name] = {"description": description} 

361 

362 

363def handle_deprecations(doc): 

364 if "parser" in doc: 

365 warnings.warn( 

366 "The parser attribute is deprecated, use expect instead", 

367 DeprecationWarning, 

368 stacklevel=2, 

369 ) 

370 doc["expect"] = doc.get("expect", []) + [doc.pop("parser")] 

371 if "body" in doc: 

372 warnings.warn( 

373 "The body attribute is deprecated, use expect instead", 

374 DeprecationWarning, 

375 stacklevel=2, 

376 ) 

377 doc["expect"] = doc.get("expect", []) + [doc.pop("body")]