<commit_before>==================
tastypie-rpc-proxy
==================

.. image:: https://travis-ci.org/nk113/tastiypie-rpc-proxy.png?branch=master
    :alt: Build Status
    :target: http://travis-ci.org/nk113/tastiypie-rpc-proxy

The concept of **tastypie-rpc-proxy**, an etension of `tastypie-queryset-client`_ - many kudos to the author, is to help coding `tastypie`_ based RPC in easy manner. You can handle remote `tastypie`_ resources as if operating over local `django`_ model objects. Now you don't need to code your business logics and unit tests for both central `django`_ models and API client to read the central data from remote boxes separately - in other word you can deploy the same application code for central API and remote client, **rpc-proxy** looks after everything for you. Don't you think it's convenient if you can code like below to control remote object behind `tastypie`_ API?

::

    title_ja = Track.objects.get(item__source_item_id__startswith='t-2').localize('ja').title

As you know, this code also works perfectly with local `django`_ model. The proxy class tries to access remote `tastypie`_ resources if *API_URL* settings is provided, and to read local model resources if it's not. All right, take a look once at how **rpc-proxy** works. The **rpc-proxy** also can be used as a simple tastypie client which has similar interfaces as `django`_ queryset API.

Features enhanced from tastypie-queryset-client
===============================================

* data proxy layer, which enables switching between local model access and RPC depending on *API_URL* settings
* API namespace
* remote API schema and foreing key caching
* remote API foreign key object operation
* supporting custom field type

etc.

Notes
=====

* setting up `django`_ cache backend is strongly recommended to reduce API requests 

Quick Start
===========

``apps/test`` application is good to start with. Following section goes through the application to describe what you can enjoy from **rpc-proxy**. See the ``apps/test`` application code for the implementation in detail. This test application has models that represent common music data scheme - Album, Track metadata and these localisations. The Item model associates them as parent and child relationship.

Define models
-------------

Define `django`_ models as usual. The model methods will be implemented on *proxy* classes later instead of on the models so just define model fields here - ``apps/test/models.py``.

::

    (...)
    META_TYPES = ((0, 'Track',), (1, 'Album',),)

    (...)
    class Item(Model):
        (...)
        meta_type = models.SmallIntegerField(choices=META_TYPES, default=0)
        parents = models.ManyToManyField('self', symmetrical=False, related_name='children', blank=True, null=True)
        source_item_id = models.CharField(max_length=64, unique=True)


    class Album(BasicLocalizable):

        item = models.OneToOneField(Item, primary_key=True)
        title = models.CharField(max_length=255, blank=True, null=False)
        (...)


    class AlbumLocalization(BasicLocalization, MusicLocalization):

        album = models.ForeignKey(Album)
        language_code = models.CharField(max_length=2, choices=getattr(settings, 'LANGUAGES'), blank=False, null=False)
        (...)

Define resources
----------------

Design `tastypie`_ resources carefully. Might need to have various filters, orderings and access controls - ``apps/test/resources.py``.

::

    (...)
    class Item(resources.ModelResource):

        class Meta(resources.SuperuserMeta):

            queryset = models.Item.objects.all()
            resource_name = 'item'
            (...)

        parents = fields.ToManyField('apps.test.resources.Item', 'parents', null=True)
        children = fields.ToManyField('apps.test.resources.Item', 'children', null=True)
        (...)

    class Album(resources.ModelResource):

        class Meta(resources.SuperuserMeta):

            queryset = models.Album.objects.all()
            resource_name = 'album'
            (...)

        item = fields.ForeignKey(Item, 'item')
        (...)


    class AlbumLocalization(resources.ModelResource):

        class Meta(resources.SuperuserMeta):

            queryset = models.AlbumLocalization.objects.all()
            resource_name = 'albumlocalization'
            (...)

        album = fields.ForeignKey(Album, 'album')
        (...)

Configure URLs, separate metadata resources from Item resource to demonstrate namespaces - ``apps/test/urls/url.py``.

