Source code for atomdb.nosql

"""
Copyright (c) 2018-2022, CodeLV.

Distributed under the terms of the MIT License.

The full license is in the file LICENSE.text, distributed with this software.

Created on Jun 12, 2018
"""
import weakref

import bson
from atom.api import Atom, Dict, Instance, Typed, Value

from .base import JSONSerializer, Model, ModelManager, ModelSerializer, find_subclasses


[docs]class NoSQLModelSerializer(ModelSerializer): """Handles serializing and deserializing of Model subclasses. It will automatically save and restore references where present. """
[docs] async def get_or_create(self, cls, state, scope): """Restore an object from the database. If the object is cached, use that instead. """ # Check if this is in the cache pk = state.get("_id") cache = cls.objects.cache if pk is not None: obj = cache.get(pk) else: obj = None if obj is None: # Create and cache it obj = cls.__new__(cls) if pk is not None: cache[pk] = obj # This ideally should only be done if created return (obj, True) return (obj, False)
[docs] async def get_object_state(self, obj, state, scope): ModelType = obj.__class__ return await ModelType.objects.find_one({"_id": state["_id"]})
[docs] def flatten_object(self, obj, scope): ref = obj.__ref__ if ref in scope: return {"__ref__": ref, "__model__": obj.__model__} scope[ref] = obj state = obj.__getstate__(scope) _id = state.get("_id") if _id is None: return state return {"_id": _id, "__ref__": ref, "__model__": obj.__model__}
def _default_registry(self): """Add all nosql and json models to the registry""" registry = JSONSerializer.instance().registry.copy() registry.update({m.__model__: m for m in find_subclasses(NoSQLModel)}) return registry
[docs]class NoSQLDatabaseProxy(Atom): """A proxy to the collection which holds a cache of model objects.""" #: Object cache cache = Typed(weakref.WeakValueDictionary, ()) #: Database handle table = Value() def __getattr__(self, name): return getattr(self.table, name)
[docs]class NoSQLModelManager(ModelManager): """A descriptor so you can use this somewhat like Django's models. Assuming your using motor or txmongo. Examples -------- MyModel.objects.find_one({'_id':'someid}) """ #: Table proxy cache proxies = Dict() def __get__(self, obj, cls=None): """Handle objects from the class that owns the manager""" cls = cls or obj.__class__ if not issubclass(cls, Model): return self # Only return the collection when used from a Model proxy = self.proxies.get(cls) if proxy is None: proxy = self.proxies[cls] = NoSQLDatabaseProxy( table=self.database[cls.__model__] ) return proxy def _default_database(self): raise EnvironmentError( "No database has been set. Use " "NoSQLModelManager.instance().database = <db>" )
[docs]class NoSQLModel(Model): """An atom model that can be serialized and deserialized to and from MongoDB. """ #: ID of this object in the database _id = Instance(bson.ObjectId) # type: ignore #: Handles encoding and decoding serializer = NoSQLModelSerializer.instance() #: Handles database access objects = NoSQLModelManager.instance()
[docs] @classmethod async def restore(cls, state, force=False): """Restore an object from the database. If the object is cached, use that instead. """ pk = state["_id"] if pk: # Check if this is in the cache cache = cls.objects.cache obj = cache.get(pk) else: obj = None # Restore if obj is None: # Create and cache it obj = cls.__new__(cls) if pk: cache[pk] = obj restore = True else: restore = force if restore: await obj.__restorestate__(state) return obj
[docs] async def load(self): """Alias to load this object from the database""" pk = self._id if self.__restored__ or pk is None: return # Already loaded or nothing to load state = await self.objects.find_one({"_id": pk}) if state is not None: await self.__restorestate__(state)
[docs] async def save(self): """Alias to delete this object to the database""" db = self.objects state = self.__getstate__() if self._id is None: r = await db.insert_one(state) self._id = r.inserted_id db.cache[self._id] = self else: r = await db.replace_one({"_id": self._id}, state, upsert=True) self.__restored__ = True return r
[docs] async def delete(self): """Alias to delete this object in the database""" db = self.objects pk = self._id if pk: r = await db.delete_one({"_id": pk}) del db.cache[pk] del self._id return r