Source code for queryset_client.client

# -*- coding: utf-8 -*-
#from datetime import datetime
import copy
import urlparse
import slumber


__all__ = ["Client"]


class ObjectDoesNotExist(Exception):
    pass


class MultipleObjectsReturned(Exception):
    pass


class FieldTypeError(TypeError):
    pass


def parse_id(resouce_uri):
    """ url parsing

    :param resource_uri:
    :rtype: str
    :return: primary id
    """
    return resouce_uri.split("/")[::-1][1]


class Response(object):
    """ Proxy Model Class """
    def __init__(self, model, response=dict()):
        """

        :param model: The Model
        :param response: response from Client Library
        """
        self.__dict__["_response"] = response
        self.model = model(**response)
        self._schema = self.model.schema()
        self._url = ""

    def _get_res(self):
        return self.__dict__["_response"]

    def _set_res(self, value):
        self.__dict__["_response"] = value

    def _get_response(self):
        return self._res

    def _set_response(self, value):
        self._res = value

    _res = property(_get_res, _set_res)
    _response = property(_get_response, _set_response)

    def __repr__(self):
        return "<{0}: {1} {2}>".format(self.model._model_name, self._url, self._res)

    def __getattr__(self, attr):
        """ return Response Class """
        if not attr in self._response:
            raise AttributeError(attr)
        elif not "related_type" in self._schema["fields"][attr]:
            return self._response[attr]

        related_type = self._schema["fields"][attr]["related_type"]
        if related_type == "to_many":
            return ManyToManyManager(model=self.model,
                            query={"id__in": [parse_id(url) for url in self._response[attr]]})
        elif related_type == "to_one":
            return LazyResponse(model=self.model, model_name=attr, url=self._response[attr])

    def __getitem__(self, item):
        if item in self._response:
            return self._response[item]
        else:
            raise KeyError(item)

    def __contains__(self, attr):
        return attr in self._res

    def __setattr__(self, attr, value):
        if self.__contains__(attr):
            self._res[attr] = value
            self.model.__setattr__(attr, value)
        super(Response, self).__setattr__(attr, value)

    def save(self):
        """ save saved response """
        self.model.save()

    def delete(self):
        """ remove saved response """
        self.model.delete()
        self._response = dict()


class LazyResponse(Response):
    """ convert response model and lazy response """

    def __init__(self, model_name, url, *args, **kwargs):
        super(LazyResponse, self).__init__(*args, **kwargs)
        self.model = self.model._clone(model_name)
        self._client = getattr(self.model._main_client, model_name)
        self._schema = self.model.schema()
        self._url = url

    def _get_response(self):
        if not self._res:
            self._res = self._client(parse_id(self._url)).get()
        return self._res

    def _set_response(self, value):
        self._res = value

    _response = property(_get_response, _set_response)


