Exemple #1
0
    def _cascade_operation(self, reference, cascading_type):
        entity = reference

        if isinstance(reference, ProxyObject):
            entity = reference._actual

        if not EntityMetadataHelper.hasMetadata(entity):
            return

        entity_meta = EntityMetadataHelper.extract(entity)
        relational_map = entity_meta.relational_map

        for property_name in relational_map:
            guide = relational_map[property_name]

            if guide.inverted_by:
                continue

            actual_data = entity.__getattribute__(property_name)
            reference = self.hydrate_entity(actual_data)

            if not guide.cascading_options or cascading_type not in guide.cascading_options or not reference:
                continue

            if isinstance(reference, list):
                for sub_reference in actual_data:
                    self._forward_operation(self.hydrate_entity(sub_reference), cascading_type, guide.target_class)

                continue

            self._forward_operation(reference, cascading_type, guide.target_class)
Exemple #2
0
    def repository(self, reference):
        """ Retrieve the collection

            :param reference: the entity class or entity metadata of the target repository / collection
            :rtype: tori.db.repository.Repository
        """
        key = None

        if isinstance(reference, EntityMetadata):
            key = reference.collection_name
        elif EntityMetadataHelper.hasMetadata(reference):
            is_registerable_reference = True

            metadata = EntityMetadataHelper.extract(reference)
            key      = metadata.collection_name

            self.register_class(reference)

        if not key:
            raise UnsupportedRepositoryReferenceError('Either a class with metadata or an entity metadata is supported.')

        if key not in self._repository_map:
            repository = Repository(
                session            = self,
                representing_class = reference
            )

            repository.setup_index()

            self._repository_map[key] = repository

        return self._repository_map[key]
Exemple #3
0
    def collection_name(self):
        """ Auto-generated Collection Name

            :rtype: str

            .. note:: This is a read-only property.
        """
        if not self.__collection_name:
            self.__collection_name = self.collection_name_tmpl.format(
                origin      = EntityMetadataHelper.extract(self.origin).collection_name,
                destination = EntityMetadataHelper.extract(self.destination).collection_name
            )

        return self.__collection_name
Exemple #4
0
    def _load_extra_associations(self, record, change_set):
        origin_id = record.entity.id
        relational_map = EntityMetadataHelper.extract(record.entity).relational_map

        for property_name in relational_map:
            if property_name not in change_set:
                continue

            property_change_set = change_set[property_name]
            guide = relational_map[property_name]
            repository = self._em.collection(guide.association_class.cls)

            if property_change_set["action"] == "update":
                for unlinked_destination_id in property_change_set["deleted"]:
                    association = repository.filter_one({"origin": origin_id, "destination": unlinked_destination_id})

                    if not association:
                        continue

                    self._register_deleted(association)

                for new_destination_id in property_change_set["new"]:
                    association = repository.new(origin=origin_id, destination=new_destination_id)

                    self._register_new(association)

                return
            elif property_change_set["action"] == "purge":
                for association in repository.filter({"origin": origin_id}):
                    self._register_deleted(association)

                return

            raise RuntimeError("Unknown changes on external associations for {}".format(origin_id))
Exemple #5
0
    def _sub_query(self, query, alias_to_query_map, iteration):
        is_join_query = True
        alias         = iteration.alias

        if alias not in alias_to_query_map:
            return False

        join_config  = query.join_map[alias]
        joined_type  = join_config['class']
        joined_meta  = EntityMetadataHelper.extract(joined_type)
        native_query = alias_to_query_map[alias]
        local_constrains = {}

        if not iteration.parent_alias:
            is_root    = False
            constrains = self.driver.dialect.get_iterating_constrains(query)

        result_list = self.driver.query(joined_meta, native_query, local_constrains)

        # No result in a sub-query means no result in the main query.
        if not result_list:
            return False

        join_config['result_list'] = result_list

        alias_to_query_map.update(self.driver.dialect.get_alias_to_native_query_map(query))

        return True
Exemple #6
0
    def extra_associations(self, data, stack_depth=0):
        if not isinstance(data, object):
            raise TypeError('The provided data must be an object')

        returnee           = {}
        relational_map     = EntityMetadataHelper.extract(data).relational_map if self._is_entity(data) else {}
        extra_associations = {}

        for name in dir(data):
            # Skip all protected/private/reserved properties.
            if self._is_preserved_property(name):
                continue

            guide = self._retrieve_guide(relational_map, name)

            # Skip all properties without an associative guide or with reverse mapping or without pseudo association class.
            if not guide or guide.inverted_by or not guide.association_class:
                continue

            property_reference = data.__getattribute__(name)

            # Skip all callable properties and non-list properties
            if callable(property_reference) or not isinstance(property_reference, list):
                continue

            # With a valid association class, this property has the many-to-many relationship with the other entity.
            extra_associations[name] = []

            for destination in property_reference:
                extra_associations[name].append(destination.id)

        return extra_associations
