• 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

83.87
/flask_combo_jsonapi/data_layers/alchemy.py
1
"""This module is a CRUD interface between resource managers and the sqlalchemy ORM"""
2
from typing import TYPE_CHECKING
1✔
3

4
if TYPE_CHECKING:
1✔
5
    from sqlalchemy.orm import Session as SessionType
×
6

7
from sqlalchemy.orm.exc import NoResultFound
1✔
8
from sqlalchemy.orm.collections import InstrumentedList
1✔
9
from sqlalchemy.inspection import inspect
1✔
10
from sqlalchemy.orm.attributes import QueryableAttribute
1✔
11
from sqlalchemy.orm import joinedload, ColumnProperty, RelationshipProperty
1✔
12
from marshmallow import class_registry
1✔
13
from marshmallow.base import SchemaABC
1✔
14

15
from flask_combo_jsonapi.data_layers.base import BaseDataLayer
1✔
16
from flask_combo_jsonapi.data_layers.sorting.alchemy import create_sorts
1✔
17
from flask_combo_jsonapi.exceptions import (
1✔
18
    RelationNotFound,
19
    RelatedObjectNotFound,
20
    JsonApiException,
21
    ObjectNotFound,
22
    InvalidInclude,
23
    InvalidType,
24
    PluginMethodNotImplementedError,
25
)
26
from flask_combo_jsonapi.data_layers.filtering.alchemy import create_filters
1✔
27
from flask_combo_jsonapi.schema import (
1✔
28
    get_model_field,
29
    get_related_schema,
30
    get_relationships,
31
    get_nested_fields,
32
    get_schema_field,
33
)
34
from flask_combo_jsonapi.utils import SPLIT_REL
1✔
35

36

37
class SqlalchemyDataLayer(BaseDataLayer):
1✔
38
    """Sqlalchemy data layer"""
39
    if TYPE_CHECKING:
1✔
40
        session: "SessionType"
×
41

42
    def __init__(self, kwargs):
1✔
43
        """Initialize an instance of SqlalchemyDataLayer
44

45
        :param dict kwargs: initialization parameters of an SqlalchemyDataLayer instance
46
        """
47
        super().__init__(kwargs)
1✔
48

49
        if not hasattr(self, "session"):
1✔
50
            raise Exception(
1✔
51
                f"You must provide a session in data_layer_kwargs to use sqlalchemy data layer in {self.resource.__name__}"
52
            )
53
        if not hasattr(self, "model"):
1✔
54
            raise Exception(
1✔
55
                f"You must provide a model in data_layer_kwargs to use sqlalchemy data layer in {self.resource.__name__}"
56
            )
57

58
        self.disable_collection_count: bool = False
1✔
59
        self.default_collection_count: int = -1
1✔
60

61
    def post_init(self):
1✔
62
        """
63
        Checking some props here
64
        :return:
65
        """
66
        if self.resource is None:
1✔
67
            # if working outside the resource, it's not assigned here
68
            return
×
69

70
        if not hasattr(self.resource, "disable_collection_count") or self.resource.disable_collection_count is False:
1✔
71
            return
1✔
72

73
        params = self.resource.disable_collection_count
1✔
74

75
        if isinstance(params, (bool, int)):
1✔
76
            self.disable_collection_count = bool(params)
×
77

78
        if isinstance(params, (tuple, list)):
1✔
79
            try:
1✔
80
                self.disable_collection_count, self.default_collection_count = params
1✔
81
            except ValueError:
×
82
                raise ValueError(
×
83
                    "Resource's attribute `disable_collection_count` "
84
                    "has to be bool or list/tuple with exactly 2 values!\n"
85
                    "For example `disable_collection_count = (True, 999)`"
86
                )
87
        # just ignoring other types, we don't know how to process them
88

89
    def create_object(self, data, view_kwargs):
1✔
90
        """Create an object through sqlalchemy
91

92
        :param dict data: the data validated by marshmallow
93
        :param dict view_kwargs: kwargs from the resource view
94
        :return DeclarativeMeta: an object from sqlalchemy
95
        """
96
        for i_plugins in self.resource.plugins:
1✔
97
            try:
×
98
                i_plugins.data_layer_before_create_object(data=data, view_kwargs=view_kwargs, self_json_api=self)
×
99
            except PluginMethodNotImplementedError:
×
100
                pass
×
101

102
        self.before_create_object(data, view_kwargs)
1✔
103

104
        relationship_fields = get_relationships(self.resource.schema, model_field=True)
1✔
105
        nested_fields = get_nested_fields(self.resource.schema, model_field=True)
1✔
106

107
        join_fields = relationship_fields + nested_fields
1✔
108

