diff --git a/.gitignore b/.gitignore index 66728b5..3178ff7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -*.pyc +# Text editors temporary files +*.sw[op] *.sublime* *.wp* - +# Python byte-code artifacts +*.py[cod] diff --git a/playground.py b/playground.py deleted file mode 100644 index 2693b08..0000000 --- a/playground.py +++ /dev/null @@ -1,26 +0,0 @@ -class SO(object): - - def __init__(self,**kwargs): - self.base_url = kwargs.pop('base_url',[]) or 'http://api.stackoverflow.com/1.1' - self.uriparts = kwargs.pop('uriparts',[]) - for k,v in kwargs.items(): - setattr(self,k,v) - - def __getattr__(self,key): - self.uriparts.append(key) - return self.__class__(**self.__dict__) - - def __getitem__(self,key): - return self.__getattr__(key) - - def __call__(self,**kwargs): - call_url = "%s/%s"%(self.base_url,"/".join(self.uriparts)) - self.uriparts = [] - return call_url - -if __name__ == '__main__': - print SO().abc.mno.ghi.jkl() - print SO().abc.mno['ghi'].jkl() - user1 = SO().users['55562'] - print user1.questions.unanswered() - print user1.questions.answered() \ No newline at end of file diff --git a/restconsumer.py b/restconsumer.py index 75a767f..37e82f6 100644 --- a/restconsumer.py +++ b/restconsumer.py @@ -1,60 +1,119 @@ -import requests, json +import json +import requests -def append_to_url(base_url,param): - return "%s%s/" % (base_url,param) + +def json_response(consumer, response): + return json.loads(response.content.decode()) + + +def append_to_url(base_url, param): + return "{}{}/".format(base_url, param) class RestConsumer(object): + """ Call REST-like endpoints this way: + + >>> consumer = RestConsumer('http://example.com/api/v1/') + >>> events = consumer.projects.groups[22].events() - def __init__(self,base_url,append_json=False,append_slash=False): - self.base_url = base_url if base_url[-1] == '/' else "%s%s" % (base_url,"/") - self.append_json = append_json - self.append_slash = append_slash + and it will call http://example.com/api/v1/projects/groups/22/events/ + """ - def __getattr__(self,key): - new_base = append_to_url(self.base_url,key) + base_url = '' + + _append_json = False + _append_slash = False + _requests_kwargs = {} + _response_wrapper = None + + def __init__(self, base_url, append_json=False, append_slash=False, response_wrapper=json_response, requests_kwargs={}): + self.base_url = base_url if base_url[-1] == '/' else "{}{}".format(base_url, "/") + self._append_json = append_json + self._append_slash = append_slash + self._response_wrapper = response_wrapper + self._requests_kwargs = requests_kwargs + + def __getattr__(self, key): + new_base = append_to_url(self.base_url, key) return self.__class__(base_url=new_base, - append_json=self.append_json, - append_slash=self.append_slash) - - def __getitem__(self,key): + append_json=self._append_json, + append_slash=self._append_slash, + response_wrapper=self._response_wrapper, + requests_kwargs=self._requests_kwargs) + + def __getitem__(self, key): return self.__getattr__(key) def __call__(self, **kwargs): - if not self.append_slash: + if not self._append_slash: self.base_url = self.base_url[:-1] - if self.append_json: - self.base_url = "%s%s" % (self.base_url,'.json') - print "Calling %s" % self.base_url - return self.get(self.base_url,**kwargs) + if self._append_json: + self.base_url = "{}{}".format(self.base_url, '.json') + if self._requests_kwargs: + kwargs.update(self._requests_kwargs) + return self.get(self.base_url, **kwargs) + + def get(self, url, **kwargs): + response = requests.get(url, **kwargs) + return self._response_wrapper(self, response) + + def post(self, **kwargs): + response = requests.post(**kwargs) + return self._response_wrapper(self, response) + + +class PaginatableResponse(object): + """ This iterator supports pagination done by Link header. + + Here's how you can use it: + + >>> consumer = RestConsumer('http://example.com/api/v1/', + response_wrapper=PaginatableResponse) + >>> events = consumer.projects.groups[22].events() + >>> for event in events: + print event + + It will iterate through all events on a server, + downloading next pages automatically when needed. + """ - def get(self,url,**kwargs): - r = requests.get(url,**kwargs) - return json.loads(r.content) + def __init__(self, consumer, response): + self._consumer = consumer + self._response = response + self._set_state(response) - def post(self,**kwargs): - r = requests.post(**kwargs) - return json.loads(r.content) + def __iter__(self): + return self + def __next__(self): + try: + return next(self._content_iterator) + except StopIteration: + if self._is_paginatable(): + self._download_next_page() + return next(self._content_iterator) + else: + raise -Twitter = RestConsumer(base_url='https://api.twitter.com/1',append_json=True) -Github = RestConsumer(base_url='https://api.github.com') -Stackoverflow = RestConsumer(base_url='http://api.stackoverflow.com/1.1') + def next(self): + return self.__next__() -if __name__=='__main__': - from pprint import pprint - t = RestConsumer(base_url='https://api.twitter.com/1',append_json=True) - public_timeline = t.statuses.public_timeline() - pprint(public_timeline) + def _set_state(self, response): + self._response = response + self._content_iterator = iter(json.loads(response.content.decode())) - g = RestConsumer(base_url='https://api.github.com') - repos = g.users.kennethreitz.repos() - pprint(repos) + def _download_next_page(self): + self._consumer.base_url = self._response.links['next']['url'] + response = self._consumer()._response + self._set_state(response) - s = RestConsumer(base_url='http://api.stackoverflow.com/1.1') - sr = s.users['55562'].questions.unanswered() - pprint(sr) + # FIXME This guy should be reimplemented according to HTTP Link standard. + def _is_paginatable(self): + if 'next' in self._response.links: + if 'results' in self._response.links['next']: + if self._response.links['next']['results'] == 'true': + return True + else: + return True - sr2 = s.tags.python['top-answerers']['all-time'] - pprint(sr2()) - \ No newline at end of file + return False diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ce40d96 --- /dev/null +++ b/setup.py @@ -0,0 +1,27 @@ +import os + +from setuptools import setup, find_packages + +install_requires = [ + 'requests', +] + +setup( + name='python-restconsumer', + version='1.1.2', + description='RESTful API generic client with sweet interface.', + long_description='See README.rst', + classifiers=[ + "Programming Language :: Python", + "Programming Language :: Python :: 3", + ], + author='murchik', + author_email='mixturchik@gmail.com', + url='https://github.com/moorchegue/python-restconsumer', + keywords='rest api generic client', + packages=find_packages(), + py_modules=['restconsumer'], + include_package_data=True, + zip_safe=False, + install_requires=install_requires, +) diff --git a/test.py b/test.py deleted file mode 100644 index 2693b08..0000000 --- a/test.py +++ /dev/null @@ -1,26 +0,0 @@ -class SO(object): - - def __init__(self,**kwargs): - self.base_url = kwargs.pop('base_url',[]) or 'http://api.stackoverflow.com/1.1' - self.uriparts = kwargs.pop('uriparts',[]) - for k,v in kwargs.items(): - setattr(self,k,v) - - def __getattr__(self,key): - self.uriparts.append(key) - return self.__class__(**self.__dict__) - - def __getitem__(self,key): - return self.__getattr__(key) - - def __call__(self,**kwargs): - call_url = "%s/%s"%(self.base_url,"/".join(self.uriparts)) - self.uriparts = [] - return call_url - -if __name__ == '__main__': - print SO().abc.mno.ghi.jkl() - print SO().abc.mno['ghi'].jkl() - user1 = SO().users['55562'] - print user1.questions.unanswered() - print user1.questions.answered() \ No newline at end of file