• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

AdCombo / flask-combo-jsonapi / 3771624463

pending completion
3771624463

push

github

GitHub
Merge pull request #68 from AdCombo/fix__init_subclass__for_multi_project

60 of 60 new or added lines in 1 file covered. (100.0%)

1351 of 1620 relevant lines covered (83.4%)

0.83 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.88
/flask_combo_jsonapi/resource.py
1
"""This module contains the logic of resource management"""
2

3
import inspect
1✔
4
import typing as t
1✔
5

6
import simplejson as json
1✔
7

8
from werkzeug.wrappers import Response
1✔
9
from flask import request, url_for, make_response
1✔
10
from flask.wrappers import Response as FlaskResponse
1✔
11
from flask.views import MethodView
1✔
12
from marshmallow_jsonapi.exceptions import IncorrectTypeError
1✔
13
from marshmallow import ValidationError
1✔
14

15
from flask_combo_jsonapi.querystring import QueryStringManager as QSManager
1✔
16
from flask_combo_jsonapi.pagination import add_pagination_links
1✔
17
from flask_combo_jsonapi.exceptions import InvalidType, BadRequest, RelationNotFound, PluginMethodNotImplementedError, \
1✔
18
    ObjectNotFound
19
from flask_combo_jsonapi.decorators import check_headers, check_method_requirements, jsonapi_exception_formatter
1✔
20
from flask_combo_jsonapi.schema import compute_schema, get_relationships, get_model_field
1✔
21
from flask_combo_jsonapi.data_layers.base import BaseDataLayer
1✔
22
from flask_combo_jsonapi.data_layers.alchemy import SqlalchemyDataLayer
1✔
23
from flask_combo_jsonapi.utils import JSONEncoder
1✔
24

25

26
class Resource(MethodView):
1✔
27
    """Base resource class"""
28

29
    qs_manager_class = QSManager
1✔
30

31
    def __new__(cls):
1✔
32
        """Constructor of a resource instance"""
33
        if hasattr(cls, "_data_layer"):
1✔
34
            cls._data_layer.resource = cls
1✔
35
            cls._data_layer.post_init()
1✔
36

37
        return super().__new__(cls)
1✔
38

39
    @jsonapi_exception_formatter
1✔
40
    def dispatch_request(self, *args, **kwargs):
1✔
41
        """Logic of how to handle a request"""
42
        method = getattr(self, request.method.lower(), None)
1✔
43
        if method is None and request.method == "HEAD":
1✔
44
            method = getattr(self, "get", None)
1✔
45

46
        if method is None:
1✔
47
            raise AttributeError(f"Unimplemented method {request.method}")
×
48

49
        headers = {"Content-Type": "application/vnd.api+json"}
1✔
50

51
        response = method(*args, **kwargs)
1✔
52

53
        if isinstance(response, Response):
1✔
54
            response.headers.add("Content-Type", "application/vnd.api+json")
1✔
55
            return response
1✔
56

57
        if not isinstance(response, tuple):
1✔
58
            if isinstance(response, dict):
1✔
59
                response.update({"jsonapi": {"version": "1.0"}})
1✔
60
            return make_response(json.dumps(response, cls=JSONEncoder), 200, headers)
1✔
61

62
        try:
1✔
63
            data, status_code, headers = response
1✔
64
            headers.update({"Content-Type": "application/vnd.api+json"})
1✔
65
        except ValueError:
1✔
66
            pass
1✔
67

68
        try:
1✔
69
            data, status_code = response
1✔
70
        except ValueError:
1✔
71
            pass
1✔
72

73
        if isinstance(data, dict):
1✔
74
            data.update({"jsonapi": {"version": "1.0"}})
1✔
75

76
        if isinstance(data, FlaskResponse):
1✔
77
            data.headers.add("Content-Type", "application/vnd.api+json")
×
78
            data.status_code = status_code
×
79
            return data
×
80
        elif isinstance(data, str):
1✔
81
            json_reponse = data
×
82
        else:
83
            json_reponse = json.dumps(data, cls=JSONEncoder)
1✔
84

85
        return make_response(json_reponse, status_code, headers)
1✔
86

87

88
class ResourceList(Resource):
1✔
89
    """Base class of a resource list manager"""
90

91
    def __init__(self):
1✔
92
        """Constructor of a resource instance"""
93
        super().__init__()