109
        for i_plugins in self.resource.plugins:
1✔
110
            try:
×
111
                data = i_plugins.data_layer_create_object_clean_data(
×
112
                    data=data, view_kwargs=view_kwargs, join_fields=join_fields, self_json_api=self,
113
                )
114
            except PluginMethodNotImplementedError:
×
115
                pass
×
116
        obj = self.model(**{key: value for (key, value) in data.items() if key not in join_fields})
1✔
117
        self.apply_relationships(data, obj)
1✔
118
        self.apply_nested_fields(data, obj)
1✔
119

120
        for i_plugins in self.resource.plugins:
1✔
121
            try:
×
122
                i_plugins.data_layer_after_create_object(
×
123
                    data=data, view_kwargs=view_kwargs, obj=obj, self_json_api=self,
124
                )
125
            except PluginMethodNotImplementedError:
×
126
                pass
×
127

128
        self.session.add(obj)
1✔
129
        try:
1✔
130
            self.session.commit()
1✔
131
        except JsonApiException as e:
1✔
132
            self.session.rollback()
×
133
            raise e
×
134
        except Exception as e:
1✔
135
            self.session.rollback()
1✔
136
            raise JsonApiException(f"Object creation error: {e}", source={"pointer": "/data"})
1✔
137

138
        self.after_create_object(obj, data, view_kwargs)
1✔
139

140
        return obj
1✔
141

142
    def get_object(self, view_kwargs, qs=None):
1✔
143
        """Retrieve an object through sqlalchemy
144

145
        :params dict view_kwargs: kwargs from the resource view
146
        :return DeclarativeMeta: an object from sqlalchemy
147
        """
148
        # Нужно выталкивать из sqlalchemy Закешированные запросы, иначе не удастся загрузить данные о current_user
149
        self.session.expire_all()
1✔
150

151
        self.before_get_object(view_kwargs)
1✔
152

153
        id_field = getattr(self, "id_field", inspect(self.model).primary_key[0].key)
1✔
154
        try:
1✔
155
            filter_field = getattr(self.model, id_field)
1✔
156
        except Exception:
1✔
157
            raise Exception(f"{self.model.__name__} has no attribute {id_field}")
1✔
158

159
        url_field = getattr(self, "url_field", "id")
1✔
160
        filter_value = view_kwargs[url_field]
1✔
161

162
        query = self.retrieve_object_query(view_kwargs, filter_field, filter_value)
1✔
163

164
        if self.resource is not None:
1✔
165
            for i_plugins in self.resource.plugins:
1✔
166
                try:
×
167
                    query = i_plugins.data_layer_get_object_update_query(
×
168
                        query=query, qs=qs, view_kwargs=view_kwargs, self_json_api=self,
169
                    )
170
                except PluginMethodNotImplementedError:
×
171
                    pass
×
172

173
        if qs is not None:
1✔
174
            query = self.eagerload_includes(query, qs)
1✔
175

176
        try:
1✔
177
            obj = query.one()
1✔
178
        except NoResultFound:
1✔
179
            obj = None
1✔
180

181
        self.after_get_object(obj, view_kwargs)
1✔
182

183
        return obj
1✔
184

185
    def get_collection_count(self, query, qs, view_kwargs) -> int:
1✔
186
        """
187
        :param query: SQLAlchemy query
188
        :param qs: QueryString
189
        :param view_kwargs: view kwargs
190
        :return:
191
        """
192
        if self.disable_collection_count is True:
1✔
193
            return self.default_collection_count
1✔
194

195
        return query.count()
1✔
196

197
    def get_collection(self, qs, view_kwargs):
1✔
198
        """Retrieve a collection of objects through sqlalchemy
199

200
        :param QueryStringManager qs: a querystring manager to retrieve information from url
201
        :param dict view_kwargs: kwargs from the resource view
202
        :return tuple: the number of object and the list of objects
203
        """
204
        # Нужно выталкивать из sqlalchemy Закешированные запросы, иначе не удастся загрузить данные о current_user
205
        self.session.expire_all()
1✔
206

207
        self.before_get_collection(qs, view_kwargs)
1✔
208

209
        query = self.query(view_kwargs)
1✔
210

211
        for i_plugins in self.resource.plugins:
1✔
212
            try:
×
213
                query = i_plugins.data_layer_get_collection_update_query(
×
214
                    query=query, qs=qs, view_kwargs=view_kwargs, self_json_api=self,
215
                )
216
            except PluginMethodNotImplementedError:
×
217
                pass
×
218

219
        if qs.filters:
1✔
220
            query = self.filter_query(query, qs.filters, self.model)
1✔
221

222
        if qs.sorting:
1✔
223
            query = self.sort_query(query, qs.sorting)
1✔
224

225
        objects_count = self.get_collection_count(query, qs, view_kwargs)
1✔
226

227
        if getattr(self, "eagerload_includes", True):
1✔
228
            query = self.eagerload_includes(query, qs)
1✔
229

230
        query = self.paginate_query(query, qs.pagination)
1✔
231

232
        collection = query.all()
1✔
233

234
        collection = self.after_get_collection(collection, qs, view_kwargs)
1✔
235

236
        return objects_count, collection
1✔
237

238
    def update_object(self, obj, data, view_kwargs):
1✔
239
        """Update an object through sqlalchemy
240

241
        :param DeclarativeMeta obj: an object from sqlalchemy
242
        :param dict data: the data validated by marshmallow
243
        :param dict view_kwargs: kwargs from the resource view
244
        :return boolean: True if object have changed else False
245
        """
246
        if obj is None:
1✔
247
            url_field = getattr(self, "url_field", "id")
×
248
            filter_value = view_kwargs[url_field]
×
249
            raise ObjectNotFound(f"{self.model.__name__}: {filter_value} not found", source={"parameter": url_field})
×
250

251
        self.before_update_object(obj, data, view_kwargs)
1✔
252

253
        relationship_fields = get_relationships(self.resource.schema, model_field=True)
1✔
254
        nested_fields = get_nested_fields(self.resource.schema, model_field=True)
1✔
255

256
        join_fields = relationship_fields + nested_fields
1✔
257

258
        for i_plugins in self.resource.plugins:
1✔
259
            try:
×
260
                data = i_plugins.data_layer_update_object_clean_data(
×
261
                    data=data, obj=obj, view_kwargs=view_kwargs, join_fields=join_fields, self_json_api=self,
262
                )
263
            except PluginMethodNotImplementedError:
×
264
                pass
×
265

266
        for key, value in data.items():
1✔
267
            if hasattr(obj, key) and key not in join_fields:
1✔
268
                setattr(obj, key, value)
1✔
269

270
        self.apply_relationships(data, obj)
1✔
271
        self.apply_nested_fields(data, obj)
1✔
272

273
        try:
1✔
274
            self.session.commit()
1✔
275
        except JsonApiException as e:
1✔
276
            self.session.rollback()
×
277
            raise e
×
278
        except Exception as e:
1✔
279
            self.session.rollback()
1✔
280
            orig_e = getattr(e, "orig", object)
1✔
281
            message = getattr(orig_e, "args", [])
1✔
282
            message = message[0] if message else None
1✔
283
            e = message if message else e
1✔
284
            raise JsonApiException("Update object error: " + str(e), source={"pointer": "/data"})
1✔
285

286
        self.after_update_object(obj, data, view_kwargs)
1✔
287

288
    def delete_object(self, obj, view_kwargs):
1✔
289
        """Delete an object through sqlalchemy
290

291
        :param DeclarativeMeta item: an item from sqlalchemy
292
        :param dict view_kwargs: kwargs from the resource view
293
        """
294
        if obj is None:
1✔
295
            url_field = getattr(self, "url_field", "id")
×
296
            filter_value = view_kwargs[url_field]
×
297
            raise ObjectNotFound(f"{self.model.__name__}: {filter_value} not found", source={"parameter": url_field})
×
298

299
        self.before_delete_object(obj, view_kwargs)
1✔
300

301
        for i_plugins in self.resource.plugins:
1✔
302
            try:
×
303
                i_plugins.data_layer_delete_object_clean_data(obj=obj, view_kwargs=view_kwargs, self_json_api=self)
×
304
            except PluginMethodNotImplementedError:
×
305
                pass
×
306

307
        self.session.delete(obj)
1✔
308
        try:
1✔
309
            self.session.commit()
1✔
310
        except JsonApiException as e:
1✔
311
            self.session.rollback()
×
312
            raise e
×
313
        except Exception as e:
1✔
314
            self.session.rollback()
1✔
315
            raise JsonApiException("Delete object error: " + str(e))
1✔
316

317
        self.after_delete_object(obj, view_kwargs)
1✔
318

319
    def create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
1✔
320
        """Create a relationship
321

322
        :param dict json_data: the request params
323
        :param str relationship_field: the model attribute used for relationship
324
        :param str related_id_field: the identifier field of the related model
325
        :param dict view_kwargs: kwargs from the resource view
326
        :return boolean: True if relationship have changed else False
327
        """
328
        self.before_create_relationship(json_data, relationship_field, related_id_field, view_kwargs)
1✔
329

330
        obj = self.get_object(view_kwargs)
1✔
331

