summer.dao — summer 0.7.2 documentation

# Source code for summer.dao

    # Copyright (C) 2009-2019 Martin Slouf <martinslouf@users.sourceforge.net>
    #
    # This file is a part of Summer.
    #
    # Summer is free software; you can redistribute it and/or modify it under
    # the terms of the GNU Lesser General Public License as published by the Free
    # Software Foundation; either version 3 of the License, or (at your option)
    # any later version.
    #
    # This program 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
    # GNU Lesser General Public License for more details.
    #
    # You should have received a copy of the GNU Lesser General Public License
    # along with this program; if not, write to the Free Software
    # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA

    """Provides *DAO* support.

    Base :py:class:`Dao` class provides access to :py:meth:`Dao.session` and
    py:meth:`Dao.connection` properties.

    That means that inside any *DAO* object subclassed from :py:class:`Dao` you
    can access current (thread-bound) *SqlAlchemy* session simply by accessing
    :py:meth:`Dao.session` and direct connection to database by accessing
    :py:meth:`Dao.connection`.

    Much more interesting is :py:class:`EntityDao` class which is inteded to be
    used as a base *DAO* class for your :py:class:`summer.domain.Entity`.
    """

    import logging
    import typing
    import sqlalchemy.engine
    import sqlalchemy.orm.exc

    from .domain import Entity, CodeEntity, Filter
    from .ex import ApplicationException
    from .sf import SessionFactory

    logger = logging.getLogger(__name__)

    class Dao(object):
        """Base *DAO* class.

        Provides safe access to thread bound session through :py:attr:`session`
        attribute.  Alternative is :py:meth:`get_session`.
        """

        def __init__(self, session_factory: SessionFactory):
            """Creates :py:class:`Dao` instance.

            Args:

                session_factory (SessionFactory): session factory to be used
            """
            self.session_factory = session_factory

        @property
        def session(self) -> sqlalchemy.orm.Session:
            """Get current session (bound to current thread).

            Returns:

                Session: current :py:class:`sqlalchemy.orm.Session` instance
            """
            return self.session_factory.sqlalchemy_session

        @property
        def connection(self) -> sqlalchemy.engine.Connection:
            """Get current connection (bound to current thread).

            Use :py:attr:`connection.connection` to obtain
            *Python DB API 2.0 connection* object.

            Returns:

                Connection: current :py:class:`sqlalchemy.engine.Connection` instance
            """
            return self.session_factory.connection

    class EntityDao(Dao):
        """Base *DAO* class for persistent classes subclassed from
        :py:class:`summer.domain.Entity`.

        Provides basic persistent operations.

        Defines another property -- :py:meth:`query` -- access to
        generic *SqlAlchemy* query over :py:meth:`clazz`.
        """

        def __init__(self, session_factory: SessionFactory, clazz: type):
            """Creates :py:class:`EntityDao` instance.

            Args:

                session_factory (SessionFactory): session factory intance to be
                                                  passed to superclass
                                                  (:py:class:`Dao`)

                clazz (type): reference to class type
            """
            Dao.__init__(self, session_factory)
            self._clazz = clazz

        @property
        def clazz(self) -> type:
            """Get class type for this dao.

            Returns:

                type: class for this :py:class:`EntityDao`
            """
            return self._clazz

        @property
        def query(self) -> sqlalchemy.orm.Query:
            """Get new query instance using :py:meth:`clazz`.

            Returns:

                sqlalchemy.orm.Query: query over :py:meth:`clazz`
            """
            return self.session.query(self.clazz)

        def _check_entity(self, entity: Entity):
            """Check if entity is correct.

            Args:

                entity (Entity): entity instance to be checked
            """
            if entity is None:
                raise DaoException("entity == None")
            elif not isinstance(entity, self.clazz):
                msg = "entity is not instance of %s" % self.clazz
                raise DaoException(msg, entity_class=entity.__class__)

        def _get_result_list(self, query: sqlalchemy.orm.Query, query_filter: Filter) -> typing.List[Entity]:
            """Get list of entities with filter offset applied.  Useful for paging.

            Args:

                query (Query): query to be executed

                query_filter (Filter): filter to be used for paging

            Returns:

                list: list of entities using query and paging supplied
            """
            logger.debug("query %s", query)
            logger.debug("query_filter %s", query_filter)
            # apply query_filter limits
            query = query.offset(query_filter.get_offset())
            if query_filter.get_max_results() > 0:
                query = query.limit(query_filter.get_max_results())
            # query
            return query.all()

        def get(self, ident: object) -> Entity:
            """Get entity by :py:attr:`Entity.id` attribute.

            Args:

                ident (object): primary key for :py:class:`Entity`

            Returns:

                Entity: entity instance or raise :py:class:`NoResultFound` if
                        none is found
            """
            return self.query.populate_existing().get(ident)

        def save(self, entity: Entity) -> Entity:
            """Save an entity.

            Args:

                entity (Entity): entity to be persisted

            Returns:

                Entity: persisted instance
            """
            self._check_entity(entity)
            self.session.add(entity)
            return entity

        def merge(self, entity: Entity) -> Entity:
            """Merge an entity.

            Same as :py:meth:`save`, but entity is associated with current session
            if it is not.  For example if entity comes from another session
            (or thread).

            Args:

                entity (Entity): entity to be merged

            Returns:

                Entity: persisted instance
            """
            self._check_entity(entity)
            return self.session.merge(entity)

        def delete(self, entity_or_id: object) -> Entity:
            """Delete an entity.

            Args:

                entity_or_id (object): either :py:class:`Entity` or its primary
                                       key

            Returns:

                Entity: just deleted entity instance
            """
            if isinstance(entity_or_id, self.clazz):
                entity = entity_or_id
            else:
                entity = self.get(entity_or_id)
            self._check_entity(entity)
            self.session.delete(entity)
            return entity

        def find(self, query_filter: Filter) -> typing.List[Entity]:
            """Find collection of entities.

            Args:

                query_filter (Filter): filter with at least paging set

            Returns:

                list: list of entities using query and paging supplied
            """
            return self._get_result_list(self.query, query_filter)

        def get_by_uniq(self, col_name: str, col_value: object) -> Entity:
            """Get entity by its unique attribute value.

            Args:

                col_name (str): attribute column name

                col_value (object): attribute value

            Returns:

                Entity: entity instance or raise :py:class:`NoResultFound` if
                        none is found.
            """
            assert col_name is not None, "col_name == None"
            assert col_value is not None, "col_value == None"
            kwargs = {col_name: col_value}
            return self.query.filter_by(**kwargs).one()

        def get_by_uniq_or_none(self, col_name: str, col_value: object) -> Entity:
            """Get entity by its unique attribute value.

            Args:

                col_name (str): attribute column name

                col_value (object): attribute value

            Returns:

                Entity: entity instance or ``None`` if none is found.
            """
            try:
                tmp = self.get_by_uniq(col_name, col_value)
                return tmp
            except sqlalchemy.orm.exc.NoResultFound:
                logger.exception("no result found for %s having %s == %s",
                                 self.clazz, col_name, col_value)
                return None

    class CodeEntityDao(EntityDao):
        """Base *DAO* class for persistent classes subclassed from
        :py:class:`summer.domain.CodeEntity`.

        """

        def __init__(self, session_factory: SessionFactory, clazz: type):
            """Creates :py:class:`CodeEntityDao` instance.

            Args:

                session_factory (SessionFactory): session factory intance to be
                                                  passed to superclass
                                                  (:py:class:`Dao`)

                clazz (type): reference to class type
            """
            EntityDao.__init__(self, session_factory, clazz)

        def find_map(self, query_filter: Filter) -> typing.Dict[str, CodeEntity]:
            """Loads the objects into a map by :py:attr:`CodeEntity.code` attribute
            used as a map key.

                query_filter (Filter): filter with at least paging set

            Returns:

                dict: dictionary of entities using query and paging supplied
            """
            return {i.code: i for i in self.find(query_filter)}

    class DaoException(ApplicationException):

        def __init__(self, message: str = None, **kwargs):
            ApplicationException.__init__(self, message, **kwargs)