1✔
94
        if not hasattr(self, "plugins"):
1✔
95
            self.plugins = []
1✔
96

97
    def __init_subclass__(cls, **kwargs: t.Any) -> None:
1✔
98
        """Constructor of a resource class"""
99
        super().__init_subclass__(**kwargs)
1✔
100
        if hasattr(cls, "data_layer"):
1✔
101
            if not isinstance(cls.data_layer, dict):
1✔
102
                raise Exception(f"You must provide a data layer information as dict in {cls.__name__}")
×
103

104
            if cls.data_layer.get("class") is not None and BaseDataLayer not in inspect.getmro(
1✔
105
                    cls.data_layer["class"]
106
            ):
107
                raise Exception(f"You must provide a data layer class inherited from BaseDataLayer in {cls.__name__}")
×
108

109
            data_layer_cls = cls.data_layer.get("class", SqlalchemyDataLayer)
1✔
110
            data_layer_kwargs = cls.data_layer
1✔
111
            cls._data_layer = data_layer_cls(data_layer_kwargs)
1✔
112

113
        if check_headers not in cls.decorators:
1✔
114
            decorators = [check_headers,]
1✔
115
            decorators.extend(cls.decorators)
1✔
116
            cls.decorators = decorators
1✔
117

118
        if not hasattr(cls, "plugins"):
1✔
119
            cls.plugins = []
1✔
120

121
    @check_method_requirements
1✔
122
    def get(self, *args, **kwargs):
1✔
123
        """Retrieve a collection of objects"""
124
        self.before_get(args, kwargs)
1✔
125

126
        qs = self.qs_manager_class(request.args, self.schema)
1✔
127

128
        objects_count, objects = self.get_collection(qs, kwargs)
1✔
129

130
        schema_kwargs = getattr(self, "get_schema_kwargs", dict())
1✔
131
        schema_kwargs.update({"many": True})
1✔
132

133
        self.before_marshmallow(args, kwargs)
1✔
134

135
        schema = compute_schema(self.schema, schema_kwargs, qs, qs.include)
1✔
136

137
        for i_plugins in self.plugins:
1✔
138
            try:
×
139
                i_plugins.after_init_schema_in_resource_list_get(
×
140
                    *args, schema=schema, model=self.data_layer["model"], **kwargs
141
                )
142
            except PluginMethodNotImplementedError:
×
143
                pass
×
144

145
        result = schema.dump(objects)
1✔
146

147
        view_kwargs = request.view_args if getattr(self, "view_kwargs", None) is True else dict()
1✔
148
        add_pagination_links(result, objects_count, qs, url_for(self.view, _external=True, **view_kwargs))
1✔
149

150
        result.update({"meta": {"count": objects_count}})
1✔
151

152
        final_result = self.after_get(result)
1✔
153

154
        return final_result
1✔
155

156
    @check_method_requirements
1✔
157
    def post(self, *args, **kwargs):
1✔
158
        """Create an object"""
159
        json_data = request.json or {}
1✔
160

161
        qs = self.qs_manager_class(request.args, self.schema)
1✔
162

163
        schema = compute_schema(self.schema, getattr(self, "post_schema_kwargs", dict()), qs, qs.include)
1✔
164

165
        for i_plugins in self.plugins:
1✔
166
            try:
×
167
                i_plugins.after_init_schema_in_resource_list_post(
×
168
                    *args, schema=schema, model=self.data_layer["model"], **kwargs
169
                )
170
            except PluginMethodNotImplementedError:
×
171
                pass
×
172

173
        try:
1✔
174
            data = schema.load(json_data)
1✔
175
        except IncorrectTypeError as e:
1✔
176
            errors = e.messages
1✔
177
            for error in errors["errors"]:
1✔
178
                error["status"] = "409"
1✔
179
                error["title"] = "Incorrect type"
1✔
180
            return errors, 409
1✔
181
        except ValidationError as e:
1✔
182
            errors = e.messages
1✔
183
            for message in errors["errors"]:
1✔
184
                message["status"] = "422"
1✔
185
                message["title"] = "Validation error"
1✔
186
            return errors, 422
1✔
187

188
        self.before_post(args, kwargs, data=data)
1✔
189

190
        obj = self.create_object(data, kwargs)
1✔
191

192
        if obj is None:
1✔
193
            result = {"data": None}
×
194
        else:
195
            result = schema.dump(obj)