332
        if obj is None:
1✔
333
            url_field = getattr(self, "url_field", "id")
×
334
            filter_value = view_kwargs[url_field]
×
335
            raise ObjectNotFound(f"{self.model.__name__}: {filter_value} not found", source={"parameter": url_field})
×
336

337
        if not hasattr(obj, relationship_field):
1✔
338
            raise RelationNotFound(f"{obj.__class__.__name__} has no attribute {relationship_field}")
1✔
339

340
        related_model = getattr(obj.__class__, relationship_field).property.mapper.class_
1✔
341

342
        updated = False
1✔
343

344
        if isinstance(json_data["data"], list):
1✔
345
            obj_ids = {str(getattr(obj__, related_id_field)) for obj__ in getattr(obj, relationship_field)}
1✔
346

347
            for obj_ in json_data["data"]:
1✔
348
                if obj_["id"] not in obj_ids:
1✔
349
                    getattr(obj, relationship_field).append(
1✔
350
                        self.get_related_object(related_model, related_id_field, obj_)
351
                    )
352
                    updated = True
1✔
353
        else:
354
            related_object = None
1✔
355

356
            if json_data["data"] is not None:
1✔
357
                related_object = self.get_related_object(related_model, related_id_field, json_data["data"])
1✔
358

359
            obj_id = getattr(getattr(obj, relationship_field), related_id_field, None)
1✔
360
            new_obj_id = getattr(related_object, related_id_field, None)
1✔
361
            if obj_id != new_obj_id:
1✔
362
                setattr(obj, relationship_field, related_object)
1✔
363
                updated = True
1✔
364

365
        try:
1✔
366
            self.session.commit()
1✔
367
        except JsonApiException as e:
1✔
368
            self.session.rollback()
×
369
            raise e
×
370
        except Exception as e:
1✔
371
            self.session.rollback()
1✔
372
            raise JsonApiException("Create relationship error: " + str(e))
1✔
373

374
        self.after_create_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
1✔
375

376
        return obj, updated
1✔
377

378
    def get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs):
1✔
379
        """Get a relationship
380

381
        :param str relationship_field: the model attribute used for relationship
382
        :param str related_type_: the related resource type
383
        :param str related_id_field: the identifier field of the related model
384
        :param dict view_kwargs: kwargs from the resource view
385
        :return tuple: the object and related object(s)
386
        """
387
        self.before_get_relationship(relationship_field, related_type_, related_id_field, view_kwargs)
1✔
388

389
        obj = self.get_object(view_kwargs)
1✔
390

391
        if obj is None:
1✔
392
            url_field = getattr(self, "url_field", "id")
×
393
            filter_value = view_kwargs[url_field]
×
394
            raise ObjectNotFound(f"{self.model.__name__}: {filter_value} not found", source={"parameter": url_field})
×
395

396
        if not hasattr(obj, relationship_field):
1✔
397
            raise RelationNotFound(f"{obj.__class__.__name__} has no attribute {relationship_field}")
1✔
398

399
        related_objects = getattr(obj, relationship_field)
1✔
400

401
        if related_objects is None:
1✔
402
            return obj, related_objects
1✔
403

404
        self.after_get_relationship(
1✔
405
            obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs,
406
        )
407

408
        if isinstance(related_objects, InstrumentedList):
1✔
409
            return obj, [{"type": related_type_, "id": getattr(obj_, related_id_field)} for obj_ in related_objects]
1✔
410
        else:
411
            return obj, {"type": related_type_, "id": getattr(related_objects, related_id_field)}
1✔
412

413
    def update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
1✔
414
        """Update a relationship
415

416
        :param dict json_data: the request params
417
        :param str relationship_field: the model attribute used for relationship
418
        :param str related_id_field: the identifier field of the related model
419
        :param dict view_kwargs: kwargs from the resource view
420
        :return boolean: True if relationship have changed else False
421
        """
422
        self.before_update_relationship(json_data, relationship_field, related_id_field, view_kwargs)
1✔
423

424
        obj = self.get_object(view_kwargs)
1✔
425

426
        if obj is None:
1✔
427
            url_field = getattr(self, "url_field", "id")
×
428
            filter_value = view_kwargs[url_field]
×
429
            raise ObjectNotFound(f"{self.model.__name__}: {filter_value} not found", source={"parameter": url_field})
×
430

431
        if not hasattr(obj, relationship_field):
1✔
432
            raise RelationNotFound(f"{obj.__class__.__name__} has no attribute {relationship_field}")
1✔
433

434
        related_model = getattr(obj.__class__, relationship_field).property.mapper.class_
1✔
435

436
        updated = False
1✔
437

