Spaces:
Runtime error
Runtime error
#!/usr/bin/python | |
# -*- coding: utf-8 -*- | |
# Hive Appier Framework | |
# Copyright (c) 2008-2024 Hive Solutions Lda. | |
# | |
# This file is part of Hive Appier Framework. | |
# | |
# Hive Appier Framework is free software: you can redistribute it and/or modify | |
# it under the terms of the Apache License as published by the Apache | |
# Foundation, either version 2.0 of the License, or (at your option) any | |
# later version. | |
# | |
# Hive Appier Framework is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
# Apache License for more details. | |
# | |
# You should have received a copy of the Apache License along with | |
# Hive Appier Framework. If not, see <http://www.apache.org/licenses/>. | |
__author__ = "João Magalhães <joamag@hive.pt>" | |
""" The author(s) of the module """ | |
__copyright__ = "Copyright (c) 2008-2024 Hive Solutions Lda." | |
""" The copyright for the module """ | |
__license__ = "Apache License, Version 2.0" | |
""" The license for the module """ | |
import copy | |
from . import common | |
from . import legacy | |
from . import exceptions | |
class ModelAsync(object): | |
async def get_a(cls, *args, **kwargs): | |
( | |
fields, | |
eager, | |
eager_l, | |
map, | |
rules, | |
meta, | |
build, | |
fill, | |
resolve_a, | |
skip, | |
limit, | |
sort, | |
raise_e, | |
) = cls._get_attrs( | |
kwargs, | |
( | |
("fields", None), | |
("eager", None), | |
("eager_l", None), | |
("map", False), | |
("rules", True), | |
("meta", False), | |
("build", True), | |
("fill", True), | |
("resolve_a", None), | |
("skip", 0), | |
("limit", 0), | |
("sort", None), | |
("raise_e", True), | |
), | |
) | |
# in case there's a sort field and the safe search mode is enabled | |
# we must add sorting by the `_id` field so that the retrieval is | |
# considered to be deterministic, otherwise some DB implementations | |
# will not respect the same sorting sequence across different calls | |
if sort and (skip or limit): | |
if not isinstance(sort, list): | |
sort = list(sort) | |
sort.append(["_id", 1]) | |
if eager_l == None: | |
eager_l = map | |
if resolve_a == None: | |
resolve_a = map | |
if eager_l: | |
eager = cls._eager_b(eager) | |
fields = cls._sniff(fields, rules=rules) | |
collection = cls._collection_a() | |
model = await collection.find_one( | |
kwargs, fields, skip=skip, limit=limit, sort=sort | |
) | |
if not model and raise_e: | |
is_devel = common.is_devel() | |
if is_devel: | |
message = "%s not found for %s" % (cls.__name__, str(kwargs)) | |
else: | |
message = "%s not found" % cls.__name__ | |
raise exceptions.NotFoundError(message=message) | |
if not model and not raise_e: | |
return model | |
cls.types(model) | |
if fill: | |
cls.fill(model, safe=True) | |
if build: | |
cls.build(model, map=map, rules=rules, meta=meta) | |
if eager: | |
model = cls._eager(model, eager, map=map) | |
if resolve_a: | |
model = cls._resolve_all(model, resolve=False) | |
return model if map else cls.old(model=model, safe=False) | |
async def find_a(cls, *args, **kwargs): | |
( | |
fields, | |
eager, | |
eager_l, | |
map, | |
rules, | |
meta, | |
build, | |
fill, | |
resolve_a, | |
skip, | |
limit, | |
sort, | |
raise_e, | |
) = cls._get_attrs( | |
kwargs, | |
( | |
("fields", None), | |
("eager", None), | |
("eager_l", False), | |
("map", False), | |
("rules", True), | |
("meta", False), | |
("build", True), | |
("fill", True), | |
("resolve_a", None), | |
("skip", 0), | |
("limit", 0), | |
("sort", None), | |
("raise_e", False), | |
), | |
) | |
# in case there's a sort field and the safe search mode is enabled | |
# we must add sorting by the `_id` field so that the search is | |
# considered to be deterministic, otherwise some DB implementations | |
# will not respect the same sorting sequence across different calls | |
if sort and (skip or limit): | |
if not isinstance(sort, list): | |
sort = list(sort) | |
sort.append(["_id", 1]) | |
if resolve_a == None: | |
resolve_a = map | |
if eager_l: | |
eager = cls._eager_b(eager) | |
cls._find_s(kwargs) | |
cls._find_d(kwargs) | |
fields = cls._sniff(fields, rules=rules) | |
collection = cls._collection_a() | |
models = collection.find(kwargs, fields, skip=skip, limit=limit, sort=sort) | |
if not models and raise_e: | |
is_devel = common.is_devel() | |
if is_devel: | |
message = "%s not found for %s" % (cls.__name__, str(kwargs)) | |
else: | |
message = "%s not found" % cls.__name__ | |
raise exceptions.NotFoundError(message=message) | |
models = [cls.types(model) async for model in models] | |
if fill: | |
models = [cls.fill(model, safe=True) for model in models] | |
if build: | |
[cls.build(model, map=map, rules=rules, meta=meta) for model in models] | |
if eager: | |
models = cls._eager(models, eager, map=map) | |
if resolve_a: | |
models = [cls._resolve_all(model, resolve=False) for model in models] | |
models = ( | |
models if map else [cls.old(model=model, safe=False) for model in models] | |
) | |
return models | |
async def _increment_a(cls, name): | |
_name = cls._name() + ":" + name | |
store = cls._collection_a(name="counters") | |
value = await store.find_and_modify( | |
{"_id": _name}, {"$inc": {"seq": 1}}, new=True, upsert=True | |
) | |
value = value or await store.find_one({"_id": _name}) | |
return value["seq"] | |
async def save_a( | |
self, | |
validate=True, | |
verify=True, | |
is_new=None, | |
increment_a=None, | |
immutables_a=None, | |
pre_validate=True, | |
pre_save=True, | |
pre_create=True, | |
pre_update=True, | |
post_validate=True, | |
post_save=True, | |
post_create=True, | |
post_update=True, | |
before_callbacks=[], | |
after_callbacks=[], | |
): | |
# ensures that the current instance is associated with | |
# a concrete model, ready to be persisted in database | |
if verify: | |
self.assert_is_concrete() | |
# checks if the instance to be saved is a new instance | |
# or if this is an update operation and then determines | |
# series of default values taking that into account | |
if is_new == None: | |
is_new = self.is_new() | |
if increment_a == None: | |
increment_a = is_new | |
if immutables_a == None: | |
immutables_a = not is_new | |
# runs the validation process in the current model, this | |
# should ensure that the model is ready to be saved in the | |
# data source, without corruption of it, only run this process | |
# in case the validate flag is correctly set | |
validate and self._validate( | |
pre_validate=pre_validate, post_validate=post_validate | |
) | |
# calls the complete set of event handlers for the current | |
# save operation, this should trigger changes in the model | |
if pre_save: | |
self.pre_save() | |
if pre_create and is_new: | |
self.pre_create() | |
if pre_update and not is_new: | |
self.pre_update() | |
# filters the values that are present in the current model | |
# so that only the valid ones are stored in, invalid values | |
# are going to be removed, note that if the operation is an | |
# update operation and the "immutable rules" also apply, the | |
# returned value is normalized meaning that for instance if | |
# any relation is loaded the reference value is returned instead | |
# of the loaded relation values (required for persistence) | |
model = await self._filter_a( | |
increment_a=increment_a, immutables_a=immutables_a, normalize=True | |
) | |
# in case the current model is not new must create a new | |
# model instance and remove the main identifier from it | |
if not is_new: | |
_model = copy.copy(model) | |
del _model["_id"] | |
# calls the complete set of callbacks that should be called | |
# before the concrete data store save operation | |
for callback in before_callbacks: | |
callback(self, model) | |
# retrieves the reference to the store object to be used and | |
# uses it to store the current model data | |
store = self._get_store_a() | |
if is_new: | |
await store.insert(model) | |
self.apply(model, safe_a=False) | |
else: | |
await store.update({"_id": model["_id"]}, {"$set": _model}) | |
# calls the complete set of callbacks that should be called | |
# after the concrete data store save operation | |
for callback in after_callbacks: | |
callback(self, model) | |
# calls the post save event handlers in order to be able to | |
# execute appropriate post operations | |
if post_save: | |
self.post_save() | |
if post_create and is_new: | |
self.post_create() | |
if post_update and not is_new: | |
self.post_update() | |
# returns the instance that has just been used for the save | |
# operation, this may be used for chaining operations | |
return self | |
async def delete_a( | |
self, | |
verify=True, | |
pre_delete=True, | |
post_delete=True, | |
before_callbacks=[], | |
after_callbacks=[], | |
): | |
# ensures that the current instance is associated with | |
# a concrete model, ready to be persisted in database | |
if verify: | |
self.assert_is_concrete() | |
# calls the complete set of event handlers for the current | |
# delete operation, this should trigger changes in the model | |
if pre_delete: | |
self.pre_delete() | |
# calls the complete set of callbacks that should be called | |
# before the concrete data store delete operation | |
for callback in before_callbacks: | |
callback(self) | |
# retrieves the reference to the store object to be able to | |
# execute the removal command for the current model | |
store = self._get_store_a() | |
await store.remove({"_id": self._id}) | |
# calls the underlying delete handler that may be used to extend | |
# the default delete functionality | |
self._delete() | |
# calls the complete set of callbacks that should be called | |
# after the concrete data store delete operation | |
for callback in after_callbacks: | |
callback(self) | |
# calls the complete set of event handlers for the current | |
# delete operation, this should trigger changes in the model | |
if post_delete: | |
self.post_delete() | |
async def reload_a(self, *args, **kwargs): | |
is_new = self.is_new() | |
if is_new: | |
raise exceptions.OperationalError( | |
message="Can't reload a new model entity", code=412 | |
) | |
cls = self.__class__ | |
return await cls.get_a(_id=self._id, *args, **kwargs) | |
async def _filter_a( | |
self, | |
increment_a=True, | |
immutables_a=False, | |
normalize=False, | |
resolve=False, | |
all=False, | |
evaluator="json_v", | |
): | |
# creates the model that will hold the "filtered" model | |
# with all the items that conform with the class specification | |
model = {} | |
# retrieves the class associated with the current instance | |
# to be able to retrieve the correct definition methods | |
cls = self.__class__ | |
# retrieves the (schema) definition for the current model | |
# to be "filtered" it's going to be used to retrieve the | |
# various definitions for the model fields | |
definition = cls.definition() | |
# retrieves the complete list of fields that are meant to be | |
# automatically incremented for every save operation | |
increments = cls.increments() | |
# gather the set of elements that are considered immutables and | |
# that are not meant to be changed if the current operation to | |
# apply the filter is not a new operation (update operation) | |
immutables = cls.immutables() | |
# iterates over all the increment fields and increments their | |
# fields so that a new value is set on the model, note that if | |
# the increment apply is unset the increment operation is ignored | |
for name in increments: | |
if not increment_a: | |
continue | |
if name in self.model: | |
model[name] = cls._ensure_min(name, self.model[name]) | |
else: | |
model[name] = await cls._increment_a(name) | |
# iterates over all the model items to filter the ones | |
# that are not valid for the current class context | |
for name, value in legacy.eager(self.model.items()): | |
if not name in definition: | |
continue | |
if immutables_a and name in immutables: | |
continue | |
value = self._evaluate(name, value, evaluator=evaluator) | |
model[name] = value | |
# in case the normalize flag is set must iterate over all | |
# items to try to normalize the values by calling the reference | |
# value this will returns the reference index value instead of | |
# the normal value that would prevent normalization | |
if normalize: | |
for name, value in legacy.eager(self.model.items()): | |
if not name in definition: | |
continue | |
if not hasattr(value, "ref_v"): | |
continue | |
model[name] = value.ref_v() | |
# in case the resolution flag is set, it means that a recursive | |
# approach must be performed for the resolution of values that | |
# implement the map value (recursive resolution) method, this is | |
# a complex (and possible computational expensive) process that | |
# may imply access to the base data source | |
if resolve: | |
for name, value in legacy.eager(self.model.items()): | |
if not name in definition: | |
continue | |
model[name] = cls._resolve(name, value) | |
# in case the all flag is set the extra fields (not present | |
# in definition) must also be used to populate the resulting | |
# (filtered) map so that it contains the complete set of values | |
# present in the base map of the current instance | |
if all: | |
for name, value in legacy.eager(self.model.items()): | |
if name in model: | |
continue | |
model[name] = value | |
# returns the model containing the "filtered" items resulting | |
# from the validation of the items against the model class | |
return model | |