1✔
196

197
        if (result["data"] or {}).get("links", {}).get("self"):
1✔
198
            final_result = (result, 201, {"Location": result["data"]["links"]["self"]})
1✔
199
        else:
200
            final_result = (result, 201)
×
201

202
        result = self.after_post(final_result)
1✔
203

204
        return result
1✔
205

206
    def before_get(self, args, kwargs):
1✔
207
        """Hook to make custom work before get method"""
208
        pass
1✔
209

210
    def after_get(self, result):
1✔
211
        """Hook to make custom work after get method"""
212
        return result
1✔
213

214
    def before_post(self, args, kwargs, data=None):
1✔
215
        """Hook to make custom work before post method"""
216
        pass
1✔
217

218
    def after_post(self, result):
1✔
219
        """Hook to make custom work after post method"""
220
        return result
1✔
221

222
    def before_marshmallow(self, args, kwargs):
1✔
223
        pass
1✔
224

225
    def get_collection(self, qs, kwargs):
1✔
226
        return self._data_layer.get_collection(qs, kwargs)
1✔
227

228
    def create_object(self, data, kwargs):
1✔
229
        return self._data_layer.create_object(data, kwargs)
1✔
230

231

232
class ResourceDetail(Resource):
1✔
233
    """Base class of a resource detail manager"""
234

235
    def __init__(self):
1✔
236
        """Constructor of a resource instance"""
237
        super().__init__()
1✔
238
        if not hasattr(self, "plugins"):
1✔
239
            self.plugins = []
1✔
240

241
    def __init_subclass__(cls, **kwargs: t.Any) -> None:
1✔
242
        """Constructor of a resource class"""
243
        super().__init_subclass__(**kwargs)
1✔
244
        if hasattr(cls, "data_layer"):
1✔
245
            if not isinstance(cls.data_layer, dict):
1✔
246
                raise Exception(f"You must provide a data layer information as dict in {cls.__name__}")
1✔
247

248
            if cls.data_layer.get("class") is not None and BaseDataLayer not in inspect.getmro(
1✔
249
                    cls.data_layer["class"]
250
            ):
251
                raise Exception(f"You must provide a data layer class inherited from BaseDataLayer in {cls.__name__}")
1✔
252

253
            data_layer_cls = cls.data_layer.get("class", SqlalchemyDataLayer)
1✔
254
            data_layer_kwargs = cls.data_layer
1✔
255
            cls._data_layer = data_layer_cls(data_layer_kwargs)
1✔
256

257
        if check_headers not in cls.decorators:
1✔
258
            decorators = [check_headers,]
1✔
259
            decorators.extend(cls.decorators)
1✔
260
            cls.decorators = decorators
1✔
261

262
        if not hasattr(cls, "plugins"):
1✔
263
            cls.plugins = []
1✔
264

265
    @check_method_requirements
1✔
266
    def get(self, *args, **kwargs):
1✔
267
        """Get object details"""
268
        self.before_get(args, kwargs)
1✔
269

270
        qs = self.qs_manager_class(request.args, self.schema)
1✔
271

272
        obj = self.get_object(kwargs, qs)
1✔
273

274
        if obj is None:
1✔
275
            url_field = getattr(self._data_layer, "url_field", "id")
1✔
276
            value = f" '{kwargs.get(url_field)}'" if kwargs.get(url_field) else ""
1✔
277
            raise ObjectNotFound(f"{self.data_layer['model'].__name__}{value} not found.")
1✔
278

279
        self.before_marshmallow(args, kwargs)
1✔
280

281
        schema = compute_schema(self.schema, getattr(self, "get_schema_kwargs", dict()), qs, qs.include)
1✔
282

283
        for i_plugins in self.plugins:
1✔
284
            try:
×
285
                i_plugins.after_init_schema_in_resource_detail_get(
×
286
                    *args, schema=schema, model=self.data_layer["model"], **kwargs
287
                )
288
            except PluginMethodNotImplementedError:
×
289
                pass
×
290

291
        result = schema.dump(obj)
1✔
292

293
        final_result = self.after_get(result)
1✔
294

295
        return final_result
1✔
296

297
    @check_method_requirements
1✔
298
    def patch(self, *args, **kwargs):
1✔
299
        """Update an object"""
300
        json_data = request.json or {}
1✔
301

302
        qs = self.qs_manager_class(request.args, self.schema)