438
        if isinstance(json_data["data"], list):
1✔
439
            related_objects = []
1✔
440

441
            for obj_ in json_data["data"]:
1✔
442
                related_objects.append(self.get_related_object(related_model, related_id_field, obj_))
1✔
443

444
            obj_ids = {getattr(obj__, related_id_field) for obj__ in getattr(obj, relationship_field)}
1✔
445
            new_obj_ids = {getattr(related_object, related_id_field) for related_object in related_objects}
1✔
446
            if obj_ids != new_obj_ids:
1✔
447
                setattr(obj, relationship_field, related_objects)
1✔
448
                updated = True
1✔
449

450
        else:
451
            related_object = None
1✔
452

453
            if json_data["data"] is not None:
1✔
454
                related_object = self.get_related_object(related_model, related_id_field, json_data["data"])
1✔
455

456
            obj_id = getattr(getattr(obj, relationship_field), related_id_field, None)
1✔
457
            new_obj_id = getattr(related_object, related_id_field, None)
1✔
458
            if obj_id != new_obj_id:
1✔
459
                setattr(obj, relationship_field, related_object)
1✔
460
                updated = True
1✔
461

462
        try:
1✔
463
            self.session.commit()
1✔
464
        except JsonApiException as e:
1✔
465
            self.session.rollback()
×
466
            raise e
×
467
        except Exception as e:
1✔
468
            self.session.rollback()
1✔
469
            raise JsonApiException("Update relationship error: " + str(e))
1✔
470

471
        self.after_update_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
1✔
472

473
        return obj, updated
1✔
474

475
    def delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
1✔
476
        """Delete a relationship
477

478
        :param dict json_data: the request params
479
        :param str relationship_field: the model attribute used for relationship
480
        :param str related_id_field: the identifier field of the related model
481
        :param dict view_kwargs: kwargs from the resource view
482
        """
483
        self.before_delete_relationship(json_data, relationship_field, related_id_field, view_kwargs)
1✔
484

485
        obj = self.get_object(view_kwargs)
1✔
486

487
        if obj is None:
1✔
488
            url_field = getattr(self, "url_field", "id")
×
489
            filter_value = view_kwargs[url_field]
×
490
            raise ObjectNotFound(f"{self.model.__name__}: {filter_value} not found", source={"parameter": url_field})
×
491

492
        if not hasattr(obj, relationship_field):
1✔
493
            raise RelationNotFound(f"{obj.__class__.__name__} has no attribute {relationship_field}")
1✔
494

495
        related_model = getattr(obj.__class__, relationship_field).property.mapper.class_
1✔
496

497
        updated = False
1✔
498

499
        if isinstance(json_data["data"], list):
1✔
500
            obj_ids = {str(getattr(obj__, related_id_field)) for obj__ in getattr(obj, relationship_field)}
1✔
501

502
            for obj_ in json_data["data"]:
1✔
503
                if obj_["id"] in obj_ids:
1✔
504
                    getattr(obj, relationship_field).remove(
1✔
505
                        self.get_related_object(related_model, related_id_field, obj_)
506
                    )
507
                    updated = True
1✔
508
        else:
509
            setattr(obj, relationship_field, None)
1✔
510
            updated = True
1✔
511

512
        try:
1✔
513
            self.session.commit()
1✔
514
        except JsonApiException as e:
1✔
515
            self.session.rollback()
×
516
            raise e
×
517
        except Exception as e:
1✔
518
            self.session.rollback()
1✔
519
            raise JsonApiException("Delete relationship error: " + str(e))
1✔
520

521
        self.after_delete_relationship(obj, updated, json_data, relationship_field, related_id_field, view_kwargs)
1✔
522

523
        return obj, updated
1✔
524

525
    def get_related_object(self, related_model, related_id_field, obj):
1✔
526
        """Get a related object
527

528
        :param Model related_model: an sqlalchemy model
529
        :param str related_id_field: the identifier field of the related model
530
        :param DeclarativeMeta obj: the sqlalchemy object to retrieve related objects from
531
        :return DeclarativeMeta: a related object
532
        """
533
        try:
1✔
534
            related_object = (
1✔
535
                self.session.query(related_model).filter(getattr(related_model, related_id_field) == obj["id"]).one()
536
            )
537
        except NoResultFound:
1✔
538
            raise RelatedObjectNotFound(f"{related_model.__name__}.{related_id_field}: {obj['id']} not found")
1✔
539

540
        return related_object
1✔
541

542
    def apply_relationships(self, data, obj):
1✔
543
        """Apply relationship provided by data to obj
544

545
        :param dict data: data provided by the client
546
        :param DeclarativeMeta obj: the sqlalchemy object to plug relationships to
547
        :return boolean: True if relationship have changed else False
548
        """