Exemple #7
0
def __prevent_duplicated_mapping(cls, property_name):
    if not cls:
        raise ValueError('Expecting a valid type')

    metadata = EntityMetadataHelper.extract(cls)

    if property_name in metadata.relational_map:
        raise DuplicatedRelationalMapping('The property is already mapped.')
Exemple #8
0
    def name(self):
        """ Collection name

            :rtype: str
        """
        metadata = EntityMetadataHelper.extract(self._class)

        return metadata.collection_name
Exemple #9
0
    def _convert_object_id_to_str(self, object_id, entity=None, cls=None):
        class_hash = "generic"

        if not cls and entity:
            cls = entity.__class__

        if cls:
            metadata = EntityMetadataHelper.extract(cls)
            class_hash = metadata.collection_name

        object_key = "{}/{}".format(class_hash, str(object_id))

        return object_key
Exemple #10
0
    def _construct_dependency_graph(self):
        self._dependency_map = {}

        for uid in self._record_map:
            record = self._record_map[uid]

            object_id = self._convert_object_id_to_str(record.entity.id, record.entity)

            current_set = Record.serializer.encode(record.entity)
            extra_association = Record.serializer.extra_associations(record.entity)

            # Register the current entity into the dependency map if it's never
            # been registered or eventually has no dependencies.
            if object_id not in self._dependency_map:
                self._dependency_map[object_id] = DependencyNode(record)

            relational_map = EntityMetadataHelper.extract(record.entity).relational_map

            if not relational_map:
                continue

            # Go through the relational map to establish relationship between dependency nodes.
            for property_name in relational_map:
                guide = relational_map[property_name]

                # Ignore a property from reverse mapping.
                if guide.inverted_by:
                    continue

                # ``data`` can be either an object ID or list.
                data = current_set[property_name]

                if not data:
                    # Ignore anything evaluated as False.
                    continue
                elif not isinstance(data, list):
                    other_uid = self._retrieve_entity_guid_by_id(data, guide.target_class)
                    other_record = self._record_map[other_uid]

                    self._register_dependency(record, other_record)

                    continue

                for dependency_object_id in data:
                    other_uid = self._retrieve_entity_guid_by_id(dependency_object_id, guide.target_class)
                    other_record = self._record_map[other_uid]

                    self._register_dependency(record, other_record)

        return self._dependency_map
Exemple #11
0
    def encode(self, data, stack_depth=0, convert_object_id_to_str=False):
        """ Encode data into dictionary and list.

            :param data:        the data to encode
            :param stack_depth: traversal depth limit
            :param convert_object_id_to_str: flag to convert object ID into string
        """
        if not isinstance(data, object):
            raise TypeError('The provided data must be an object')

        returnee       = {}
        relational_map = EntityMetadataHelper.extract(data).relational_map if self._is_entity(data) else {}

        for name in dir(data):
            # Skip all protected/private/reserved properties.
            if self._is_preserved_property(name):
                continue

            guide = self._retrieve_guide(relational_map, name)

            # Skip all pseudo properties used for reverse mapping.
            if guide and guide.inverted_by:
                continue

            property_reference = data.__getattribute__(name)

            is_list = isinstance(property_reference, list)
            value   = None

            # Skip all callable properties
            if callable(property_reference):
                continue

            # For one-to-many relationship, this property relies on the built-in list type.
            if is_list:
                value = []

                for item in property_reference:
                    value.append(self._process_value(data, item, stack_depth, convert_object_id_to_str))
            else:
                value = self._process_value(data, property_reference, stack_depth, convert_object_id_to_str)

            returnee[name] = value

        # If this is not a pseudo object ID, add the reserved key '_id' with the property 'id' .
        if data.id and not isinstance(data.id, PseudoObjectId):
            returnee['_id'] = self._process_value(data, data, stack_depth, convert_object_id_to_str)

        return returnee
Exemple #12
0
    def _update_join_map(self, origin_metadata, join_map, origin_alias):
        link_map           = origin_metadata.relational_map
        iterating_sequence = []

        # Compute the (local) iterating sequence for updating the join map.
        # Note: this is not the query iterating sequence.
        for alias in join_map:
            join_config = join_map[alias]

            if join_config['class']:
                continue

            parent_alias, property_path = join_config['path'].split('.', 2)

            join_config['alias']         = alias
            join_config['property_path'] = property_path
            join_config['parent_alias']  = parent_alias
            join_config['result_list']   = []

            iterating_sequence.append((join_config, alias, parent_alias, property_path))

        # Update the immediate properties.
        for join_config, current_alias, parent_alias, property_path in iterating_sequence:
            if parent_alias != origin_alias:
                continue

            if property_path not in link_map:
                continue

            mapper = link_map[property_path]

            join_config['class']  = mapper.target_class
            join_config['mapper'] = mapper

        # Update the joined properties.
        for join_config, current_alias, parent_alias, property_path in iterating_sequence:

            if current_alias not in join_map:
                continue

            if not join_map[current_alias]['class']:
                continue

            next_origin_class = join_map[current_alias]['class']
            next_metadata     = EntityMetadataHelper.extract(next_origin_class)
            self._update_join_map(next_metadata, join_map, current_alias)