1✔
303
        schema_kwargs = getattr(self, "patch_schema_kwargs", dict())
1✔
304
        schema_kwargs.update({"partial": True})
1✔
305

306
        self.before_marshmallow(args, kwargs)
1✔
307

308
        schema = compute_schema(self.schema, schema_kwargs, qs, qs.include)
1✔
309

310
        for i_plugins in self.plugins:
1✔
311
            try:
×
312
                i_plugins.after_init_schema_in_resource_detail_patch(
×
313
                    *args, schema=schema, model=self.data_layer["model"], **kwargs
314
                )
315
            except PluginMethodNotImplementedError:
×
316
                pass
×
317

318
        if "data" not in json_data:
1✔
319
            raise BadRequest('Missing "data" node', source={"pointer": "/data"})
1✔
320
        if "id" not in json_data["data"]:
1✔
321
            raise BadRequest('Missing id in "data" node', source={"pointer": "/data/id"})
1✔
322
        if str(json_data["data"]["id"]) != str(kwargs[getattr(self._data_layer, "url_field", "id")]):
1✔
323
            raise BadRequest(
1✔
324
                "Value of id does not match the resource identifier in url", source={"pointer": "/data/id"}
325
            )
326

327
        try:
1✔
328
            data = schema.load(json_data)
1✔
329
        except IncorrectTypeError as e:
1✔
330
            errors = e.messages
1✔
331
            for error in errors["errors"]:
1✔
332
                error["status"] = "409"
1✔
333
                error["title"] = "Incorrect type"
1✔
334
            return errors, 409
1✔
335
        except ValidationError as e:
1✔
336
            errors = e.messages
1✔
337
            for message in errors["errors"]:
1✔
338
                message["status"] = "422"
1✔
339
                message["title"] = "Validation error"
1✔
340
            return errors, 422
1✔
341

342
        self.before_patch(args, kwargs, data=data)
1✔
343

344
        obj = self.update_object(data, qs, kwargs)
1✔
345

346
        result = schema.dump(obj)
1✔
347

348
        final_result = self.after_patch(result)
1✔
349

350
        return final_result
1✔
351

352
    @check_method_requirements
1✔
353
    def delete(self, *args, **kwargs):
1✔
354
        """Delete an object"""
355
        self.before_delete(args, kwargs)
1✔
356

357
        self.delete_object(kwargs)
1✔
358

359
        result = {"meta": {"message": "Object successfully deleted"}}
1✔
360

361
        final_result = self.after_delete(result)
1✔
362

363
        return final_result
1✔
364

365
    def before_get(self, args, kwargs):
1✔
366
        """Hook to make custom work before get method"""
367
        pass
1✔
368

369
    def after_get(self, result):
1✔
370
        """Hook to make custom work after get method"""
371
        return result
1✔
372

373
    def before_patch(self, args, kwargs, data=None):
1✔
374
        """Hook to make custom work before patch method"""
375
        pass
1✔
376

377
    def after_patch(self, result):
1✔
378
        """Hook to make custom work after patch method"""
379
        return result
1✔
380

381
    def before_delete(self, args, kwargs):
1✔
382
        """Hook to make custom work before delete method"""
383
        pass
1✔
384

385
    def after_delete(self, result):
1✔
386
        """Hook to make custom work after delete method"""
387
        return result
1✔
388

389
    def before_marshmallow(self, args, kwargs):
1✔
390
        pass
1✔
391

392
    def get_object(self, kwargs, qs):
1✔
393
        return self._data_layer.get_object(kwargs, qs=qs)
1✔
394

395
    def update_object(self, data, qs, kwargs):
1✔
396
        obj = self._data_layer.get_object(kwargs, qs=qs)
1✔
397
        self._data_layer.update_object(obj, data, kwargs)
1✔
398

399
        return obj
1✔
400

401
    def delete_object(self, kwargs):
1✔
402
        obj = self._data_layer.get_object(kwargs)
1✔
403
        self._data_layer.delete_object(obj, kwargs)
1✔
404

405

406
class ResourceRelationship(Resource):
1✔
407
    """Base class of a resource relationship manager"""
408

409
    def __init__(self):
1✔
410
        """Constructor of a resource instance"""
411
        super().__init__()
1✔
412
        if not hasattr(self, "plugins"):
1✔
413
            self.plugins = []
×
414