549
        relationships_to_apply = []
1✔
550
        relationship_fields = get_relationships(self.resource.schema, model_field=True)
1✔
551
        for key, value in data.items():
1✔
552
            if key in relationship_fields:
1✔
553
                related_model = getattr(obj.__class__, key).property.mapper.class_
1✔
554
                schema_field = get_schema_field(self.resource.schema, key)
1✔
555
                related_id_field = self.resource.schema._declared_fields[schema_field].id_field
1✔
556

557
                if isinstance(value, list):
1✔
558
                    related_objects = []
1✔
559

560
                    for identifier in value:
1✔
561
                        related_object = self.get_related_object(related_model, related_id_field, {"id": identifier})
1✔
562
                        related_objects.append(related_object)
1✔
563

564
                    relationships_to_apply.append({"field": key, "value": related_objects})
1✔
565
                else:
566
                    related_object = None
1✔
567

568
                    if value is not None:
1✔
569
                        related_object = self.get_related_object(related_model, related_id_field, {"id": value})
1✔
570

571
                    relationships_to_apply.append({"field": key, "value": related_object})
1✔
572

573
        for relationship in relationships_to_apply:
1✔
574
            setattr(obj, relationship["field"], relationship["value"])
1✔
575

576
    def apply_nested_fields(self, data, obj):
1✔
577
        nested_fields_to_apply = []
1✔
578
        nested_fields = get_nested_fields(self.resource.schema, model_field=True)
1✔
579
        for key, value in data.items():
1✔
580
            if key in nested_fields:
1✔
581
                nested_field_inspection = inspect(getattr(obj.__class__, key))
1✔
582

583
                if not isinstance(nested_field_inspection, QueryableAttribute):
1✔
584
                    raise InvalidType("Unrecognized nested field type: not a queryable attribute.")
×
585

586
                if isinstance(nested_field_inspection.property, RelationshipProperty):
1✔
587
                    nested_model = getattr(obj.__class__, key).property.mapper.class_
1✔
588

589
                    if isinstance(value, list):
1✔
590
                        nested_objects = []
1✔
591

592
                        for identifier in value:
1✔
593
                            nested_object = nested_model(**identifier)
1✔
594
                            nested_objects.append(nested_object)
1✔
595

596
                        nested_fields_to_apply.append({"field": key, "value": nested_objects})
1✔
597
                    else:
598
                        nested_field = getattr(obj, key)
1✔
599
                        if nested_field:
1✔
600
                            for attribute, new_value in value.items():
1✔
601
                                setattr(nested_field, attribute, new_value)
1✔
602
                        else:
603
                            nested_fields_to_apply.append({"field": key, "value": nested_model(**value)})
1✔
604
                elif isinstance(nested_field_inspection.property, ColumnProperty):
1✔
605
                    nested_fields_to_apply.append({"field": key, "value": value})
1✔
606
                else:
607
                    raise InvalidType("Unrecognized nested field type: not a RelationshipProperty or ColumnProperty.")
×
608

609
        for nested_field in nested_fields_to_apply:
1✔
610
            setattr(obj, nested_field["field"], nested_field["value"])
1✔
611

612
    def filter_query(self, query, filter_info, model):
1✔
613
        """Filter query according to jsonapi 1.0
614

615
        :param Query query: sqlalchemy query to sort
616
        :param filter_info: filter information
617
        :type filter_info: dict or None
618
        :param DeclarativeMeta model: an sqlalchemy model
619
        :return Query: the sorted query
620
        """
621
        if filter_info:
1✔
622
            filters, joins = create_filters(model, filter_info, self.resource)
1✔
623
            for i_join in joins:
1✔
624
                query = query.join(*i_join)
1✔
625
            query = query.filter(*filters)
1✔
626

627
        return query
1✔
628

629
    def sort_query(self, query, sort_info):
1✔
630
        """Sort query according to jsonapi 1.0
631

632
        :param Query query: sqlalchemy query to sort
633
        :param list sort_info: sort information
634
        :return Query: the sorted query
635
        """
636
        if sort_info:
1✔
637
            sorts, joins = create_sorts(self.model, sort_info, self.resource if hasattr(self, "resource") else None)
1✔
638
            for i_join in joins:
1✔
639
                query = query.join(*i_join)
×
640
            for i_sort in sorts:
1✔
641
                query = query.order_by(i_sort)
1✔
642
        return query
1✔
643

644
    def paginate_query(self, query, paginate_info):
1✔
645
        """Paginate query according to jsonapi 1.0
646

647
        :param Query query: sqlalchemy queryset
648
        :param dict paginate_info: pagination information
649
        :return Query: the paginated query
650
        """