Exemple #13
0
    def setup_index(self):
        """ Set up index for the entity based on the ``entity`` and ``link`` decorators
        """
        metadata = EntityMetadataHelper.extract(self._class)

        # Apply the relational indexes.
        for field in metadata.relational_map:
            guide = metadata.relational_map[field]

            if guide.inverted_by or guide.association != AssociationType.ONE_TO_ONE:
                continue

            self.index(field)

        # Apply the manual indexes.
        for index in metadata.index_list:
            self.index(index)
Exemple #14
0
    def has_cascading(self):
        if self._has_cascading is not None:
            return self._has_cascading

        self._has_cascading = False
        relational_map      = EntityMetadataHelper.extract(self._class).relational_map

        for property_name in relational_map:
            cascading_options = relational_map[property_name].cascading_options

            if cascading_options \
              and (
                  CascadingType.DELETE in cascading_options \
                  or CascadingType.PERSIST in cascading_options
              ):
                self._has_cascading = True

                break

        return self._has_cascading
Exemple #15
0
    def new(self, **attributes):
        """ Create a new document/entity

            :param attributes: attribute map
            :return: object

            .. note::

                This method deal with data mapping.

        """
        spec = inspect.getargspec(self._class.__init__) # constructor contract
        meta = EntityMetadataHelper.extract(self._class)
        rmap = meta.relational_map # relational map

        # Default missing argument to NULL or LIST
        # todo: respect the default value of the argument
        for argument_name in spec.args:
            if argument_name == 'self' or argument_name in attributes:
                continue

            default_to_list = argument_name in rmap\
                and rmap[argument_name].association in [
                    AssociationType.ONE_TO_MANY,
                    AssociationType.MANY_TO_MANY
                ]

            attributes[argument_name] = [] if default_to_list else None

        attribute_name_list = list(attributes.keys())

        # Remove unwanted arguments/attributes/properties
        for attribute_name in attribute_name_list:
            if argument_name == 'self' or attribute_name in spec.args:
                continue

            del attributes[attribute_name]

        return self._class(**attributes)
Exemple #16
0
    def register_class(self, entity_class):
        """ Register the entity class

            :param entity_class: the class of document/entity
            :type  entity_class: type

            :rtype: tori.db.repository.Repository

            .. note::

                This is for internal operation only. As it seems to be just a
                residual from the prototype stage, the follow-up investigation
                in order to remove the method will be for Tori 3.1.

        """
        key = entity_class

        if isinstance(entity_class, type):
            metadata = EntityMetadataHelper.extract(entity_class)
            key      = metadata.collection_name

        if key not in self._registered_types:
            self._registered_types[key] = entity_class
Exemple #17
0
    def query(self, query):
        metadata = EntityMetadataHelper.extract(query.origin)

        # Deprecated in Tori 3.1; Only for backward compatibility
        if not query.is_new_style:
            return self.driver.query(
                metadata,
                query._condition,
                self.driver.dialect.get_iterating_constrains(query)
            )

        root_class     = query.origin
        expression_set = query.criteria.get_analyzed_version()

        # Register the root entity
        query.join_map[query.alias] = {
            'alias': query.alias,
            'path':  None,
            'class': root_class,
            'parent_alias': None,
            'property_path': None,
            'result_list': []
        }

        self._update_join_map(metadata, query.join_map, query.alias)

        iterating_sequence = self._compute_iterating_sequence(query.join_map)
        alias_to_query_map = self.driver.dialect.get_alias_to_native_query_map(query)

        for alias in query.join_map:
            mapping = query.join_map[alias]

        for iteration in iterating_sequence:
            if not self._sub_query(query, alias_to_query_map, iteration):
                break

        return query.join_map[query.alias]['result_list']