::

    (...)
    core_api = Api(api_name='core')
    core_api.register(resources.Item())

    meta_api = Api(api_name='meta')
    meta_api.register(resources.Album())
    meta_api.register(resources.AlbumLocalization()proxy* classes by default. Write business logics usually we write on django models here. Proxies here are implementing some useful methods for localization - ``apps/test/proxies.py``.

::

    (...)
    from apps.test.models import ITEM_TYPES,):
            self.__init_proxy__()

            language_code = language_code if language_code else get_default_language_code()
            localizations = self.localizations.filter(language_code=language_code)

            if len(localizations) < 1:

                class EmptyLocalization(object):

                    def __init__(self, *args, **kwargs):
                        for key in kwargs:
                            setattr(self, key, kwaeta:

            namespace = 'core'

        (...)
        @property
        def meta_type_display(self):
            if 'get_meta_type_display' in dir(self):
                return self.get_meta_type_display()

            return META_TYPES[self.meta_type][1]

        @property
        def metadata(self):
            try:
                meta = getattr(import_module(self.__module__),
                               self.meta_type_display)
            except Exception, e:
                logger.exception(e)
                raise exceptions.ProxyException(_('No metadata model for '
                                                  '%s found.' % self.meta_type_display))

            return meta.objects.get(item=self)


    class Album(Localizable):

        pass


    class AlbumLocalization(Localization):

        pass


Import proxies
--------------

All right, let's call those proxies with the ``manage.py shell``. After loading fixture, import them with no `API_URL` settings like below, then you can see accesses to the local models:

::

    TASTYPIE_RPC_PROXY = {
        'API_NAMESPACE': 'meta',
        'NON_DEFAULT_ID_FOREIGNKEYS': ('item',),
        'SUPERUSER_USERNAME': 'test',
        'SUPERUSER_PASSWORD': '<PASSWORD>',
    }

::

    >>> from apps.test.proxies import *
    >>> a = Album.objects.get(item__source_item_id__startswith='a-1')
    [DEBUG: django.db.backends: execute] (0.001) SELECT "test_album"."ctime", "test_album"."utime", "test_album"."item_id", "test_album"."release_date" FROM "test_album" INNER JOIN "test_item" ON ("test_album"."item_id" = "test_item"."id") WHERE "test_item"."source_item_id" LIKE a-1% ESCAPE '\' ; args=(u'a-1%',)
    >>> a.localize('en').title
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_item"."id", "test_item"."ctime", "test_item"."utime", "test_item"."item_type", "test_item"."meta_type", "test_item"."source_item_id" FROM "test_item" WHERE "test_item"."id" = 1 ; args=(1,)
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_albumlocalization"."id", "test_albumlocalization"."ctime", "test_albumlocalization"."utime", "test_albumlocalization"."language_code", "test_albumlocalization"."title", "test_albumlocalization"."description", "test_albumlocalization"."artist", "test_albumlocalization"."label", "test_albumlocalization"."album_id" FROM "test_albumlocalization" WHERE ("test_albumlocalization"."album_id" = 1  AND "test_albumlocalization"."language_code" = en ); args=(1, 'en')
    u'A Pop Song Collection'
    >>> t_en = a.item.children.get(source_item_id__startswith='t-1').metadata.localize('en')
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_item"."id", "test_item"."ctime", "test_item"."utime", "test_item"."item_type", "test_item"."meta_type", "test_item"."source_item_id" FROM "test_item" INNER JOIN "test_item_parents" ON ("test_item"."id" = "test_item_parents"."from_item_id") WHERE ("test_item_parents"."to_item_id" = 1  AND "test_item"."source_item_id" LIKE t-1% ESCAPE '\' ); args=(1, u't-1%')
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_track"."ctime", "test_track"."utime", "test_track"."item_id", "test_track"."release_date", "test_track"."isrc", "test_track"."length", "test_track"."trial_start_position", "test_track"."trial_duration" FROM "test_track" WHERE "test_track"."item_id" = 2 ; args=(2,)
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_item"."id", "test_item"."ctime", "test_item"."utime", "test_item"."item_type", "test_item"."meta_type", "test_item"."source_item_id" FROM "test_item" WHERE "test_item"."id" = 2 ; args=(2,)
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_tracklocalization"."id", "test_tracklocalization"."ctime", "test_tracklocalization"."utime", "test_tracklocalization"."language_code", "test_tracklocalization"."title", "test_tracklocalization"."description", "test_tracklocalization"."artist", "test_tracklocalization"."label", "test_tracklocalization"."track_id" FROM "test_tracklocalization" WHERE ("test_tracklocalization"."track_id" = 2  AND "test_tracklocalization"."language_code" = en ); args=(2, 'en')
    >>> t_en.title
    u'A Pop Song 1'
    >>> t_en.title = 'A Pop Song 1 revised title'
    >>> t_en.save()
    [DEBUG: django.db.backends: execute] (0.000) SELECT (1) AS "a" FROM "test_tracklocalization" WHERE "test_tracklocalization"."id" = 1  LIMIT 1; args=(1,)
    [DEBUG: django.db.backends: execute] (0.000) UPDATE "test_tracklocalization" SET "ctime" = 2013-06-14 02:04:20, "utime" = 2013-07-27 00:47:35.058121, "language_code" = en, "title" = A Pop Song 1 revised title, "description" = Description for the Pop Song 1., "artist" = Test, "label" = Label Test, "track_id" = 2 WHERE "test_tracklocalization"."id" = 1 ; args=(u'2013-06-14 02:04:20', u'2013-07-27 00:47:35.058121', u'en', 'A Pop Song 1 revised title', u'Description for the Pop Song 1.', u'Test', u'Label Test', 2, 1)
    >>> t_en.title
    'A Pop Song 1 revised title'

OK then reset database and let's do the same things with `API_URL` settings, you can find that the proxy calls remote `tastypie`_ API this time:

::

    TASTYPIE_RPC_PROXY = {
        'API_NAMESPACE': 'meta',
        'API_URL': 'http://127.0.0.1:8000/api',
        (...)
    }

::

    >>> from apps.test.proxies import *
    (...)
    >>> a = Album.objects.get(item__source_item_id__startswith='a-1')
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/album/?item__source_item_id__startswith=a-1 HTTP/1.1" 200 None
    [DEBUG: rpc_proxy.proxies: to_python] to_python (release_date <date>): '2013-07-26' -> datetime.date(2013, 7, 26)
    >>> a.localize('en').title
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/albumlocalization/?album=1 HTTP/1.1" 200 None
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/albumlocalization/?id__in=1&id__in=2&language_code=en HTTP/1.1" 200 None
    'A Pop Song Collection'
    >>> t_en = a.item.children.get(source_item_id__startswith='t-1').metadata.localize('en')
    [DEBUG: rpc_proxy.proxies: __getattr__] item: /api/v1/core/item/1/, need namespace schema (http://127.0.0.1:8000/api/v1/core/)
    (...)
    [DEBUG: rpc_proxy.proxies: _response] getting cache... (/api/v1/core/item/1/)
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/core/item/1/ HTTP/1.1" 200 None
    [DEBUG: rpc_proxy.proxies: _response] setting cache... (/api/v1/core/item/1/ -> {"ctime": "2013-06-13T19:42:56", "source_item_id": "<EMAIL>", "children": ["/api/v1/core/item/2/", "/api/v1/core/item/3/", "/api/v1/core/item/5/"], "item_type": 0, "meta_type": 1, "parents": [], "utime": "2013-06-13T20:02:38", "id": 1, "resource_uri": "/api/v1/core/item/1/"})
    [DEBUG: rpc_proxy.proxies: __getattr__] children: ['/api/v1/core/item/2/', '/api/v1/core/item/3/', '/api/v1/core/item/5/'], need namespace schema (http://127.0.0.1:8000/api/v1/core/)
    (...)
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/core/item/?id__in=2&id__in=3&id__in=5 HTTP/1.1" 200 None
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/core/item/?source_item_id__startswith=t-1&id__in=2&id__in=3&id__in=5 HTTP/1.1" 200 None
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/track/?item=2 HTTP/1.1" 200 None
    [DEBUG: rpc_proxy.proxies: to_python] to_python (release_date <date>): '2013-06-14' -> datetime.date(2013, 6, 14)
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/tracklocalization/?track=2 HTTP/1.1" 200 None
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/tracklocalization/?id__in=1&id__in=2&language_code=en HTTP/1.1" 200 None
    >>> t_en.title
    'A Pop Song 1'
    >>> t_en.title = 'A Pop Song 1 revised title'
    >>> t_en.save()
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "PUT /api/v1/meta/tracklocalization/1/ HTTP/1.1" 204 0
    >>> t_en.title
    'A Pop Song 1 revised title'

That's it! Hope this enpowers you to write clean code and reduce time to code boring redundant stuff!

Testing proxy code
==================

Unit tests for proxy classes can be ran in both local `django`_ model and remote `tastypie`_ API context. If you are to run the unit tests for both contexts separated settings need to be prepared - API context with *API_URL*, local model context with **NO** *API_URL* settings. Please take a look at how the unit tests for ``apps.test`` application works - see ``runtests.py`` and ``tox.ini``.

As a simple tastypie client
===========================

You can also utilize **rpc-proxy** with no proxy definition - call remote tastypie API with queryset interface. In this case you can just only control remote resources with standard CRUD / REST manner `Tastypie`_ supports by default. See `tastypie-queryset-client`_ for detailed usages.

::

    >>> from datetime import datetime
    >>> from rpc_proxy.proxies import *
    >>>
    >>> api = ProxyClient('http://127.0.0.1:8000/api/',
    ...                   version='v1',
    ...                   namespace='meta',
    ...                   auth=('test', 'test',))
    >>> api.proxies
    {'album': queryset_client.client.Model,
     'albumlocalization': queryset_client.client.Model,
     'track': queryset_client.client.Model,
     'tracklocalization': queryset_client.client.Model}
    >>>
    >>> Track = api.track
    >>> track = Track.objects.filter(item__source_item_id__startswith='t-1')[0]
    >>> album = track.item.parents.all()[0].album
    >>> album.release_date = datetime.now().date()
    >>> album.save()
    >>> album.item.children.all()[0].parents.all()[0].album.release_date == datetime.now().date()
    True
    >>> str(album.item.children.all()[0].track) == str(track)
    True

.. note:: You have to uncomment following fields on the Item resource in *apps.test.resources.py* and to clear cache to work above expectedly though.

::

    (...)
    # album = fields.OneToOneField('apps.test.resources.Album', 'album', null=True)
    # track = fields.OneToOneField('apps.test.resources.Track', 'track', null=True)

Namespace and Resource Endpoint
===============================

The final URL of an API resource endpoint consists of:

::

    '%s/%s/%s/%s/' % (API_URL, API_VERSION, API_NAMESPACE, resource_name,)

Proxy Meta class options
========================

abstract
--------

*Boolean*, optional, indicates if the Meta class is abstract class.

api_url
-------

*String*, optional, base url prefix of the root API endpoint, if not given **rpc-proxy** tries to load corresponding django model in local.

auth
----

*Tuple* or *List*, optional, a convination of username and password to access the API e.g. ``(username, password,)``. SUPERUSER_USERNAME and SUPERUSER_PASSWORD settings variables will be applied by default.

auth
----

*Tuple* or *List*, optional, a convination of username and password to access the API e.g. ``(username, password,)``. SUPERUSER_USERNAME and SUPERUSER_PASSWORD settings variables will be applied by default.

client
------

*ProxyClient* class, optional, intended for extending ProcxyClient class, *ProxyClient* class by default.

namespace
---------

*String*, optional, defines namespace of the resource follows to version, *API_NAMESPACE* will be applied if it's not provided.

resource_name
-------------

*String*, optional, defines resource name of the proxy, the name of the proxy class will be applied if not provided e.g. ``'track'``.

version
-------

*String*, optional, defines version of the resource follows to *api_url*, ``'v1'`` will be used if *API_VERSION* is not provided.

Settings
========

**rpc-proxy** accepts following settings variables defined as **TASTYPIE_RPC_PROXY** dictionary in `django`_ settings. The settings look like:

::

    TASTYPIE_RPC_PROXY = {
        'API_URL': 'http://127.0.0.1:8000/api',
        'SUPERUSER_USERNAME': 'test',
        'SUPERUSER_PASSWORD': '<PASSWORD>',
        (...)
    }


API_NAMESPACE
-------------

*String*, optional, specifies default remote API namespace follows to the version section e.g. ``'core/content'``.

API_URL
-------

String, optional, defines default base prefix URL of remote tastypie API, **rpc-proxy** loads local models as proxy class if this is not specified e.g. ``'https://example.com/django/app/api'``.

.. note:: This value could technically be updated dynamically but it does not take any effect until the application is reloaded.  

API_VERSION
-----------

String, optional, defines default versioning of remote API follows to API_URL e.g. ``'v1'``.

NON_DEFAULT_ID_FOREIGNKEYS
--------------------------

Tuple or List, optional, defines custom primary key field names appear in remote resouces e.g. ``('user',)``.

SUPERUSER_USERNAME
------------------

String, optional, defines default username of superuser for API authentication, useful to allow all operations over all remote resources e.g. ``'test'``.

SUPERUSER_PASSWORD
------------------

String, optional, defines default password of superuser for API authentication, useful to allow all operations over all remote resources e.g. ``'test'``.

.. _tastypie-queryset-client: https://github.com/ikeikeikeike/tastypie-queryset-client
.. _tastypie: https://github.com/toastdriven/django-tastypie
.. _django: https://www.djangoproject.com
<commit_msg>revised README.rst<commit_after>==================
tastypie-rpc-proxy
==================

.. image:: https://travis-ci.org/nk113/tastiypie-rpc-proxy.png?branch=master
    :alt: Build Status
    :target: http://travis-ci.org/nk113/tastiypie-rpc-proxy

The concept of **tastypie-rpc-proxy**, an etension of `tastypie-queryset-client`_ - many kudos to the author, is to help coding `tastypie`_ based RPC in easy manner. You can handle remote `tastypie`_ resources as if operating over local `django`_ model objects. Now you don't need to code your business logics and unit tests for both central `django`_ models and API client to read the central data from remote boxes separately - in other word you can deploy the same application code for central API and remote client, **rpc-proxy** looks after everything for you. Don't you think it's convenient if you can code like below to control remote object behind `tastypie`_ API?

::

    title_ja = Track.objects.get(item__source_item_id__startswith='t-2').localize('ja').title

As you know, this code also works perfectly with local `django`_ model. The proxy class tries to access remote `tastypie`_ resources if *API_URL* settings is provided, and to read local model resources if it's not. All right, take a look once at how **rpc-proxy** works. The **rpc-proxy** also can be used as a simple tastypie client which has similar interfaces as `django`_ queryset API.

Features enhanced from tastypie-queryset-client
===============================================

* data proxy layer, which enables switching between local model access and RPC depending on *API_URL* settings
* API namespace
* remote API schema and foreing key caching
* remote API foreign key object operation
* supporting custom field type

etc.

Notes
=====

* setting up `django`_ cache backend is strongly recommended to reduce API requests 

Quick Start
===========

``apps/test`` application is good to start with. Following section goes through the application to describe what you can enjoy from **rpc-proxy**. See the ``apps/test`` application code for the implementation in detail. This test application has models that represent common music data scheme - Album, Track metadata and these localisations. The Item model associates them as parent and child relationship.

Define models
-------------

Define `django`_ models as usual. The model methods will be implemented on *proxy* classes later instead of on the models so just define model fields here - ``apps/test/models.py``.

::

    (...)
    META_TYPES = ((0, 'Track',), (1, 'Album',),)

    (...)
    class Item(Model):
        (...)
        meta_type = models.SmallIntegerField(choices=META_TYPES, default=0)
        parents = models.ManyToManyField('self', symmetrical=False, related_name='children', blank=True, null=True)
        source_item_id = models.CharField(max_length=64, unique=True)


    class Album(BasicLocalizable):

        item = models.OneToOneField(Item, primary_key=True)
        title = models.CharField(max_length=255, blank=True, null=False)
        (...)


    class AlbumLocalization(BasicLocalization, MusicLocalization):

        album = models.ForeignKey(Album)
        language_code = models.CharField(max_length=2, choices=getattr(settings, 'LANGUAGES'), blank=False, null=False)
        (...)

Define resources
----------------

Design `tastypie`_ resources carefully. Might need to have various filters, orderings and access controls - ``apps/test/resources.py``.

::

    (...)
    class Item(resources.ModelResource):

        class Meta(resources.SuperuserMeta):

            queryset = models.Item.objects.all()
            resource_name = 'item'
            (...)

        parents = fields.ToManyField('apps.test.resources.Item', 'parents', null=True)
        children = fields.ToManyField('apps.test.resources.Item', 'children', null=True)
        (...)

    class Album(resources.ModelResource):

        class Meta(resources.SuperuserMeta):

            queryset = models.Album.objects.all()
            resource_name = 'album'
            (...)

        item = fields.ForeignKey(Item, 'item')
        (...)


    class AlbumLocalization(resources.ModelResource):

        class Meta(resources.SuperuserMeta):

            queryset = models.AlbumLocalization.objects.all()
            resource_name = 'albumlocalization'
            (...)

        album = fields.ForeignKey(Album, 'album')
        (...)

Configure URLs, separate metadata resources from Item resource to demonstrate namespaces - ``apps/test/urls/url.py``.

::

    (...)
    core_api = Api(api_name='core')
    core_api.register(resources.Item())

    meta_api = Api(api_name='meta')
    meta_api.register(resources.Album())
    meta_api.register(resources.AlbumLocalization()proxy* classes by default. Write business logics usually we write on django models here. Proxies here are implementing some useful methods for localization - ``apps/test/proxies.py``.

::

    (...)
    from apps.test.models import ITEM_TYPES,):
            self.__init_proxy__()

            language_code = language_code if language_code else get_default_language_code()
            localizations = self.localizations.filter(language_code=language_code)

            if len(localizations) < 1:

                class EmptyLocalization(object):

                    def __init__(self, *args, **kwargs):
                        for key in kwargs:
                            setattr(self, key, kwaeta:

            namespace = 'core'

        (...)
        @property
        def meta_type_display(self):
            if 'get_meta_type_display' in dir(self):
                return self.get_meta_type_display()

            return META_TYPES[self.meta_type][1]

        @property
        def metadata(self):
            try:
                meta = getattr(import_module(self.__module__),
                               self.meta_type_display)
            except Exception, e:
                logger.exception(e)
                raise exceptions.ProxyException(_('No metadata model for '
                                                  '%s found.' % self.meta_type_display))

            return meta.objects.get(item=self)


    class Album(Localizable):

        pass


    class AlbumLocalization(Localization):

        pass


Import proxies
--------------

All right, let's call those proxies with the ``manage.py shell``. After loading fixture, import them with no `API_URL` settings like below, then you can see accesses to the local models:

::

    TASTYPIE_RPC_PROXY = {
        'API_NAMESPACE': 'meta',
        'NON_DEFAULT_ID_FOREIGNKEYS': ('item',),
        'SUPERUSER_USERNAME': 'test',
        'SUPERUSER_PASSWORD': '<PASSWORD>',
    }

::

    >>> from apps.test.proxies import *
    >>> a = Album.objects.get(item__source_item_id__startswith='a-1')
    [DEBUG: django.db.backends: execute] (0.001) SELECT "test_album"."ctime", "test_album"."utime", "test_album"."item_id", "test_album"."release_date" FROM "test_album" INNER JOIN "test_item" ON ("test_album"."item_id" = "test_item"."id") WHERE "test_item"."source_item_id" LIKE a-1% ESCAPE '\' ; args=(u'a-1%',)
    >>> a.localize('en').title
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_item"."id", "test_item"."ctime", "test_item"."utime", "test_item"."item_type", "test_item"."meta_type", "test_item"."source_item_id" FROM "test_item" WHERE "test_item"."id" = 1 ; args=(1,)
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_albumlocalization"."id", "test_albumlocalization"."ctime", "test_albumlocalization"."utime", "test_albumlocalization"."language_code", "test_albumlocalization"."title", "test_albumlocalization"."description", "test_albumlocalization"."artist", "test_albumlocalization"."label", "test_albumlocalization"."album_id" FROM "test_albumlocalization" WHERE ("test_albumlocalization"."album_id" = 1  AND "test_albumlocalization"."language_code" = en ); args=(1, 'en')
    u'A Pop Song Collection'
    >>> t_en = a.item.children.get(source_item_id__startswith='t-1').metadata.localize('en')
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_item"."id", "test_item"."ctime", "test_item"."utime", "test_item"."item_type", "test_item"."meta_type", "test_item"."source_item_id" FROM "test_item" INNER JOIN "test_item_parents" ON ("test_item"."id" = "test_item_parents"."from_item_id") WHERE ("test_item_parents"."to_item_id" = 1  AND "test_item"."source_item_id" LIKE t-1% ESCAPE '\' ); args=(1, u't-1%')
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_track"."ctime", "test_track"."utime", "test_track"."item_id", "test_track"."release_date", "test_track"."isrc", "test_track"."length", "test_track"."trial_start_position", "test_track"."trial_duration" FROM "test_track" WHERE "test_track"."item_id" = 2 ; args=(2,)
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_item"."id", "test_item"."ctime", "test_item"."utime", "test_item"."item_type", "test_item"."meta_type", "test_item"."source_item_id" FROM "test_item" WHERE "test_item"."id" = 2 ; args=(2,)
    [DEBUG: django.db.backends: execute] (0.000) SELECT "test_tracklocalization"."id", "test_tracklocalization"."ctime", "test_tracklocalization"."utime", "test_tracklocalization"."language_code", "test_tracklocalization"."title", "test_tracklocalization"."description", "test_tracklocalization"."artist", "test_tracklocalization"."label", "test_tracklocalization"."track_id" FROM "test_tracklocalization" WHERE ("test_tracklocalization"."track_id" = 2  AND "test_tracklocalization"."language_code" = en ); args=(2, 'en')
    >>> t_en.title
    u'A Pop Song 1'
    >>> t_en.title = 'A Pop Song 1 revised title'
    >>> t_en.save()
    [DEBUG: django.db.backends: execute] (0.000) SELECT (1) AS "a" FROM "test_tracklocalization" WHERE "test_tracklocalization"."id" = 1  LIMIT 1; args=(1,)
    [DEBUG: django.db.backends: execute] (0.000) UPDATE "test_tracklocalization" SET "ctime" = 2013-06-14 02:04:20, "utime" = 2013-07-27 00:47:35.058121, "language_code" = en, "title" = A Pop Song 1 revised title, "description" = Description for the Pop Song 1., "artist" = Test, "label" = Label Test, "track_id" = 2 WHERE "test_tracklocalization"."id" = 1 ; args=(u'2013-06-14 02:04:20', u'2013-07-27 00:47:35.058121', u'en', 'A Pop Song 1 revised title', u'Description for the Pop Song 1.', u'Test', u'Label Test', 2, 1)
    >>> t_en.title
    'A Pop Song 1 revised title'

OK then reset database and let's do the same things with `API_URL` settings, you can find that the proxy calls remote `tastypie`_ API this time:

::

    TASTYPIE_RPC_PROXY = {
        'API_NAMESPACE': 'meta',
        'API_URL': 'http://1172.16.58.3:8000/api',
        (...)
    }

::

    >>> from apps.test.proxies import *
    (...)
    >>> a = Album.objects.get(item__source_item_id__startswith='a-1')
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/album/?item__source_item_id__startswith=a-1 HTTP/1.1" 200 None
    [DEBUG: rpc_proxy.proxies: to_python] to_python (release_date <date>): '2013-07-26' -> datetime.date(2013, 7, 26)
    >>> a.localize('en').title
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/albumlocalization/?album=1 HTTP/1.1" 200 None
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/albumlocalization/?id__in=1&id__in=2&language_code=en HTTP/1.1" 200 None
    'A Pop Song Collection'
    >>> t_en = a.item.children.get(source_item_id__startswith='t-1').metadata.localize('en')
    [DEBUG: rpc_proxy.proxies: __getattr__] item: /api/v1/core/item/1/, need namespace schema (http://127.0.0.1:8000/api/v1/core/)
    (...)
    [DEBUG: rpc_proxy.proxies: _response] getting cache... (/api/v1/core/item/1/)
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/core/item/1/ HTTP/1.1" 200 None
    [DEBUG: rpc_proxy.proxies: _response] setting cache... (/api/v1/core/item/1/ -> {"ctime": "2013-06-13T19:42:56", "source_item_id": "<EMAIL>", "children": ["/api/v1/core/item/2/", "/api/v1/core/item/3/", "/api/v1/core/item/5/"], "item_type": 0, "meta_type": 1, "parents": [], "utime": "2013-06-13T20:02:38", "id": 1, "resource_uri": "/api/v1/core/item/1/"})
    [DEBUG: rpc_proxy.proxies: __getattr__] children: ['/api/v1/core/item/2/', '/api/v1/core/item/3/', '/api/v1/core/item/5/'], need namespace schema (http://127.0.0.1:8000/api/v1/core/)
    (...)
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/core/item/?id__in=2&id__in=3&id__in=5 HTTP/1.1" 200 None
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/core/item/?source_item_id__startswith=t-1&id__in=2&id__in=3&id__in=5 HTTP/1.1" 200 None
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/track/?item=2 HTTP/1.1" 200 None
    [DEBUG: rpc_proxy.proxies: to_python] to_python (release_date <date>): '2013-06-14' -> datetime.date(2013, 6, 14)
    [INFO: requests.packages.urllib3.connectionpool: _new_conn] Starting new HTTP connection (1): 127.0.0.1
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/tracklocalization/?track=2 HTTP/1.1" 200 None
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "GET /api/v1/meta/tracklocalization/?id__in=1&id__in=2&language_code=en HTTP/1.1" 200 None
    >>> t_en.title
    'A Pop Song 1'
    >>> t_en.title = 'A Pop Song 1 revised title'
    >>> t_en.save()
    [DEBUG: requests.packages.urllib3.connectionpool: _make_request] "PUT /api/v1/meta/tracklocalization/1/ HTTP/1.1" 204 0
    >>> t_en.title
    'A Pop Song 1 revised title'

That's it! Hope this enpowers you to write clean code and reduce time to code boring redundant stuff!

Testing proxy code
==================

Unit tests for proxy classes can be ran in both local `django`_ model and remote `tastypie`_ API context. If you are to run the unit tests for both contexts separated settings need to be prepared - API context with *API_URL*, local model context with **NO** *API_URL* settings. Please take a look at how the unit tests for ``apps.test`` application works - see ``runtests.py`` and ``tox.ini``.

As a simple tastypie client
===========================

You can also utilize **rpc-proxy** with no proxy definition - call remote tastypie API with queryset interface. In this case you can just only control remote resources with standard CRUD / REST manner `Tastypie`_ supports by default. See `tastypie-queryset-client`_ for detailed usages.

::

    >>> from datetime import datetime
    >>> from rpc_proxy.proxies import *
    >>>
    >>> api = ProxyClient('http://127.0.0.1:8000/api/',
    ...                   version='v1',
    ...                   namespace='meta',
    ...                   auth=('test', 'test',))
    >>> api.proxies
    {'album': queryset_client.client.Model,
     'albumlocalization': queryset_client.client.Model,
     'track': queryset_client.client.Model,
     'tracklocalization': queryset_client.client.Model}
    >>>
    >>> Track = api.track
    >>> track = Track.objects.filter(item__source_item_id__startswith='t-1')[0]
    >>> album = track.item.parents.all()[0].album
    >>> album.release_date = datetime.now().date()
    >>> album.save()
    >>> album.item.children.all()[0].parents.all()[0].album.release_date == datetime.now().date()
    True
    >>> str(album.item.children.all()[0].track) == str(track)
    True

.. note:: You have to uncomment following fields on the Item resource in *apps.test.resources.py* and to clear cache to work above expectedly though.

::

    (...)
    # album = fields.OneToOneField('apps.test.resources.Album', 'album', null=True)
    # track = fields.OneToOneField('apps.test.resources.Track', 'track', null=True)

Namespace and Resource Endpoint
===============================

The final URL of an API resource endpoint consists of:

::

    '%s/%s/%s/%s/' % (API_URL, API_VERSION, API_NAMESPACE, resource_name,)

Proxy Meta class options
========================

abstract
--------

*Boolean*, optional, indicates if the Meta class is abstract class.

api_url
-------

*String*, optional, base url prefix of the root API endpoint, if not given **rpc-proxy** tries to load corresponding django model in local.

auth
----

*Tuple* or *List*, optional, a convination of username and password to access the API e.g. ``(username, password,)``. SUPERUSER_USERNAME and SUPERUSER_PASSWORD settings variables will be applied by default.

client
------

*ProxyClient* class, optional, intended for extending ProcxyClient class, *ProxyClient* class by default.

namespace
---------

*String*, optional, defines namespace of the resource follows to version, *API_NAMESPACE* will be applied if it's not provided.

resource_name
-------------

*String*, optional, defines resource name of the proxy, the name of the proxy class will be applied if not provided e.g. ``'track'``.

version
-------

*String*, optional, defines version of the resource follows to *api_url*, ``'v1'`` will be used if *API_VERSION* is not provided.

Settings
========

**rpc-proxy** accepts following settings variables defined as **TASTYPIE_RPC_PROXY** dictionary in `django`_ settings. The settings look like:

::

    TASTYPIE_RPC_PROXY = {
        'API_URL': 'http://127.0.0.1:8000/api',
        'SUPERUSER_USERNAME': 'test',
        'SUPERUSER_PASSWORD': '<PASSWORD>',
        (...)
    }


API_NAMESPACE
-------------

*String*, optional, specifies default remote API namespace follows to the version section e.g. ``'core/content'``.

API_URL
-------

String, optional, defines default base prefix URL of remote tastypie API, **rpc-proxy** loads local models as proxy class if this is not specified e.g. ``'https://example.com/django/app/api'``.

.. note:: This value could technically be updated dynamically but it does not take any effect until the application is reloaded.  

API_VERSION
-----------

String, optional, defines default versioning of remote API follows to API_URL e.g. ``'v1'``.

NON_DEFAULT_ID_FOREIGNKEYS
--------------------------

Tuple or List, optional, defines custom primary key field names appear in remote resouces e.g. ``('user',)``.

SUPERUSER_USERNAME
------------------

String, optional, defines default username of superuser for API authentication, useful to allow all operations over all remote resources e.g. ``'test'``.

SUPERUSER_PASSWORD
------------------

String, optional, defines default password of superuser for API authentication, useful to allow all operations over all remote resources e.g. ``'test'``.

.. _tastypie-queryset-client: https://github.com/ikeikeikeike/tastypie-queryset-client
.. _tastypie: https://github.com/toastdriven/django-tastypie
.. _django: https://www.djangoproject.com

{'fraction_non_alphanumeric': 0.15194419325668518, 'fraction_numerical': 0.02229686087068854, 'mean_word_length': 3.6521634615384615, 'pattern_counts': {'":': 18, '<': 15, '<?xml version=': 0, '>': 193, 'https://': 10, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 20, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1908371', 'n_tokens_mistral': 12717, 'n_tokens_neox': 11884, 'n_words': 3251}