651
        if paginate_info.get("size") == 0:
1✔
652
            return query
1✔
653

654
        page_size = paginate_info.get("size")
1✔
655
        query = query.limit(page_size)
1✔
656
        if paginate_info.get("number"):
1✔
657
            query = query.offset((paginate_info["number"] - 1) * page_size)
1✔
658

659
        return query
1✔
660

661
    def eagerload_includes(self, query, qs):
1✔
662
        """Use eagerload feature of sqlalchemy to optimize data retrieval for include querystring parameter
663

664
        :param Query query: sqlalchemy queryset
665
        :param QueryStringManager qs: a querystring manager to retrieve information from url
666
        :return Query: the query with includes eagerloaded
667
        """
668
        for include in qs.include:
1✔
669
            joinload_object = None
1✔
670

671
            if SPLIT_REL in include:
1✔
672
                current_schema = self.resource.schema
1✔
673
                for obj in include.split(SPLIT_REL):
1✔
674
                    try:
1✔
675
                        field = get_model_field(current_schema, obj)
1✔
676
                    except Exception as e:
×
677
                        raise InvalidInclude(str(e))
×
678

679
                    if joinload_object is None:
1✔
680
                        joinload_object = joinedload(field)
1✔
681
                    else:
682
                        joinload_object = joinload_object.joinedload(field)
1✔
683

684
                    related_schema_cls = get_related_schema(current_schema, obj)
1✔
685

686
                    if isinstance(related_schema_cls, SchemaABC):
1✔
687
                        related_schema_cls = related_schema_cls.__class__
1✔
688
                    else:
689
                        related_schema_cls = class_registry.get_class(related_schema_cls)
×
690

691
                    current_schema = related_schema_cls
1✔
692
            else:
693
                try:
1✔
694
                    field = get_model_field(self.resource.schema, include)
1✔
695
                except Exception as e:
1✔
696
                    raise InvalidInclude(str(e))
1✔
697

698
                joinload_object = joinedload(field)
1✔
699

700
            query = query.options(joinload_object)
1✔
701

702
        return query
1✔
703

704
    def retrieve_object_query(self, view_kwargs, filter_field, filter_value):
1✔
705
        """Build query to retrieve object
706

707
        :param dict view_kwargs: kwargs from the resource view
708
        :params sqlalchemy_field filter_field: the field to filter on
709
        :params filter_value: the value to filter with
710
        :return sqlalchemy query: a query from sqlalchemy
711
        """
712
        return self.session.query(self.model).filter(filter_field == filter_value)
1✔
713

714
    def query(self, view_kwargs):
1✔
715
        """Construct the base query to retrieve wanted data
716

717
        :param dict view_kwargs: kwargs from the resource view
718
        """
719
        return self.session.query(self.model)
1✔
720

721
    def before_create_object(self, data, view_kwargs):
1✔
722
        """Provide additional data before object creation
723

724
        :param dict data: the data validated by marshmallow
725
        :param dict view_kwargs: kwargs from the resource view
726
        """
727
        pass
1✔
728

729
    def after_create_object(self, obj, data, view_kwargs):
1✔
730
        """Provide additional data after object creation
731

732
        :param obj: an object from data layer
733
        :param dict data: the data validated by marshmallow
734
        :param dict view_kwargs: kwargs from the resource view
735
        """
736
        pass
1✔
737

738
    def before_get_object(self, view_kwargs):
1✔
739
        """Make work before to retrieve an object
740

741
        :param dict view_kwargs: kwargs from the resource view
742
        """
743
        pass
1✔
744

745
    def after_get_object(self, obj, view_kwargs):
1✔
746
        """Make work after to retrieve an object
747

748
        :param obj: an object from data layer
749
        :param dict view_kwargs: kwargs from the resource view
750
        """
751
        pass
1✔
752

753
    def before_get_collection(self, qs, view_kwargs):
1✔
754
        """Make work before to retrieve a collection of objects
755

756
        :param QueryStringManager qs: a querystring manager to retrieve information from url
757
        :param dict view_kwargs: kwargs from the resource view
758
        """
759
        pass
1✔
760

761
    def after_get_collection(self, collection, qs, view_kwargs):
1✔
762
        """Make work after to retrieve a collection of objects
763

764
        :param iterable collection: the collection of objects
765
        :param QueryStringManager qs: a querystring manager to retrieve information from url
766
        :param dict view_kwargs: kwargs from the resource view
767
        """
768
        return collection
1✔
769

770
    def before_update_object(self, obj, data, view_kwargs):