Exemple #18
0
def prepare_entity_class(cls, collection_name=None, indexes=[]):
    """ Create a entity class

    :param cls: the document class
    :type  cls: object
    :param collection_name: the name of the corresponding collection where the
                            default is the lowercase version of the name of the
                            given class (cls)
    :type  collection_name: str

    The object decorated with this decorator will be automatically provided with
    a few additional attributes.

    =================== ======== =================== ==== =================================
    Attribute           Access   Description         Read Write
    =================== ======== =================== ==== =================================
    id                  Instance Document Identifier Yes  Yes, ONLY ``id`` is undefined.
    __t3_orm_meta__     Static   Tori 3's Metadata   Yes  ONLY the property of the metadata
    __session__         Instance DB Session          Yes  Yes, but NOT recommended.
    =================== ======== =================== ==== =================================

    The following attributes might stay around but are deprecated as soon as
    the stable Tori 3.0 is released.

    =================== ======== =================== ==== =================================
    Attribute           Access   Description         Read Write
    =================== ======== =================== ==== =================================
    __collection_name__ Static   Collection Name     Yes  Yes, but NOT recommended.
    __relational_map__  Static   Relational Map      Yes  Yes, but NOT recommended.
    __indexes__         Static   Indexing List       Yes  Yes, but NOT recommended.
    =================== ======== =================== ==== =================================

    ``__session__`` is used to resolve the managing rights in case of using
    multiple sessions simutaneously.

    For example,

    .. code-block:: python

        @entity
        class Note(object):
            def __init__(self, content, title=''):
                self.content = content
                self.title   = title

    where the collection name is automatically defined as "note".

    .. versionchanged:: 3.0

        The way Tori stores metadata objects in ``__collection_name__``,
        ``__relational_map__`` and ``__indexes__`` are now ignored by the ORM
        in favour of ``__t3_orm_meta__`` which is an entity metadata object.

        This change is made to allow easier future development.

    .. tip::

        You can define it as "notes" by replacing ``@entity`` with ``@entity('notes')``.
    """
    if not cls:
        raise ValueError('Expecting a valid type')

    def get_id(self):
        return self.__dict__['_id'] if '_id' in self.__dict__ else None

    def set_id(self, id):
        """
        Define the document ID if the original ID is not defined.

        :param id: the ID of the document.
        """
        if '_id' in self.__dict__ and self.__dict__['_id']\
            and not isinstance(self.__dict__['_id'], PseudoObjectId):
            raise LockedIdException('The ID is already assigned and cannot be changed.')

        self._id = id

    cls.__session__ = None

    EntityMetadataHelper.imprint(
        cls,
        collection_name or cls.__name__.lower(),
        indexes
    )

    cls.id = property(get_id, set_id)

    return cls
Exemple #19
0
 def _is_entity(self, data):
     return EntityMetadataHelper.hasMetadata(data)
Exemple #20
0
def __map_property(cls, property_name, guide):
    metadata = EntityMetadataHelper.extract(cls)

    metadata.relational_map[property_name] = guide
Exemple #21
0
    def apply_relational_map(self, entity):
        """ Wire connections according to the relational map """
        meta = EntityMetadataHelper.extract(entity)
        rmap = meta.relational_map

        for property_name in rmap:
            guide = rmap[property_name]
            """ :type: tori.db.mapper.RelatingGuide """

            # In the reverse mapping, the lazy loading is not possible but so
            # the proxy object is still used.
            if guide.inverted_by:
                target_meta = EntityMetadataHelper.extract(guide.target_class)

                api = self._driver.collection(target_meta.collection_name)

                if guide.association in [AssociationType.ONE_TO_ONE, AssociationType.MANY_TO_ONE]:
                    # Replace with Criteria
                    target = api.find_one({guide.inverted_by: entity.id})

                    entity.__setattr__(property_name, ProxyFactory.make(self, target['_id'], guide))
                elif guide.association == AssociationType.ONE_TO_MANY:
                    # Replace with Criteria
                    proxy_list = [
                        ProxyFactory.make(self, target['_id'], guide)
                        for target in api.find({guide.inverted_by: entity.id})
                    ]

                    entity.__setattr__(property_name, proxy_list)
                elif guide.association == AssociationType.MANY_TO_MANY:
                    entity.__setattr__(property_name, ProxyCollection(self, entity, guide))
                else:
                    raise IntegrityConstraintError('Unknown type of entity association (reverse mapping)')

                return # Done the application

            # In the direct mapping, the lazy loading is applied wherever applicable.
            if guide.association in [AssociationType.ONE_TO_ONE, AssociationType.MANY_TO_ONE]:
                if not entity.__getattribute__(property_name):
                    continue

                entity.__setattr__(
                    property_name,
                    ProxyFactory.make(
                        self,
                        entity.__getattribute__(property_name),
                        guide
                    )
                )
            elif guide.association == AssociationType.ONE_TO_MANY:
                proxy_list = []

                for object_id in entity.__getattribute__(property_name):
                    if not object_id:
                        continue

                    proxy_list.append(ProxyFactory.make(self, object_id, guide))

                entity.__setattr__(property_name, proxy_list)
            elif guide.association == AssociationType.MANY_TO_MANY:
                entity.__setattr__(property_name, ProxyCollection(self, entity, guide))
            else:
                raise IntegrityConstraintError('Unknown type of entity association')