class QuerySet(object):

    def __init__(self, model, responses=None, **kwargs):
        self.model = model
        self._kwargs = kwargs
        self._responses = responses
        self._meta = responses["meta"] if responses else {"total_count": 0}
        self._objects = dict(enumerate(responses["objects"])) if responses else []
        self._response_class = kwargs.get("response_class", Response)
        self._query = kwargs.get("query", dict())

    def __repr__(self):
        return "<QuerySet {0} ({1}/{2})>".format(
                    self._response_class, self.__len__(), self._meta["total_count"])

    def __len__(self):
        return len(self._objects)

    def __iter__(self):
        if self.__len__() < 1:
            raise StopIteration()
        index = 0
        klass = copy.deepcopy(self)
        while 1:
            try:
                yield klass._wrap_response(klass._objects[index])
                index += 1
            except KeyError:
                klass = klass._next()
                index = 0

    def _clone(self, responses=None, klass=None, **kwargs):
        if klass is None:
            klass = self.__class__
        k = klass(model=self.model, responses=responses)
        k.__dict__.update(kwargs)
        return k

    def _request(self, url):
        return self.model._base_client.request(url)

    def _next(self):
        """ request next page """
        if not self._meta["next"]:
            raise StopIteration()
        return self._clone(self._request(self._meta["next"]))

    def _previous(self):
        """ request previous page """
        if not self._meta["previous"]:
            raise StopIteration()
        return self._clone(self._request(self._meta["previous"]))

    def __getitem__(self, index):
        try:
            if isinstance(index, slice):
                start = index.start
                stop = index.stop
                # step = index.step
                return [self._wrap_response(self._objects[i]) for i in range(start, stop)]
            else:
                return self._wrap_response(self._objects[index])
        except KeyError as err:
            raise IndexError(err)

    def _wrap_response(self, dic):
        return self._response_class(self.model, dic)

    def get_pk(self, pk):
        return self._wrap_response(self.model._client(pk).get())

    def count(self):
        if self._objects:
            return self._meta["total_count"]
        return self.filter()._meta["total_count"]

    def get(self, *args, **kwargs):
        """ create

        :param args: XXX no descript
        :param kwargs: XXX no descript
        :rtype: Response
        :return: Response object.
        """
        clone = self.filter(*args, **kwargs)
        num = len(clone)
        if num > 1:
            raise MultipleObjectsReturned(
                "get() returned more than one {0} -- it returned {1}! Lookup parameters were {2}"
                    .format(self.model._model_name, num, kwargs))
        elif not num:
            raise ObjectDoesNotExist("{0} matching query does not exist."
                    .format(self.model._model_name))
        return clone[0]

    def create(self, **kwargs):
        """ create

        :param kwargs: XXX No Description
        :rtype: Model
        :return: created object.
        """
        obj = self.model(**kwargs)
        obj.save()
        return obj

    def get_or_create(self, **kwargs):
        """

        :param kwargs: field
        :rtype: tuple
        :return: Returns a tuple of (object, created)
        """
        assert kwargs, 'get_or_create() must be passed at least one keyword argument'

        try:
            return self.get(**kwargs), False
        except ObjectDoesNotExist:
            obj = self.model(**kwargs)
            obj.save()
            return obj, True

    def latest(self, field_name=None):
        assert bool(field_name), \
            "latest() requires either a field_name parameter or 'get_latest_by' in the model"
        clone = self._filter(**{"order_by": "-{0}".format(field_name), "limit": 1})
        return clone[0]

    def exists(self):
        return bool(self._responses)

    def all(self):
        return self.filter()

    def filter(self, *args, **kwargs):
        return self._filter(*args, **kwargs)

    def _filter(self, *args, **kwargs):

        # TODO: ↓↓↓ ManyToManyで 一件も relationがない場合の処理, 現状元のQuerySetの結果が返される ↓↓↓↓
        # <QuerySet <class 'queryset_client.client.Response'> (0/0)>

        kwargs_ = dict(self._query.items() + kwargs.items())
        clone = self._clone(self.model._client.get(**kwargs_))
        clone._query.update({
            "id__in": [parse_id(klass.resource_uri) for klass in clone[0:len(clone)]]
        })
        return clone

    def order_by(self, *args, **kwargs):

        # TODO: multiple order_by = "order_by=-body&order_by=id"

        order = {"order_by": args[0]}
        clone = self._filter(*args, **dict(order.items() + kwargs.items()))
        clone._query.update(order)
        return clone