1✔
771
        """Make checks or provide additional data before update object
772

773
        :param obj: an object from data layer
774
        :param dict data: the data validated by marshmallow
775
        :param dict view_kwargs: kwargs from the resource view
776
        """
777
        pass
1✔
778

779
    def after_update_object(self, obj, data, view_kwargs):
1✔
780
        """Make work after update object
781

782
        :param obj: an object from data layer
783
        :param dict data: the data validated by marshmallow
784
        :param dict view_kwargs: kwargs from the resource view
785
        """
786
        pass
1✔
787

788
    def before_delete_object(self, obj, view_kwargs):
1✔
789
        """Make checks before delete object
790

791
        :param obj: an object from data layer
792
        :param dict view_kwargs: kwargs from the resource view
793
        """
794
        pass
1✔
795

796
    def after_delete_object(self, obj, view_kwargs):
1✔
797
        """Make work after delete object
798

799
        :param obj: an object from data layer
800
        :param dict view_kwargs: kwargs from the resource view
801
        """
802
        pass
1✔
803

804
    def before_create_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
1✔
805
        """Make work before to create a relationship
806

807
        :param dict json_data: the request params
808
        :param str relationship_field: the model attribute used for relationship
809
        :param str related_id_field: the identifier field of the related model
810
        :param dict view_kwargs: kwargs from the resource view
811
        :return boolean: True if relationship have changed else False
812
        """
813
        pass
1✔
814

815
    def after_create_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
1✔
816
        """Make work after to create a relationship
817

818
        :param obj: an object from data layer
819
        :param bool updated: True if object was updated else False
820
        :param dict json_data: the request params
821
        :param str relationship_field: the model attribute used for relationship
822
        :param str related_id_field: the identifier field of the related model
823
        :param dict view_kwargs: kwargs from the resource view
824
        :return boolean: True if relationship have changed else False
825
        """
826
        pass
1✔
827

828
    def before_get_relationship(self, relationship_field, related_type_, related_id_field, view_kwargs):
1✔
829
        """Make work before to get information about a relationship
830

831
        :param str relationship_field: the model attribute used for relationship
832
        :param str related_type_: the related resource type
833
        :param str related_id_field: the identifier field of the related model
834
        :param dict view_kwargs: kwargs from the resource view
835
        :return tuple: the object and related object(s)
836
        """
837
        pass
1✔
838

839
    def after_get_relationship(
1✔
840
            self, obj, related_objects, relationship_field, related_type_, related_id_field, view_kwargs,
841
    ):
842
        """Make work after to get information about a relationship
843

844
        :param obj: an object from data layer
845
        :param iterable related_objects: related objects of the object
846
        :param str relationship_field: the model attribute used for relationship
847
        :param str related_type_: the related resource type
848
        :param str related_id_field: the identifier field of the related model
849
        :param dict view_kwargs: kwargs from the resource view
850
        :return tuple: the object and related object(s)
851
        """
852
        pass
1✔
853

854
    def before_update_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
1✔
855
        """Make work before to update a relationship
856

857
        :param dict json_data: the request params
858
        :param str relationship_field: the model attribute used for relationship
859
        :param str related_id_field: the identifier field of the related model
860
        :param dict view_kwargs: kwargs from the resource view
861
        :return boolean: True if relationship have changed else False
862
        """
863
        pass
1✔
864

865
    def after_update_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
1✔
866
        """Make work after to update a relationship
867

868
        :param obj: an object from data layer
869
        :param bool updated: True if object was updated else False
870
        :param dict json_data: the request params
871
        :param str relationship_field: the model attribute used for relationship
872
        :param str related_id_field: the identifier field of the related model
873
        :param dict view_kwargs: kwargs from the resource view
874
        :return boolean: True if relationship have changed else False
875
        """
876
        pass
1✔
877

878
    def before_delete_relationship(self, json_data, relationship_field, related_id_field, view_kwargs):
1✔
879
        """Make work before to delete a relationship
880

881
        :param dict json_data: the request params
882
        :param str relationship_field: the model attribute used for relationship
883
        :param str related_id_field: the identifier field of the related model
884
        :param dict view_kwargs: kwargs from the resource view
885
        """
886
        pass
1✔
887

888
    def after_delete_relationship(self, obj, updated, json_data, relationship_field, related_id_field, view_kwargs):
1✔
889
        """Make work after to delete a relationship
890

891
        :param obj: an object from data layer
892
        :param bool updated: True if object was updated else False
893
        :param dict json_data: the request params
894
        :param str relationship_field: the model attribute used for relationship
895
        :param str related_id_field: the identifier field of the related model
896
        :param dict view_kwargs: kwargs from the resource view
897
        """
898
        pass
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