redis-limpyd-extensions 2.1
Some extensions for redis-limpyd, a redis orm (sort of) in python.
License: WTFPL
Requires Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*
Keywords: redis,orm

|PyPI Version| |Build Status|

redis-limpyd-extensions
=======================

Some extensions for
`redis-limpyd <https://github.com/limpyd/redis-limpyd>`__
(`redis <http://redis.io>`__ orm (sort of) in python)

Where to find it:

-  Github repository: https://github.com/limpyd/redis-limpyd-extensions
-  Pypi package: https://pypi.python.org/pypi/redis-limpyd-extensions
-  Documentation: http://documentup.com/limpyd/redis-limpyd-extensions

Install:

Python versions 2.7, and 3.5 to 3.8 are supported (CPython and PyPy).

Redis-server versions >= 3 are supported.

Redis-py versions >= 3 are supported.

Redis-limpyd versions >= 2 are supported.

You can still use limpyd-extensions versions < 2 if you need something older than the above requirements.

.. code:: bash

    pip install redis-limpyd-extensions

List of available extensions:

-  Add/remove related on both sides
-  Dynamic fields

Add/remove related on both sides
--------------------------------

Say we have the following related models:

.. code:: python

        class Person(RelatedModel):
            database = main_database
            name = HashableField(indexable=True)

        class Group(Relatedmodel):
            database = main_database
            name = HashableField(indexable=True)
            members = M2MSetField(Person, related_name='membership')

And some data:

.. code:: python

        somebody = Person(name='foobar')
        group_1 = Group(name='group 1')
        group_2 = Group(name='group 2')
        group_3 = Group(name='group 3')

We can add membership the normal way:

.. code:: python

        group_1.members.sadd(somebody)

And retrieving then this way:

.. code:: python

        group_1_members = group_1.members()  # somebody !
        somebody_membership = somebody.membership()  # group_1

But say that we want to put a person in many groups at ones, we can do:

.. code:: python

        group_2.members.sadd(somebody)
        group_3.members.sadd(somebody)

``limpyd_extensions`` provide a way to add/remove relations via the
other side of the relation:

.. code:: python

        somebody.membership.sadd(group2, group3)

To use this, simple import the related fields from
``limpyd_extensions.related`` instead of ``limpyd.contrib.related``:

.. code:: python

    from limpyd_extensions.related import (FKStringField, FKHashableField,
                                           M2MSetField, M2MListField,
                                           M2MSortedSetField)

And use them as usual. (Note that for convenience you can also import
the standard ``RelatedModel`` from there)

The added methods for the reverse side of each related field are:

FKStringField
~~~~~~~~~~~~~

-  ``sadd``, to set the reverse relation as the fk of the arguments:

Having:

.. code:: python

        class Group(RelatedModel):
            parent = FKStringField(self, related_name='children')

The standard:

.. code:: python

        child_group.parent.set(main_group)
        other_child_group.parent.set(main_group)

is the same as the new:

.. code:: python

        main_group.children.sadd(child_group, other_child_group)

-  ``srem`` works the same way as ``sadd`` but for deleting fk:

The standard:

.. code:: python

        child_group.parent.delete(main_group)
        other_child_group.parent.delete(main_group)

is the same as the new:

.. code:: python

        main_group.children.srem(child_group, other_child_group)

FKHashableField
~~~~~~~~~~~~~~~

-  ``sadd``
-  ``srem``

Both work the exact same way as for FKStringField, the only difference
is that ``sadd`` emulates a ``hset``, not a ``set``.

M2MSetField
~~~~~~~~~~~

-  ``sadd``

The standard:

.. code:: python

        group_2.members.sadd(somebody)
        group_3.members.sadd(somebody)

is the same as the new:

.. code:: python

        somebody.membership.sadd(group2, group3)

-  ``srem`` works the same way as ``sadd`` but for removing relations:

The standard:

.. code:: python

        group_2.members.srem(somebody)
        group_3.members.srem(somebody)

is the same as the new:

.. code:: python

        somebody.membership.srem(group2, group3)

M2MListField
~~~~~~~~~~~~

-  ``lpush`` and ``rpush``, that works for ``M2MListField`` like
   ``sadd`` for ``M2MSetField``

If in our Person/Group example ``members`` is a ``M2MListField`` instead
of a ``M2MSetField``,

The standard:

.. code:: python

        group_2.members.rpush(somebody)
        group_3.members.rpush(somebody)

is the same as the new:

.. code:: python

        somebody.membership.rpush(group2, group3)

-  ``lrem`` works the same way as ``rpush`` and ``lpush`` but for
   removing relations:

The standard:

.. code:: python

        group_2.members.lrem(0, somebody)  # 0 for "all occurences"
        group_3.members.lrem(0, somebody)

is the same as the new:

.. code:: python

        somebody.membership.lrem(group2, group3)  # the count is forced to 0

M2MSortedSetField
~~~~~~~~~~~~~~~~~

-  ``zadd`` that works for ``M2MSortedSetField`` like ``sadd`` for
   ``M2MSetField``, but managing scores. Arguments can be set the same
   way as the normal ``zadd`` command.

If in our Person/Group example ``members`` is a ``M2MSortedSetField``
instead of a ``M2MSetField``, using the score to save the date of
membership

The standard:

.. code:: python

        group_2.members.zadd({somebody: sometime})  # sometime, a float, can be a call to time.time()
        group_3.members.zadd({somebody: another_time})

is the same as the new:

.. code:: python

        somebody.membership.zadd({group2: sometime, group3: another_time})

-  ``zrem`` works the same way as ``zadd``, without the score, but for
   removing relations:

The standard:

.. code:: python

        group_2.members.zrem(somebody)
        group_3.members.zrem(somebody)

is the same as the new:

.. code:: python

        somebody.membership.zrem(group2, group3)

Dynamic fields
--------------

Dynamic fields provide a way to add unlimited fields to a model by
defining a (or many) dynamic field, and use it with a dynamic part. ie a
dynamic field name "foo" can be used with as many dynamic parts as you
want to create dynamic variations: "foo\_bar" for the dynamic part
"bar", "foo\_baz" for the dynamic part "baz", and so on.

A simple API to use them, and filter on them, is provided.

To use a dynamic field, your model must inherit from the following
mixin: ``ModelWithDynamicFieldMixin``, found in
``limpyd_extensions.dynamic.model``. It's a mixin, you should use it
with another ``RedisModel`` class. Fields are available as field classes
(``DynamicStringField``, ``DynamicInstanceHashField``,
``DynamicListField``, ``DynamicSetField``, ``DynamicSortedSetField``,
``DynamicHashField``) or as a mixin (``DynamicFieldMixin``) if you want
to adapt an external field. You can find them in
``limpyd_extensions.dynamic.fields``

A short example on how to define a dynamic field on a model:

.. code:: python

    from limpyd.model import RedisModel

    from limpyd_extension.dynamic.model import ModelWithDynamicFieldMixin
    from limpyd_extension.dynamic.fields import DynamicSetField


    class MyModel(ModelWithDynamicFieldMixin, RedisModel):
        foo = DynamicSetField(indexable=True)

As the ``foo`` field is dynamic, you cannot run any command on it, but
only on its dynamic variations. How to do it ?

There is two ways:

-  use the ``get_field`` method of the model:

.. code:: python

    foo_bar = myinstance.get_field('foo_bar')

-  use the ``get_for`` method of the field:

.. code:: python

    foo_bar = myinstance.foo.get_for('bar')

The latter is useful if you have a variable instead of known value:

.. code:: python

    somebar = 'bar'
    foo_bar = myinstance.foo.get_for(somevar)

Note that you can use this shortcut instead of using ``get_for``:

.. code:: python

    foo_bar = myinstance.foo(somevar)

Knowing this, you can do operations on these fields:

.. code:: python

    myinstance.foo(somevar).sadd('one', 'two', 'three')
    myinstance.foo(othervar).sadd('four', 'five')
    myotherinstance.foo(somevar).sadd('three', 'thirty')
    print myinstance.foo(somevar).smembers()
    print myinstance.foo(othervar).smembers()
    print myotherinstance.foo(somevar).smembers()


To know the existing versions in a dynamic_field, you can use ``scan_fields``.

It takes the same argument as the ``sscan`` command of ``SetField`` (from limpyd), because it is applied on the inventory key where all versions are saved.

So if you have some versions:

.. code::python

    myinstance.foo('foo').set('111')
    myinstance.foo('bar').set('222')
    myinstance.foo('baz').set('333')

You can retrieve them all:

.. code::python

    set(myinstance.foo.scan_versions())  # returns {'foo', 'bar', 'baz'}

Or only a part:

.. code::python

    set(myinstance.foo.scan_versions('b*'))  # returns {'bar', 'baz'}


Filtering
~~~~~~~~~

To filter on indexable dynamic fields, there is two ways too:

-  use the classic way, if you now the dynamic part in advance:

.. code:: python

    MyModel.collection(foo_bar='three')

-  use the new ``dynamic_filter`` method:

.. code:: python

    MyModel.collection().dynamic_filter('foo', 'bar', 'three')

Parameters are: the field name, the dynamic part, the value for the
filter and, not show in the previous example, the index suffix to use.

This suffix is default to ''.

But if what you want to do is

.. code:: python

    MyModel.collection(foo_bar__eq='three')

You can use ``dynamic_filter`` this way:

.. code:: python

    MyModel.collection().dynamic_filter('foo', 'bar', 'three', 'eq')  # you can use '__eq' too


The collection manager used with ``ModelWithDynamicFieldMixin`` depends
on ``ExtendedCollectionManager``, so you can chain filters and dynamic
filters on the resulting collection.

Dynamic related fields
~~~~~~~~~~~~~~~~~~~~~~

Dynamic fields also work with related fields, exactly the same way.
There is only two additions:

-  if you pass a model instance in the ``get_for`` method, it will be
   translated to it's pk
-  the first argument of a "related collection" is the dynamic part (can
   also be an instance)

An exemple using dynamic related fields:

.. code:: python

    from limpyd.fields import PKField
    from limpyd_extensions.dynamic.model import ModelWithDynamicFieldMixin
    from limpyd_extensions.dynamic.related import DynamicM2MSetField

    class Tag(MyBaseModel):
        slug = PKField()

    class Person(MyBaseModel):
        name = PKField()

    class Movie(ModelWithDynamicFieldMixin, MyBaseModel):
        name = PKField()
        tags = DynamicM2MSetField(Tag, related_name='movies')

    somebody = Person(name='Somebody')
    matrix = Movie(name='Matrix')
    cool = Tag(name='cool')

    matrix.tags.get_for(somebody).sadd(cool)
    # same as: matrix.tags(somebody).sadd(cool)

    cool_movies_for_somebody = cool.movies(somebody)  # the related collection
    # ['Matrix']

Provided classes
~~~~~~~~~~~~~~~~

Here is the list of modules and classes provided with the
``limpyd_extensions.dynamic`` module:

-  **model**

   -  **mixins**

      -  ``ModelWithDynamicFieldMixin(object)`` - A mixin tu use for
         your model with dynamic fields

-  **collection**

   -  **mixins**

      -  ``CollectionManagerForModelWithDynamicFieldMixin(object)`` - A
         mixin to use if you want to add the ``dynamic_filter`` method
         to your own collection manager

   -  **full classes**

      -  ``CollectionManagerForModelWithDynamicField(CollectionManagerForModelWithDynamicFieldMixin, ExtendedCollectionManager)``
         - A simple class inheriting from our mixin and the manager from
         ``limpyd.contrib.collection``

-  **field**

   -  **mixins**

      -  ``DynamicFieldMixin(object)`` - A mixin within all the stuff
         for dynamic fields is done, to use to add dynamic field support
         to your own fields

   -  **full classes** All fields simply inherits from our mixin and the
      wanted base field, without anymore addition:

      -  ``DynamicStringField(DynamicFieldMixin, StringField)``
      -  ``DynamicInstanceHashField(DynamicFieldMixin, InstanceHashField)``
      -  ``DynamicListField(DynamicFieldMixin, ListField)``
      -  ``DynamicSetField(DynamicFieldMixin, SetField)``
      -  ``DynamicSortedSetField(DynamicFieldMixin, SortedSetField)``
      -  ``DynamicHashField(DynamicFieldMixin, HashField)``

-  **related**

   -  **mixins**

      -  ``DynamicRelatedFieldMixin(DynamicFieldMixin)`` - A mixin
         within all the stuff for dynamic related fields is done, to use
         to add dynamic field support to your own related fields

   -  **full classes**

      -  ``DynamicFKStringField(DynamicRelatedFieldMixin, FKStringField)``
      -  ``DynamicFKInstanceHashField(DynamicRelatedFieldMixin, FKInstanceHashField)``
      -  ``DynamicM2MSetField(DynamicRelatedFieldMixin, M2MSetField)``
      -  ``DynamicM2MListField(DynamicRelatedFieldMixin, M2MListField)``
      -  ``DynamicM2MSortedSetField(DynamicRelatedFieldMixin, M2MSortedSetField)``


.. |PyPI Version| image:: https://img.shields.io/pypi/v/redis-limpyd-extensions.png
   :target: https://pypi.python.org/pypi/redis-limpyd-extensions
.. |Build Status| image:: https://travis-ci.org/limpyd/redis-limpyd-extensions.png?branch=master
   :target: https://travis-ci.org/limpyd/redis-limpyd-extensions