[docs]class Manager(object): def __init__(self, model): self.model = model
[docs] def get_query_set(self): return QuerySet(self.model)
[docs] def all(self): # TODO: return self.get_query_set() return self.get_query_set().all()
[docs] def count(self): return self.get_query_set().count() # def dates(self, *args, **kwargs): # return self.get_query_set().dates(*args, **kwargs) # def distinct(self, *args, **kwargs): # return self.get_query_set().distinct(*args, **kwargs) # def extra(self, *args, **kwargs): # return self.get_query_set().extra(*args, **kwargs)
[docs] def get(self, *args, **kwargs): return self.get_query_set().get(*args, **kwargs)
[docs] def get_or_create(self, **kwargs): return self.get_query_set().get_or_create(**kwargs)
[docs] def create(self, **kwargs): return self.get_query_set().create(**kwargs) # TODO: next implementation # def bulk_create(self, *args, **kwargs): # return self.get_query_set().bulk_create(*args, **kwargs)
[docs] def filter(self, *args, **kwargs): return self.get_query_set().filter(*args, **kwargs) # def aggregate(self, *args, **kwargs): # return self.get_query_set().aggregate(*args, **kwargs) # def annotate(self, *args, **kwargs): # return self.get_query_set().annotate(*args, **kwargs) # def complex_filter(self, *args, **kwargs): # return self.get_query_set().complex_filter(*args, **kwargs) # def exclude(self, *args, **kwargs): # return self.get_query_set().exclude(*args, **kwargs) # def in_bulk(self, *args, **kwargs): # return self.get_query_set().in_bulk(*args, **kwargs) # def iterator(self, *args, **kwargs): # return self.get_query_set().iterator(*args, **kwargs)
[docs] def latest(self, *args, **kwargs): return self.get_query_set().latest(*args, **kwargs)
[docs] def order_by(self, *args, **kwargs): return self.get_query_set().order_by(*args, **kwargs) # def select_for_update(self, *args, **kwargs): # return self.get_query_set().select_for_update(*args, **kwargs) # def select_related(self, *args, **kwargs): # return self.get_query_set().select_related(*args, **kwargs) # def prefetch_related(self, *args, **kwargs): # return self.get_query_set().prefetch_related(*args, **kwargs) # def values(self, *args, **kwargs): # return self.get_query_set().values(*args, **kwargs) # def values_list(self, *args, **kwargs): # return self.get_query_set().values_list(*args, **kwargs) # def update(self, *args, **kwargs): # return self.get_query_set().update(*args, **kwargs) # # def reverse(self, *args, **kwargs): # return self.get_query_set().reverse(*args, **kwargs) # # def defer(self, *args, **kwargs): # return self.get_query_set().defer(*args, **kwargs) # def only(self, *args, **kwargs): # return self.get_query_set().only(*args, **kwargs) # def using(self, *args, **kwargs): # return self.get_query_set().using(*args, **kwargs)
[docs] def exists(self, *args, **kwargs): return self.get_query_set().exists(*args, **kwargs)
[docs]class ManyToManyManager(Manager): def __init__(self, query, **kwargs): super(ManyToManyManager, self).__init__(**kwargs) self._query = query
[docs] def get_query_set(self): return QuerySet(self.model, query=self._query)
[docs]class Model(object):
[docs] def __init__(self, main_client, model_name, endpoint, schema, objects=None, base_client=None): self._client = getattr(main_client, model_name) self._main_client = main_client self._base_client = base_client self._model_name = model_name self._endpoint = endpoint self._schema = schema self._schema_store = self._base_client.schema(model_name) self._base_url = self._main_client._store["base_url"] self._fields = dict() # TODO: set field attribute self._strict_field = True self.objects = objects or Manager(self) # TODO: LazyCall
[docs] def __repr__(self): return "<{0}: {1}{2}>".format(self._model_name, self._endpoint, " " + str(self._fields) if self._fields else "")
[docs] def __call__(self, **kwargs): self._setattrs(**kwargs) # TODO: LazyCall klass = copy.deepcopy(self) self._clear_fields() self._fields = dict() return klass
[docs] def _clear_fields(self, klass=None): c = klass or self for field in c._fields: c.__delattr__(field)
[docs] def _clone(self, model_name, **kwargs): """ create `model_name` model """ s = self._base_client.schema()[model_name] klass = self.__class__(self._main_client, model_name, s["list_endpoint"], s["schema"], base_client=self._base_client) klass.__dict__.update(kwargs) return klass
[docs] def _setattrs(self, **kwargs): for field in kwargs: self.__setattr__(field, kwargs[field]) if not field in self._fields: raise FieldTypeError("'{0}' is an invalid keyword argument for this function" .format(field))
[docs] def __setattr__(self, attr, value): self._setfield(attr, value) super(Model, self).__setattr__(attr, value)
[docs] def _setfield(self, attr, value): if hasattr(self, "_schema_store"): if attr in self._schema_store["fields"]: nullable = self._schema_store["fields"][attr]["nullable"] blank = self._schema_store["fields"][attr]["blank"] field_type = self._schema_store["fields"][attr]["type"] check_type = False value_ = value try: # TODO: type check and convert value. if nullable or blank: check_type = True # value_ = value elif field_type == "string": check_type = isinstance(value, (str, unicode)) # value_ = value elif field_type == "integer": check_type = True # "".isdigit(), isinstance(value, int) # value_ = value elif field_type == "datetime": check_type = True # return isinstance(value, datetime) # value_ = value elif field_type == "related": check_type = True # return isinstance(value, datetime) # value_ = value elif field_type == "boolean": check_type = True # return isinstance(value, datetime) # value_ = value except Exception: check_type = False finally: if check_type is not True and self._strict_field: raise FieldTypeError( "Field Type Error: '{0}' is '{1}' type. ( Input '{2}:{3}' )" .format(attr, field_type, value_, type(value_).__name__)) # set field self._fields[attr] = value_
[docs] def schema(self, *attrs): """ usage :: >>> self.schema("fields") # out fields schema >>> self.schema("fields", "id") # out id schema :param attrs: :rtype: dict :return: model schema """ if attrs: s = self._schema_store for attr in attrs: s = s[attr] return s else: return self._schema_store
[docs] def save(self): if hasattr(self, "id"): self._client(self.id).put(self._fields) # return bool else: self._setattrs(**self._client.post(self._fields))
[docs] def delete(self): assert hasattr(self, "id") is True, "{0} object can't be deleted because its {2} attribute \ is set to None.".format(self._model_name, self._schema_store["fields"]["id"]["type"]) self._client(self.id).delete() self._clear_fields()
class SchemaStore(dict): """ schema cache """ def __setattr__(self, name, value): self[name] = value def __getattr__(self, name): return self[name] def quick_get(self, name, schema): if not self.__contains__(name): self.__setattr__(name, schema()) return self[name] class ClientMeta(type): def __new__(cls, name, bases, attrs): klass = super(ClientMeta, cls).__new__(cls, name, bases, attrs) klass._schema_store = getattr(klass, "_schema_store", SchemaStore()) return klass
[docs]class Client(object): __metaclass__ = ClientMeta
[docs] def __init__(self, base_url, auth=None, client=None): self._main_client = (client or slumber.API)(base_url, auth) self._base_url = self._main_client._store["base_url"] self._method_gen()
[docs] def request(self, url, method="GET"): """ base requester * accept format below for url. - http://api.base.biz/base/v1/path/to/api/?id=1 - /base/v1/path/to/api/?id=1 - /v1/path/to/api/?id=1 - /path/to/api/?id=1 :param url: target url :param method: GET or POST (default: GET) :rtype: json :return: json object """ request_url = self._url_gen(url) s = self._main_client._store requests = s["session"] serializer = slumber.serialize.Serializer(default_format=s["format"]) return serializer.loads(requests.request(method, request_url).content)
[docs] def schema(self, model_name=None): """ receive schema :param model_name: resource class name :rtype: dict :return: schema dictionary """ if not model_name in self._schema_store: url = self._url_gen("{0}/schema/".format(model_name)) if model_name \ else self._base_url self._schema_store[model_name] = self.request(url) return self._schema_store[model_name]
[docs] def _url_gen(self, url): parse = urlparse.urlparse(url) if not parse.scheme: url_ = urlparse.urljoin(self._base_url, parse.path) if parse.query: return urlparse.urljoin(url_, "?{0}".format(parse.query)) return url_ else: return url
[docs] def _method_gen(self, base_client=None): base_client = base_client or copy.deepcopy(self) s = self.schema() for model_name in s: setattr(self, model_name, Model(self._main_client, model_name, s[model_name]["list_endpoint"], s[model_name]["schema"], base_client=base_client))