415
    def __init_subclass__(cls, **kwargs: t.Any) -> None:
1✔
416
        """Constructor of a resource class"""
417
        super().__init_subclass__(**kwargs)
1✔
418
        if hasattr(cls, "data_layer"):
1✔
419
            if not isinstance(cls.data_layer, dict):
1✔
420
                raise Exception(f"You must provide a data layer information as dict in {cls.__name__}")
×
421

422
            if cls.data_layer.get("class") is not None and BaseDataLayer not in inspect.getmro(
1✔
423
                    cls.data_layer["class"]
424
            ):
425
                raise Exception(f"You must provide a data layer class inherited from BaseDataLayer in {cls.__name__}")
×
426

427
            data_layer_cls = cls.data_layer.get("class", SqlalchemyDataLayer)
1✔
428
            data_layer_kwargs = cls.data_layer
1✔
429
            cls._data_layer = data_layer_cls(data_layer_kwargs)
1✔
430

431
        if check_headers not in cls.decorators:
1✔
432
            decorators = [check_headers,]
1✔
433
            decorators.extend(cls.decorators)
1✔
434
            cls.decorators = decorators
1✔
435

436
        if not hasattr(cls, "plugins"):
1✔
437
            cls.plugins = []
1✔
438

439
    @check_method_requirements
1✔
440
    def get(self, *args, **kwargs):
1✔
441
        """Get a relationship details"""
442
        self.before_get(args, kwargs)
1✔
443

444
        relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
1✔
445

446
        obj, data = self._data_layer.get_relationship(model_relationship_field, related_type_, related_id_field, kwargs)
1✔
447

448
        result = {
1✔
449
            "links": {
450
                "self": request.path,
451
                "related": self.schema._declared_fields[relationship_field].get_related_url(obj),
452
            },
453
            "data": data,
454
        }
455

456
        qs = self.qs_manager_class(request.args, self.schema)
1✔
457
        if qs.include:
1✔
458
            schema = compute_schema(self.schema, dict(), qs, qs.include)
1✔
459

460
            serialized_obj = schema.dump(obj)
1✔
461
            result["included"] = serialized_obj.get("included", dict())
1✔
462

463
        final_result = self.after_get(result)
1✔
464

465
        return final_result
1✔
466

467
    def _get_validated_json_payload(self, related_type_) -> dict:
1✔
468
        """
469
        Extracting json and validating its fields
470
        :return:
471
        """
472
        json_data = request.json or {}
1✔
473

474
        if "data" not in json_data:
1✔
475
            raise BadRequest('You must provide data with a "data" route node', source={"pointer": "/data"})
1✔
476
        if isinstance(json_data["data"], dict):
1✔
477
            if "type" not in json_data["data"]:
1✔
478
                raise BadRequest('Missing type in "data" node', source={"pointer": "/data/type"})
1✔
479
            if "id" not in json_data["data"]:
1✔
480
                raise BadRequest('Missing id in "data" node', source={"pointer": "/data/id"})
1✔
481
            if json_data["data"]["type"] != related_type_:
1✔
482
                raise InvalidType("The type field does not match the resource type", source={"pointer": "/data/type"})
1✔
483
        if isinstance(json_data["data"], list):
1✔
484
            for obj in json_data["data"]:
1✔
485
                if "type" not in obj:
1✔
486
                    raise BadRequest('Missing type in "data" node', source={"pointer": "/data/type"})
1✔
487
                if "id" not in obj:
1✔
488
                    raise BadRequest('Missing id in "data" node', source={"pointer": "/data/id"})
1✔
489
                if obj["type"] != related_type_:
1✔
490
                    raise InvalidType(
1✔
491
                        "The type provided does not match the resource type", source={"pointer": "/data/type"}
492
                    )
493

494
        return json_data
1✔
495

496
    @check_method_requirements
1✔
497
    def post(self, *args, **kwargs):
1✔
498
        """
499
        Add / create relationship(s)
500

501
        https://jsonapi.org/format/#crud-updating-to-many-relationships
502
        """
503
        relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
1✔
504
        json_data = self._get_validated_json_payload(related_type_)
1✔
505
        self.before_post(args, kwargs, json_data=json_data)
1✔
506

507
        obj_, updated = self._data_layer.create_relationship(
1✔
508
            json_data, model_relationship_field, related_id_field, kwargs
509
        )
510

511
        status_code = 200
