Helpers SQLAlchemy - :class:`sqla_helpers.base_model.BaseModel`
===============================================================
Installation
-------------

.. rubric:: Git

Installation from git

.. code-block:: console

        $> git clone git@github.com:moumoutte/sqla_helpers.git
        $> cd sqla_helpers
        $> sudo python2.7 setup.py install

.. rubric:: Eggs

Installation from pypi `eggs`

.. code-block:: console

        $> sudo pip install sqla_helpers


Getting Started
----------------

The goal of :class:`sqla_helpers.base_model.BaseModel` is to provide syntactic sugar for :mod:`SQLAlchemy`.


:class:`sqla_helpers.base_model.BaseModel` is to use as mixin class. This class inherits from nothing and shouldn't be inherited.
For access method from model, models need to be declared as bellow:

.. code-block:: python

    from somewhere import DeclarativeBase
    from sqla_helpers.base_model import BaseModel

    class MyModel(DeclarativeBase, BaseModel):
        id = ... # Clef primaire , l'identifiant sous forme d'entier
        awesome_attr = ... # Attribut quelconque du modèle
        other_model = relationship('MyOtherModel', backref='mymodel')


    class MyOtherModel(DeclarativeBase, BaseModel):
        id = ... # Clef primaire
        name = ...
        model_id = ... # Clef étrangère sur MyModel


The :class:`DeclarativeBase` class is generated by :func:`declarative_base` function from `SQLAlchemy`.

To avoid mixin uses, :class:`sqla_helpers.base_model.BaseModel` class can be used as `cls` parameter in :func:`declarative_base`
function. 

.. code-block:: python

    from sqlalchemy.ext.declarative import declarative_base
    from sqla_helpers.base_model import BaseModel
    DeclarativeBase = declarative_base(cls=BaseModel)


.. code-block:: python

    class MyModel(DeclarativeBase):
        # ...


:class:`sqla_helpers.base_model.BaseModel` needs to build a session when queries are done.
In order to access a session when needing, the class uses the stored function :attr:`sqla_helpers.base_model.BaseModel.sessionmaker`. 
This function will be called each time a session is needed.
So we need to store a session_maker by calling `sqla_helpers.base_model.BaseModel.register_sessionmaker` method.

.. code-block:: python

    # Application's initialization
    def main():
        # ...
        BaseModel.register_sessionmaker(scoped_session(sessionmaker(bind=engine)))
        # ...

For a global session, you can just give a Session which is not a `callable`

.. code-block:: python

    from somwhere import DBSession

    # Application's initialization
    def main():
        # ...
        BaseModel.register_sessionmaker(DBSession)
        # ...

Registering a session maker can be dangerous. Because, technically, we change dynamically a class method. To prevent errors, an exception , :exc:`sqla_helpers.base_model.SessionMakerExists`, is raised if a session maker is already registered.

But sometimes, perhaps you need to change it while application is running. So, you can force a new record even if a session maker is already registered

.. code-block:: python

    >>> BaseModel.register_sessionmaker(db_session)
    >>> new_db_session = amazing_function()
    >>> BaseModel.register_sessionmaker(new_db_session)
    Traceback (most recent call last):
        File "<stdin>", line 1, in <module>
    SessionMakerExists: A session maker is already registered.
    >>> BaseModel.register_sessionmaker(new_db_session, force=True)
    
Basic use case :

.. code-block:: python

    >>> MyModel.all()
    [<MyModel object at 0x2c19d90>]
    >>> MyModel.get(id=2)
    <MyModel object at 0x2c19d90>
    >>> MyModel.get(id=3)
    *** NoResultFound: No row was found for one()
    >>> MyModel.filter(id=2)
    [<MyModel object at 0x2c19d90>]
    >>> MyModel.filter(id=3)
    []
    >>> MyModel.count(id=2)
    1


* :meth:`sqla_helpers.base_model.BaseModel.all` returns all the database objects
* :meth:`sqla_helpers.base_model.BaseModel.filter` returns a list of matching objects.
* :meth:`sqla_helpers.base_model.BaseModel.get` returns an uniq matching object.
* :meth:`sqla_helpers.base_model.BaseModel.count` returns the number of matching objects.

Querying criterions can be chained with an `&&` (logical and) operator.

.. code-block:: python

    >>> MyOtherModel.filter(name='toto')
    [<MyOtherModel object at 0x2c19d90>, <MyOtherModel object at 0x2e27e08>]
    >>> MyOtherModel.filter(name='toto', id=2)
    [<MyOtherModel object at 0x2c19d90>]


Querying for criterions on relations
------------------------------------

Valid querying criterions for a class are defined by the class attributes.
IE : in case of `MyOtherModel`, criterions can be `id`, `name` and `model_id`.

This is still true for a Sqlachemy relation.

IE: querying all `MyModel` witch `MyOtherModel` have a name 'foo'.

.. code-block:: python

    >>> MyModel.filter(awesome_attr__name='foo')
    [<MyModel object at 0x2c19d90>]


Querying with entire object.

.. code-block:: python

    >>> otherModel = MyOtherModel.get(name='foo')
    >>> MyModel.filter(awesome_attr=otherModel)
    [<MyModel object at 0x2c19d90>]


The `__` separator (double underscore) allows to split between the differents entities.

Quering with relations attributes can be done recursively.
If `MyOtherObject` has an `other_attr` attribute which is in relation with a `MyOtherOtherObject` object.

Querying all `MyModel` with a `MyOtherObject` has `MyOtherOtherObject` has a `name` attribute is 'foo'.

.. code-block:: python

    >>> MyModel.filter(awesome_attr__other_attr__name='foo')
    [<MyModel object at 0x2c19d90>]



Operators
---------

Others operators than equality can be used. Those operators should be written
with the attribute name following '__' (double underscore) and operator's name.

IE: if all `MyModel` with `id` different than 2 are wanted:

.. code-block:: python

    >>> MyModel.filter(id__not=2)
    []

Available operators are:

* 'not': Non-equal,
* 'lt': letter than,
* 'le': letter or equals than,
* 'gt': gretter than,
* 'ge': gretter or equal than,
* 'in': in a list,
* 'like': SQL `LIKE` operator,
* 'ilike': SQL `ILIKE` operator.


More complex querying
--------------------

As the Django way, :mod:`sqla_helpers` provides a :class:`sqla_helpers.logical.Q` object for more complex queries.
The :class:`sqla_helpers.logical.Q` object can use the :mod:`sqla_helpers' syntax.

.. code-block:: python

    >>> from sqla_helpers.logical import Q
    >>> Q(status__name='test')
    <sqla_helpers.logical.Q at 0x2376cd0>


These objects are usable as criterions for query.

:class:`sqla_helpers.base_model.BaseModel`

.. code-block:: python

    >>> Treatment.get(Q(id=2))
    >>> <sqlalchemy_test.models.Treatment at 0x2388690>

The goal of those objects is to allow SQL logical conditions in a python syntax.

If all `Treatment` objects wih an `id` == 2 or a `Status` name == 'KO' are wanted.

.. code-block:: python

    >>>  Treatment.filter(Q(id=2) | Q(status__name='KO'))
    [<sqlalchemy_test.models.Treatment at 0x2388690>, <sqlalchemy_test.models.Treatment at 0x23837d0>]


For getting, all `Treatment` objects  with an `id' attribute different than 2 :

.. code-block:: python

    >>> Treatment.filter(~Q(id=2))
    [<sqlalchemy_test.models.Treatment at 0x2383450>, <sqlalchemy_test.models.Treatment at 0x23837d0>,
      <sqlalchemy_test.models.Treatment at 0x23886d0> ]

Logical operators can be chained :

.. code-block:: python

    >>> Treatment.filter((Q(id=2) | Q(name='toto')) & (Q(name='OK') | ~Q(status__id=3)))
    2013-02-10 16:39:49,485 INFO sqlalchemy.engine.base.Engine SELECT
    treatment.id AS treatment_id, treatment.name AS treatment_name,
    treatment.status_id AS treatment_status_id
    FROM treatment JOIN status ON status.id = treatment.status_id
    WHERE (treatment.id = ? OR treatment.name = ?) AND (treatment.name = ? OR
    status.id != ?)
    2013-02-10 16:39:49,485 INFO sqlalchemy.engine.base.Engine (2, 'toto', 'OK',
    3)
    >>> [<sqlalchemy_test.models.Treatment at 0x2388690>]


JSON
----

Often in web oriented applications, client and server exchange with JSON format.
In order to have easier loading, :mod:`sqla_helpers` provides methods for loading from a regular python dictionary or a SQLAlchemy model object.

The :meth:`sqla_helpers.base_model.BaseModel.dump` method allows a JSON compatible dictionary.

.. code-block:: python

        >>> print json.dumps(t.dump(), indent=4)
        {
            "status": {
                "id": 1,
                "name": "Ok"
            },
            "status_id": 1,
            "id": 1,
            "name": "Great Treatment"
        }


The method `sqla_helpers.base_model.BaseModel.load` can build objects from a dictionary.
The meaning of use a dictionary is to facilitate access to data in JSON or generate JSON from dictionary.

Objects are getting from database if primary key attributes are found on the dictionnary. Otherwise new object
are created.

.. code-block:: python

        >>> t = Treatment.get(id=7)
        >>> t.name
        'YEAH \\o/'
        >>> t.id
        7
        >>> t.status.name
        'Holy status !'
        >>> t.status.id
        7
        >>> t = Treatment.load({'id': 7, 'name': 'hello'})
        >>> t.name, t.id
        ('hello', 7)
        >>> session.commit()
        >>> t.dump()
        {
                'id': 7,
                'name': u'hello',
                'status': {'id': 7, 'name': u'Holy status !'},
                'status_id': 7
        }
        >>> tr = Treatment.load(t.dump())
        >>> tr == t
        True
        >>> tr.status == t.status
        True
        >>> Treatment.load(tr.dump()).dump()
        {
                'id': 7,
                'name': u'hello',
                'status': {'id': 7, 'name': u'Holy status !'},
                'status_id': 7
        }
        >>> tr = Treatment.load({'name': 'new treatment', 'status': {'name': 'new status'}})
        >>> tr.id
        None
        >>> tr.status.id
        None
        >>> session.add(tr)
        >>> session.commit()
        >>> tr.id
        10
        >>> tr.status.id
        8


:class:`sqla_helpers.base_model.BaseModel` class
================================================

.. automodule:: sqla_helpers.base_model