1✔
512
        result = {"meta": {"message": "Relationship successfully created"}}
1✔
513

514
        if updated is False:
1✔
515
            result = ""
×
516
            status_code = 204
×
517

518
        final_result = self.after_post(result, status_code)
1✔
519

520
        return final_result
1✔
521

522
    @check_method_requirements
1✔
523
    def patch(self, *args, **kwargs):
1✔
524
        """
525
        Update a relationship
526

527
        # https://jsonapi.org/format/#crud-updating-relationship-responses-200
528

529
        > If a server accepts an update but also changes the targeted relationship(s)
530
        > in other ways than those specified by the request,
531
        > it MUST return a 200 OK response.
532
        > The response document MUST include a representation
533
        > of the updated relationship(s).
534

535
        > A server MUST return a 200 OK status code if an update is successful,
536
        > the client’s current data remain up to date,
537
        > and the server responds only with top-level meta data.
538
        > In this case the server MUST NOT include a representation
539
        > of the updated relationship(s).
540
        """
541
        relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
1✔
542

543
        json_data = self._get_validated_json_payload(related_type_)
1✔
544
        self.before_patch(args, kwargs, json_data=json_data)
1✔
545

546
        obj_, updated = self._data_layer.update_relationship(
1✔
547
            json_data, model_relationship_field, related_id_field, kwargs
548
        )
549

550
        status_code = 200
1✔
551
        result = {"meta": {"message": "Relationship successfully updated"}}
1✔
552

553
        if updated is False:
1✔
554
            result = ""
×
555
            status_code = 204
×
556

557
        final_result = self.after_patch(result, status_code)
1✔
558

559
        return final_result
1✔
560

561
    @check_method_requirements
1✔
562
    def delete(self, *args, **kwargs):
1✔
563
        """Delete relationship(s)"""
564

565
        relationship_field, model_relationship_field, related_type_, related_id_field = self._get_relationship_data()
1✔
566
        json_data = self._get_validated_json_payload(related_type_)
1✔
567
        self.before_delete(args, kwargs, json_data=json_data)
1✔
568

569
        obj_, updated = self._data_layer.delete_relationship(
1✔
570
            json_data, model_relationship_field, related_id_field, kwargs
571
        )
572

573
        status_code = 200
1✔
574
        result = {"meta": {"message": "Relationship successfully updated"}}
1✔
575

576
        if updated is False:
1✔
577
            result = ""
×
578
            status_code = 204
×
579

580
        final_result = self.after_delete(result, status_code)
1✔
581

582
        return final_result
1✔
583

584
    def _get_relationship_data(self):
1✔
585
        """Get useful data for relationship management"""
586
        relationship_field = request.path.split("/")[-1].replace("-", "_")
1✔
587

588
        if relationship_field not in get_relationships(self.schema):
1✔
589
            raise RelationNotFound(f"{self.schema.__name__} has no attribute {relationship_field}")
1✔
590

591
        related_type_ = self.schema._declared_fields[relationship_field].type_
1✔
592
        related_id_field = self.schema._declared_fields[relationship_field].id_field
1✔
593
        model_relationship_field = get_model_field(self.schema, relationship_field)
1✔
594

595
        return relationship_field, model_relationship_field, related_type_, related_id_field
1✔
596

597
    def before_get(self, args, kwargs):
1✔
598
        """Hook to make custom work before get method"""
599
        pass
1✔
600

601
    def after_get(self, result):
1✔
602
        """Hook to make custom work after get method"""
603
        return result
1✔
604

605
    def before_post(self, args, kwargs, json_data=None):
1✔
606
        """Hook to make custom work before post method"""
607
        pass
1✔
608

609
    def after_post(self, result, status_code):
1✔
610
        """Hook to make custom work after post method"""
611
        return result, status_code
1✔
612

613
    def before_patch(self, args, kwargs, json_data=None):
1✔
614
        """Hook to make custom work before patch method"""
615
        pass
1✔
616

617
    def after_patch(self, result, status_code):
1✔
618
        """Hook to make custom work after patch method"""
619
        return result, status_code
1✔
620

621
    def before_delete(self, args, kwargs, json_data=None):
1✔
622
        """Hook to make custom work before delete method"""
623
        pass
1✔
624

625
    def after_delete(self, result, status_code):
1✔
626
        """Hook to make custom work after delete method"""
627
        return result, status_code
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc