From 31a35e60c76d4dd84b416c760a0fec9a0a1678c5 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Wed, 19 Mar 2014 10:14:01 +0100 Subject: [PATCH 01/25] Update Git ignore list --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a9eca38..ef86b70 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +build/* dist/* docs/_build/* .tox From 623231d8c211d8637fafcad9a45469aefd5fda7c Mon Sep 17 00:00:00 2001 From: David Fischer Date: Wed, 19 Mar 2014 10:14:32 +0100 Subject: [PATCH 02/25] Add access_token helper --- slumber/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 22338de..0d4e563 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -96,6 +96,7 @@ def _request(self, method, data=None, files=None, params=None): url = url + "/" headers = {"accept": s.get_content_type()} + headers.update(self._store["headers"]) if not files: headers["content-type"] = s.get_content_type() @@ -181,7 +182,7 @@ def delete(self, **kwargs): class API(ResourceAttributesMixin, object): - def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None): + def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None, access_token=None): if serializer is None: serializer = Serializer(default=format) @@ -195,6 +196,7 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses "append_slash": append_slash, "session": session, "serializer": serializer, + "headers": {u'Authorization': u'Bearer ' + access_token} if access_token else {} } # Do some Checks for Required Values From 4b7969a366992126ca73c716ee9148e84da8925c Mon Sep 17 00:00:00 2001 From: David Fischer Date: Wed, 19 Mar 2014 11:04:01 +0100 Subject: [PATCH 03/25] Add response hook --- slumber/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 22338de..551afe3 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -123,15 +123,14 @@ def _try_to_serialize_response(self, resp): if resp.headers.get("content-type", None): content_type = resp.headers.get("content-type").split(";")[0].strip() - try: stype = s.get_serializer(content_type=content_type) + response = stype.loads(resp.content) except exceptions.SerializerNotAvailable: - return resp.content - - return stype.loads(resp.content) + response = resp.content else: - return resp.content + response = resp.content + return self._store["response_hook"](response) def get(self, **kwargs): resp = self._request("GET", params=kwargs) @@ -181,7 +180,7 @@ def delete(self, **kwargs): class API(ResourceAttributesMixin, object): - def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None): + def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None, response_hook=None): if serializer is None: serializer = Serializer(default=format) @@ -195,6 +194,7 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses "append_slash": append_slash, "session": session, "serializer": serializer, + "response_hook": response_hook or (lambda x: x) } # Do some Checks for Required Values From 4d2c05dd5f43bc80a94f56dc20643be97fb23cc7 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Fri, 21 Mar 2014 15:17:21 +0100 Subject: [PATCH 04/25] Improve token-based authentication --- CHANGELOG.rst | 5 +++++ setup.py | 2 +- slumber/__init__.py | 8 +++++--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 36ef737..ef757c6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,11 @@ Changelog development version ------------------- +0.6.2 +----- + +* Add token to authenticate requests with a token. +* Add response_hook to allow tweaking the returned response. 0.6.0 ----- diff --git a/setup.py b/setup.py index d60a944..39fe25c 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setup( name = "slumber", - version = "0.6.1.dev", + version = "0.6.2.dev", description = "A library that makes consuming a REST API easier and more convenient", long_description="\n\n".join([ open(os.path.join(base_dir, "README.rst"), "r").read(), diff --git a/slumber/__init__.py b/slumber/__init__.py index 70c63dd..c49af97 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -96,7 +96,9 @@ def _request(self, method, data=None, files=None, params=None): url = url + "/" headers = {"accept": s.get_content_type()} - headers.update(self._store["headers"]) + + if self._store["token"]: + headers["Authorization"] = "{token_type} {access_token}".format(**self._store["token"]) if not files: headers["content-type"] = s.get_content_type() @@ -182,7 +184,7 @@ def delete(self, **kwargs): class API(ResourceAttributesMixin, object): def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None, - access_token=None, response_hook=None): + token=None, response_hook=None): if serializer is None: serializer = Serializer(default=format) @@ -196,7 +198,7 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses "append_slash": append_slash, "session": session, "serializer": serializer, - "headers": {u'Authorization': u'Bearer ' + access_token} if access_token else {}, + "token": token, "response_hook": response_hook or (lambda x: x) } From a4e9d16524bd03cf5fa94178539b3d80de396904 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Fri, 21 Mar 2014 15:36:42 +0100 Subject: [PATCH 05/25] Remove unused variables --- slumber/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index c49af97..14b5270 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -143,8 +143,6 @@ def get(self, **kwargs): return # @@@ We should probably do some sort of error here? (Is this even possible?) def post(self, data=None, files=None, **kwargs): - s = self._store["serializer"] - resp = self._request("POST", data=data, files=files, params=kwargs) if 200 <= resp.status_code <= 299: return self._try_to_serialize_response(resp) @@ -153,8 +151,6 @@ def post(self, data=None, files=None, **kwargs): return def patch(self, data=None, files=None, **kwargs): - s = self._store["serializer"] - resp = self._request("PATCH", data=data, files=files, params=kwargs) if 200 <= resp.status_code <= 299: return self._try_to_serialize_response(resp) @@ -164,7 +160,6 @@ def patch(self, data=None, files=None, **kwargs): def put(self, data=None, files=None, **kwargs): resp = self._request("PUT", data=data, files=files, params=kwargs) - if 200 <= resp.status_code <= 299: return self._try_to_serialize_response(resp) else: From 2bf04fb08c9d0b228f66121c16117637db1f9eeb Mon Sep 17 00:00:00 2001 From: David Fischer Date: Fri, 21 Mar 2014 15:37:28 +0100 Subject: [PATCH 06/25] Fix pep-8 E128 --- slumber/serialize.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/slumber/serialize.py b/slumber/serialize.py index 4d8e936..1be7080 100644 --- a/slumber/serialize.py +++ b/slumber/serialize.py @@ -36,12 +36,12 @@ def dumps(self, data): class JsonSerializer(BaseSerializer): content_types = [ - "application/json", - "application/x-javascript", - "text/javascript", - "text/x-javascript", - "text/x-json", - ] + "application/json", + "application/x-javascript", + "text/javascript", + "text/x-javascript", + "text/x-json", + ] key = "json" def loads(self, data): From 8442990b758cbd4e10234ca0f43aec1645eb0a9e Mon Sep 17 00:00:00 2001 From: David Fischer Date: Wed, 9 Apr 2014 15:53:35 +0200 Subject: [PATCH 07/25] Fix tests --- slumber/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 14b5270..dad302d 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -97,7 +97,7 @@ def _request(self, method, data=None, files=None, params=None): headers = {"accept": s.get_content_type()} - if self._store["token"]: + if self._store.get("token", None): headers["Authorization"] = "{token_type} {access_token}".format(**self._store["token"]) if not files: @@ -133,7 +133,7 @@ def _try_to_serialize_response(self, resp): response = resp.content else: response = resp.content - return self._store["response_hook"](response) + return self._store.get("response_hook", lambda x: x)(response) def get(self, **kwargs): resp = self._request("GET", params=kwargs) From c95eda4695434ca8b84a3ab03de59164db4d877d Mon Sep 17 00:00:00 2001 From: David Fischer Date: Wed, 9 Apr 2014 16:18:20 +0200 Subject: [PATCH 08/25] Now compatible with Python 3 --- CHANGELOG.rst | 5 +++++ setup.py | 12 ++++++++++-- slumber/__init__.py | 8 ++++++-- slumber/exceptions.py | 5 +++++ slumber/serialize.py | 6 +++++- tests/__init__.py | 4 ++++ tests/resource.py | 4 ++++ tests/serializer.py | 4 ++++ tests/utils.py | 10 +++++++--- tox.ini | 2 +- 10 files changed, 51 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ef757c6..9dac615 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,6 +7,11 @@ Changelog development version ------------------- +0.7.0 +----- + +* Now compatible with Python 3. + 0.6.2 ----- diff --git a/setup.py b/setup.py index 39fe25c..68c0a02 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,13 @@ -import os +# -*- coding: utf-8 -*- + +import os, sys from setuptools import setup +kwargs = {} +if sys.version_info[0] >= 3: + print('Converting code to Python 3 helped by 2to3') + kwargs['use_2to3'] = True + install_requires = ["requests"] tests_require = ["mock"] @@ -8,7 +15,7 @@ setup( name = "slumber", - version = "0.6.2.dev", + version = "0.7.0.dev", description = "A library that makes consuming a REST API easier and more convenient", long_description="\n\n".join([ open(os.path.join(base_dir, "README.rst"), "r").read(), @@ -22,4 +29,5 @@ install_requires = install_requires, tests_require = tests_require, test_suite = "tests.get_tests", + **kwargs ) diff --git a/slumber/__init__.py b/slumber/__init__.py index dad302d..97fe1d7 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -1,10 +1,14 @@ +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, unicode_literals + import posixpath import urlparse import requests -from slumber import exceptions -from slumber.serialize import Serializer +from . import exceptions +from .serialize import Serializer __all__ = ["Resource", "API"] diff --git a/slumber/exceptions.py b/slumber/exceptions.py index da7068c..8706ba6 100644 --- a/slumber/exceptions.py +++ b/slumber/exceptions.py @@ -1,3 +1,8 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + + class SlumberBaseException(Exception): """ All Slumber exceptions inherit from this exception. diff --git a/slumber/serialize.py b/slumber/serialize.py index 1be7080..324446a 100644 --- a/slumber/serialize.py +++ b/slumber/serialize.py @@ -1,4 +1,8 @@ -from slumber import exceptions +# -*- coding: utf-8 -*- + +from __future__ import absolute_import, unicode_literals + +from . import exceptions _SERIALIZERS = { "json": True, diff --git a/tests/__init__.py b/tests/__init__.py index 760c0c9..fab86fc 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + import os.path import unittest diff --git a/tests/resource.py b/tests/resource.py index 6dc6132..a14bd41 100644 --- a/tests/resource.py +++ b/tests/resource.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + import mock import unittest import requests diff --git a/tests/serializer.py b/tests/serializer.py index 4f1dceb..72c5413 100644 --- a/tests/serializer.py +++ b/tests/serializer.py @@ -1,3 +1,7 @@ +# -*- coding: utf-8 -*- + +from __future__ import unicode_literals + import unittest import slumber import slumber.serialize diff --git a/tests/utils.py b/tests/utils.py index f672a63..c04663a 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -1,7 +1,10 @@ # -*- coding: utf-8 -*- +from __future__ import unicode_literals + import unittest import slumber +import sys class UtilsTestCase(unittest.TestCase): @@ -53,10 +56,11 @@ def test_url_join_encoded_unicode(self): url = slumber.url_join("http://example.com/", "tǝst/") self.assertEqual(url, expected) - url = slumber.url_join("http://example.com/", "tǝst/".decode('utf8').encode('utf8')) + url = slumber.url_join("http://example.com/", "tǝst/".encode('utf8').decode('utf8')) self.assertEqual(url, expected) + @unittest.skipIf(sys.version_info[0] >= 3, "Python 3 does not allow mixing strings and bytes.") def test_url_join_decoded_unicode(self): - url = slumber.url_join("http://example.com/", "tǝst/".decode('utf8')) - expected = "http://example.com/tǝst/".decode('utf8') + url = slumber.url_join("http://example.com/".encode('utf-8'), "tǝst/") + expected = "http://example.com/tǝst/" self.assertEqual(url, expected) diff --git a/tox.ini b/tox.ini index f445068..d446b76 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, pypy +envlist = py27, py33, pypy [testenv] commands = From a86b787f59415f7715476c37fd5802eb5f993d4c Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 17 Apr 2014 11:17:05 +0200 Subject: [PATCH 09/25] Convert bytes strings to unicode strings --- CHANGELOG.rst | 3 ++- setup.py | 2 +- slumber/serialize.py | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 9dac615..987a346 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -7,10 +7,11 @@ Changelog development version ------------------- -0.7.0 +0.7.1 ----- * Now compatible with Python 3. +* Convert bytes strings to unicode strings. 0.6.2 ----- diff --git a/setup.py b/setup.py index 68c0a02..06331c0 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name = "slumber", - version = "0.7.0.dev", + version = "0.7.1.dev", description = "A library that makes consuming a REST API easier and more convenient", long_description="\n\n".join([ open(os.path.join(base_dir, "README.rst"), "r").read(), diff --git a/slumber/serialize.py b/slumber/serialize.py index 324446a..56e4a71 100644 --- a/slumber/serialize.py +++ b/slumber/serialize.py @@ -49,7 +49,7 @@ class JsonSerializer(BaseSerializer): key = "json" def loads(self, data): - return json.loads(data) + return json.loads(data if isinstance(data, unicode) else data.decode('utf-8')) def dumps(self, data): return json.dumps(data) @@ -61,7 +61,7 @@ class YamlSerializer(BaseSerializer): key = "yaml" def loads(self, data): - return yaml.safe_load(data) + return yaml.safe_load(data if isinstance(data, unicode) else data.decode('utf-8')) def dumps(self, data): return yaml.dump(data) From 5bde0035467f4aee8a15ee1a0b37d916a1aa6b96 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 10:36:27 +0200 Subject: [PATCH 10/25] Enable Travis CI build --- README.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.rst b/README.rst index b43df19..dc94b84 100644 --- a/README.rst +++ b/README.rst @@ -1,6 +1,11 @@ Slumber ======= +.. image:: https://secure.travis-ci.org/davidfischer-ch/slumber.png + :target: http://travis-ci.org/davidfischer-ch/slumber + +Afraid of red status ? Please click on the link, sometimes this is not my fault ;-) + Slumber is a Python library that provides a convenient yet powerful object-oriented interface to ReSTful APIs. It acts as a wrapper around the excellent requests_ library and abstracts away the handling of URLs, serialization, From 3babd7c1a3e9865a21bac708a9c6302e7dfec14f Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 10:37:34 +0200 Subject: [PATCH 11/25] Cleanup setup script --- setup.py | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/setup.py b/setup.py index 06331c0..929416e 100644 --- a/setup.py +++ b/setup.py @@ -8,26 +8,23 @@ print('Converting code to Python 3 helped by 2to3') kwargs['use_2to3'] = True -install_requires = ["requests"] -tests_require = ["mock"] - base_dir = os.path.dirname(os.path.abspath(__file__)) setup( - name = "slumber", - version = "0.7.1.dev", - description = "A library that makes consuming a REST API easier and more convenient", + name="slumber", + version="0.7.1.dev", + description="A library that makes consuming a REST API easier and more convenient", long_description="\n\n".join([ open(os.path.join(base_dir, "README.rst"), "r").read(), open(os.path.join(base_dir, "CHANGELOG.rst"), "r").read() ]), - url = "http://slumber.in/", - author = "Donald Stufft", - author_email = "donald.stufft@gmail.com", - packages = ["slumber"], - zip_safe = False, - install_requires = install_requires, - tests_require = tests_require, - test_suite = "tests.get_tests", + url="http://slumber.in/", + author="Donald Stufft", + author_email="donald.stufft@gmail.com", + packages=["slumber"], + zip_safe=False, + install_requires=["requests"], + tests_require=["mock"], + test_suite="tests.get_tests", **kwargs ) From c6efaf623830fbd21d209e9b0098eb8853f29b1f Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 10:37:47 +0200 Subject: [PATCH 12/25] Add Python 3.4 to build matrix --- .travis.yml | 8 ++++++-- tox.ini | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index bc42683..fbd4e60 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,5 +4,9 @@ python: - 2.7 - 3.1 - 3.2 -install: pip install -r requirements.txt && pip install -r requirements-test.txt -script: python setup.py test + - 3.3 + - 3.4 +install: + - travis_retry pip install -r requirements.txt -r requirements-test.txt +script: + - python setup.py test diff --git a/tox.ini b/tox.ini index d446b76..16b88cf 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py33, pypy +envlist = py27, py33, py34, pypy [testenv] commands = From 392a4f0a82bd222a3bfdf4efeb7084adc9227d1f Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 10:46:15 +0200 Subject: [PATCH 13/25] Remove Python 2.6 from compatibility list --- .travis.yml | 2 -- README.rst | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index fbd4e60..6ae2e31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ language: python python: - - 2.6 - 2.7 - - 3.1 - 3.2 - 3.3 - 3.4 diff --git a/README.rst b/README.rst index dc94b84..f8743b6 100644 --- a/README.rst +++ b/README.rst @@ -39,7 +39,7 @@ Requirements Slumber requires the following modules. -* Python 2.6+ +* Python 2.7+ * requests * pyyaml (If you are using the optional YAML serialization) From a33951a5d46201b7dadfdd6d96b22f4a74fdabf7 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 13:11:31 +0200 Subject: [PATCH 14/25] Merge imports --- slumber/__init__.py | 5 +---- tests/__init__.py | 3 +-- tests/resource.py | 7 ++----- tests/serializer.py | 3 +-- tests/utils.py | 3 +-- 5 files changed, 6 insertions(+), 15 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 97fe1d7..ee877e1 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -2,10 +2,7 @@ from __future__ import absolute_import, unicode_literals -import posixpath -import urlparse - -import requests +import posixpath, urlparse, requests from . import exceptions from .serialize import Serializer diff --git a/tests/__init__.py b/tests/__init__.py index fab86fc..5adcc82 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -2,8 +2,7 @@ from __future__ import unicode_literals -import os.path -import unittest +import os.path, unittest def get_tests(): diff --git a/tests/resource.py b/tests/resource.py index a14bd41..d35237a 100644 --- a/tests/resource.py +++ b/tests/resource.py @@ -2,11 +2,8 @@ from __future__ import unicode_literals -import mock -import unittest -import requests -import slumber -import slumber.serialize +import mock, unittest, requests +import slumber, slumber.serialize class ResourceTestCase(unittest.TestCase): diff --git a/tests/serializer.py b/tests/serializer.py index 72c5413..ec2851c 100644 --- a/tests/serializer.py +++ b/tests/serializer.py @@ -3,8 +3,7 @@ from __future__ import unicode_literals import unittest -import slumber -import slumber.serialize +import slumber, slumber.serialize class ResourceTestCase(unittest.TestCase): diff --git a/tests/utils.py b/tests/utils.py index c04663a..6e6b662 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -2,9 +2,8 @@ from __future__ import unicode_literals -import unittest +import unittest, sys import slumber -import sys class UtilsTestCase(unittest.TestCase): From e25639741b030821e5d70e389593893ca3e590a4 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 13:12:41 +0200 Subject: [PATCH 15/25] Replace for loop by dict.update --- slumber/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index ee877e1..93164b5 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -35,10 +35,8 @@ def __getattr__(self, item): raise AttributeError(item) kwargs = {} - for key, value in self._store.iteritems(): - kwargs[key] = value - - kwargs.update({"base_url": url_join(self._store["base_url"], item)}) + kwargs.update(self._store) + kwargs["base_url"] = url_join(self._store["base_url"], item) return Resource(**kwargs) @@ -70,8 +68,7 @@ def __call__(self, id=None, format=None, url_override=None): return self kwargs = {} - for key, value in self._store.iteritems(): - kwargs[key] = value + kwargs.update(self._store) if id is not None: kwargs["base_url"] = url_join(self._store["base_url"], id) From fb0d38466ce6013fd1cc81e3774d1a52c2691b58 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 13:21:13 +0200 Subject: [PATCH 16/25] Improve exception class to embed response + generate msg --- slumber/__init__.py | 13 +++++++------ slumber/exceptions.py | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 93164b5..f7417cb 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -103,14 +103,15 @@ def _request(self, method, data=None, files=None, params=None): if data is not None: data = s.dumps(data) - resp = self._store["session"].request(method, url, data=data, params=params, files=files, headers=headers) + response = self._store["session"].request(method, url, data=data, params=params, files=files, headers=headers) - if 400 <= resp.status_code <= 499: - raise exceptions.HttpClientError("Client Error %s: %s" % (resp.status_code, url), response=resp, content=resp.content) - elif 500 <= resp.status_code <= 599: - raise exceptions.HttpServerError("Server Error %s: %s" % (resp.status_code, url), response=resp, content=resp.content) + if 400 <= response.status_code <= 499: + raise exceptions.HttpClientError(response) + elif 500 <= response.status_code <= 599: + raise exceptions.HttpServerError(response) - self._ = resp + self._ = response + return response return resp diff --git a/slumber/exceptions.py b/slumber/exceptions.py index 8706ba6..48008c7 100644 --- a/slumber/exceptions.py +++ b/slumber/exceptions.py @@ -14,10 +14,11 @@ class SlumberHttpBaseException(SlumberBaseException): All Slumber HTTP Exceptions inherit from this exception. """ - def __init__(self, *args, **kwargs): - for key, value in kwargs.iteritems(): - setattr(self, key, value) - super(SlumberHttpBaseException, self).__init__(*args) + def __init__(self, response): + self.response = response + value = "%s Error %s %s: %s, text: %s" % ('Client' if response.status_code <= 499 else 'Server', + response.status_code, response.reason, response.url, response.text) + super(SlumberHttpBaseException, self).__init__(value) class HttpClientError(SlumberHttpBaseException): From 441b89a82ab98ad8fdd164a71463ec3808c87422 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 13:23:00 +0200 Subject: [PATCH 17/25] Rename resp -> response + cleanup code --- slumber/__init__.py | 50 +++++++++-------- tests/resource.py | 128 ++++++++++++++++++++++++++------------------ tests/serializer.py | 12 ++--- 3 files changed, 106 insertions(+), 84 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index f7417cb..c3855ff 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -113,61 +113,59 @@ def _request(self, method, data=None, files=None, params=None): self._ = response return response - return resp - - def _handle_redirect(self, resp, **kwargs): + def _handle_redirect(self, response, **kwargs): # @@@ Hacky, see description in __call__ - resource_obj = self(url_override=resp.headers["location"]) + resource_obj = self(url_override=response.headers["location"]) return resource_obj.get(params=kwargs) - def _try_to_serialize_response(self, resp): + def _try_to_serialize_response(self, response): s = self._store["serializer"] - if resp.headers.get("content-type", None): - content_type = resp.headers.get("content-type").split(";")[0].strip() + if response.headers.get("content-type", None): + content_type = response.headers.get("content-type").split(";")[0].strip() try: stype = s.get_serializer(content_type=content_type) - response = stype.loads(resp.content) + response_content = stype.loads(response.content) except exceptions.SerializerNotAvailable: - response = resp.content + response_content = response.content else: - response = resp.content - return self._store.get("response_hook", lambda x: x)(response) + response_content = response.content + return self._store.get("response_hook", lambda x: x)(response_content) def get(self, **kwargs): - resp = self._request("GET", params=kwargs) - if 200 <= resp.status_code <= 299: - return self._try_to_serialize_response(resp) + response = self._request("GET", params=kwargs) + if 200 <= response.status_code <= 299: + return self._try_to_serialize_response(response) else: return # @@@ We should probably do some sort of error here? (Is this even possible?) def post(self, data=None, files=None, **kwargs): - resp = self._request("POST", data=data, files=files, params=kwargs) - if 200 <= resp.status_code <= 299: - return self._try_to_serialize_response(resp) + response = self._request("POST", data=data, files=files, params=kwargs) + if 200 <= response.status_code <= 299: + return self._try_to_serialize_response(response) else: # @@@ Need to be Some sort of Error Here or Something return def patch(self, data=None, files=None, **kwargs): - resp = self._request("PATCH", data=data, files=files, params=kwargs) - if 200 <= resp.status_code <= 299: - return self._try_to_serialize_response(resp) + response = self._request("PATCH", data=data, files=files, params=kwargs) + if 200 <= response.status_code <= 299: + return self._try_to_serialize_response(response) else: # @@@ Need to be Some sort of Error Here or Something return def put(self, data=None, files=None, **kwargs): - resp = self._request("PUT", data=data, files=files, params=kwargs) - if 200 <= resp.status_code <= 299: - return self._try_to_serialize_response(resp) + response = self._request("PUT", data=data, files=files, params=kwargs) + if 200 <= response.status_code <= 299: + return self._try_to_serialize_response(response) else: return False def delete(self, **kwargs): - resp = self._request("DELETE", params=kwargs) - if 200 <= resp.status_code <= 299: - if resp.status_code == 204: + response = self._request("DELETE", params=kwargs) + if 200 <= response.status_code <= 299: + if response.status_code == 204: return True else: return True # @@@ Should this really be True? diff --git a/tests/resource.py b/tests/resource.py index d35237a..546030c 100644 --- a/tests/resource.py +++ b/tests/resource.py @@ -23,10 +23,10 @@ def test_get_200_json(self): }) self.base_resource._store["session"].request.return_value = r - resp = self.base_resource._request("GET") + response = self.base_resource._request("GET") - self.assertTrue(resp is r) - self.assertEqual(resp.content, r.content) + self.assertTrue(response is r) + self.assertEqual(response.content, r.content) self.base_resource._store["session"].request.assert_called_once_with( "GET", @@ -34,11 +34,14 @@ def test_get_200_json(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.get() - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.get() + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_get_200_text(self): r = mock.Mock(spec=requests.Response) @@ -52,10 +55,10 @@ def test_get_200_text(self): }) self.base_resource._store["session"].request.return_value = r - resp = self.base_resource._request("GET") + response = self.base_resource._request("GET") - self.assertTrue(resp is r) - self.assertEqual(resp.content, "Mocked Content") + self.assertTrue(response is r) + self.assertEqual(response.content, "Mocked Content") self.base_resource._store["session"].request.assert_called_once_with( "GET", @@ -63,11 +66,14 @@ def test_get_200_text(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.get() - self.assertEqual(resp, r.content) + response = self.base_resource.get() + self.assertEqual(response, r.content) def test_post_201_redirect(self): r1 = mock.Mock(spec=requests.Response) @@ -86,10 +92,10 @@ def test_post_201_redirect(self): }) self.base_resource._store["session"].request.side_effect = (r1, r2) - resp = self.base_resource._request("POST") + response = self.base_resource._request("POST") - self.assertTrue(resp is r1) - self.assertEqual(resp.content, r1.content) + self.assertTrue(response is r1) + self.assertEqual(response.content, r1.content) self.base_resource._store["session"].request.assert_called_once_with( "POST", @@ -97,11 +103,14 @@ def test_post_201_redirect(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.post(data={'foo': 'bar'}) - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.post(data={'foo': 'bar'}) + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_post_decodable_response(self): r = mock.Mock(spec=requests.Response) @@ -115,10 +124,10 @@ def test_post_decodable_response(self): }) self.base_resource._store["session"].request.return_value = r - resp = self.base_resource._request("POST") + response = self.base_resource._request("POST") - self.assertTrue(resp is r) - self.assertEqual(resp.content, r.content) + self.assertTrue(response is r) + self.assertEqual(response.content, r.content) self.base_resource._store["session"].request.assert_called_once_with( "POST", @@ -126,11 +135,14 @@ def test_post_decodable_response(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.post(data={'foo': 'bar'}) - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.post(data={'foo': 'bar'}) + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_patch_201_redirect(self): r1 = mock.Mock(spec=requests.Response) @@ -149,10 +161,10 @@ def test_patch_201_redirect(self): }) self.base_resource._store["session"].request.side_effect = (r1, r2) - resp = self.base_resource._request("PATCH") + response = self.base_resource._request("PATCH") - self.assertTrue(resp is r1) - self.assertEqual(resp.content, r1.content) + self.assertTrue(response is r1) + self.assertEqual(response.content, r1.content) self.base_resource._store["session"].request.assert_called_once_with( "PATCH", @@ -160,11 +172,14 @@ def test_patch_201_redirect(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.patch(data={'foo': 'bar'}) - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.patch(data={'foo': 'bar'}) + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_patch_decodable_response(self): r = mock.Mock(spec=requests.Response) @@ -178,10 +193,10 @@ def test_patch_decodable_response(self): }) self.base_resource._store["session"].request.return_value = r - resp = self.base_resource._request("PATCH") + response = self.base_resource._request("PATCH") - self.assertTrue(resp is r) - self.assertEqual(resp.content, r.content) + self.assertTrue(response is r) + self.assertEqual(response.content, r.content) self.base_resource._store["session"].request.assert_called_once_with( "PATCH", @@ -189,11 +204,14 @@ def test_patch_decodable_response(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.patch(data={'foo': 'bar'}) - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.patch(data={'foo': 'bar'}) + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_put_201_redirect(self): r1 = mock.Mock(spec=requests.Response) @@ -212,10 +230,10 @@ def test_put_201_redirect(self): }) self.base_resource._store["session"].request.side_effect = (r1, r2) - resp = self.base_resource._request("PUT") + response = self.base_resource._request("PUT") - self.assertTrue(resp is r1) - self.assertEqual(resp.content, r1.content) + self.assertTrue(response is r1) + self.assertEqual(response.content, r1.content) self.base_resource._store["session"].request.assert_called_once_with( "PUT", @@ -223,11 +241,14 @@ def test_put_201_redirect(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.put(data={'foo': 'bar'}) - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.put(data={'foo': 'bar'}) + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_put_decodable_response(self): r = mock.Mock(spec=requests.Response) @@ -241,10 +262,10 @@ def test_put_decodable_response(self): }) self.base_resource._store["session"].request.return_value = r - resp = self.base_resource._request("PUT") + response = self.base_resource._request("PUT") - self.assertTrue(resp is r) - self.assertEqual(resp.content, r.content) + self.assertTrue(response is r) + self.assertEqual(response.content, r.content) self.base_resource._store["session"].request.assert_called_once_with( "PUT", @@ -252,22 +273,25 @@ def test_put_decodable_response(self): data=None, files=None, params=None, - headers={"content-type": self.base_resource._store["serializer"].get_content_type(), "accept": self.base_resource._store["serializer"].get_content_type()} + headers={ + "content-type": self.base_resource._store["serializer"].get_content_type(), + "accept": self.base_resource._store["serializer"].get_content_type() + } ) - resp = self.base_resource.put(data={'foo': 'bar'}) - self.assertEqual(resp['result'], ['a', 'b', 'c']) + response = self.base_resource.put(data={'foo': 'bar'}) + self.assertEqual(response['result'], ['a', 'b', 'c']) def test_handle_serialization(self): self.base_resource._store.update({ "serializer": slumber.serialize.Serializer(), }) - resp = mock.Mock(spec=requests.Response) - resp.headers = {"content-type": "application/json; charset=utf-8"} - resp.content = '{"foo": "bar"}' + response = mock.Mock(spec=requests.Response) + response.headers = {"content-type": "application/json; charset=utf-8"} + response.content = '{"foo": "bar"}' - r = self.base_resource._try_to_serialize_response(resp) + r = self.base_resource._try_to_serialize_response(response) if not isinstance(r, dict): self.fail("Serialization did not take place") diff --git a/tests/serializer.py b/tests/serializer.py index ec2851c..20f7d20 100644 --- a/tests/serializer.py +++ b/tests/serializer.py @@ -12,10 +12,10 @@ def test_json_get_serializer(self): s = slumber.serialize.Serializer() for content_type in [ - "application/json", - "application/x-javascript", - "text/javascript", - "text/x-javascript", - "text/x-json", - ]: + "application/json", + "application/x-javascript", + "text/javascript", + "text/x-javascript", + "text/x-json", + ]: s.get_serializer(content_type=content_type) From fa2afe87101da3a214847b76a7aad332c748008e Mon Sep 17 00:00:00 2001 From: David Fischer Date: Tue, 6 May 2014 13:23:15 +0200 Subject: [PATCH 18/25] Add pypy to travis build matrix --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 6ae2e31..b07a4c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ python: - 3.2 - 3.3 - 3.4 + - pypy install: - travis_retry pip install -r requirements.txt -r requirements-test.txt script: From 3dce54a68316680cd19eea5b22d6c6f463bc74d0 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 12:31:25 +0100 Subject: [PATCH 19/25] Remove outdated comments --- slumber/__init__.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index c3855ff..b4b5ae2 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -25,9 +25,6 @@ class ResourceAttributesMixin(object): A Mixin that makes it so that accessing an undefined attribute on a class results in returning a Resource Instance. This Instance can then be used to make calls to the a Resource. - - It assumes that a Meta class exists at self._meta with all the required - attributes. """ def __getattr__(self, item): @@ -47,9 +44,6 @@ class Resource(ResourceAttributesMixin, object): attribute -> url, kwarg -> query param, and other related behind the scenes python to HTTP transformations. It's goal is to represent a single resource which may or may not have children. - - It assumes that a Meta class exists at self._meta with all the required - attributes. """ def __init__(self, *args, **kwargs): From c9f50e57c23b9065615b9633b254b025ecb51483 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 12:32:13 +0100 Subject: [PATCH 20/25] Code cleanup --- slumber/__init__.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index b4b5ae2..437cfb0 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -30,9 +30,7 @@ class ResourceAttributesMixin(object): def __getattr__(self, item): if item.startswith("_"): raise AttributeError(item) - - kwargs = {} - kwargs.update(self._store) + kwargs = self._store.copy() kwargs["base_url"] = url_join(self._store["base_url"], item) return Resource(**kwargs) @@ -61,8 +59,7 @@ def __call__(self, id=None, format=None, url_override=None): if id is None and format is None and url_override is None: return self - kwargs = {} - kwargs.update(self._store) + kwargs = self._store.copy() if id is not None: kwargs["base_url"] = url_join(self._store["base_url"], id) @@ -76,8 +73,6 @@ def __call__(self, id=None, format=None, url_override=None): # but a Location to an object that we need to GET. kwargs["base_url"] = url_override - kwargs["session"] = self._store["session"] - return self.__class__(**kwargs) def _request(self, method, data=None, files=None, params=None): @@ -180,7 +175,7 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses self._store = { "base_url": base_url, - "format": format if format is not None else "json", + "format": "json" if format is None else format, "append_slash": append_slash, "session": session, "serializer": serializer, From 6e8182b21572d2326a350306d3c53556cf8751cc Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 12:35:14 +0100 Subject: [PATCH 21/25] Add "self" to resource hook args --- slumber/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 437cfb0..272e503 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -119,7 +119,8 @@ def _try_to_serialize_response(self, response): response_content = response.content else: response_content = response.content - return self._store.get("response_hook", lambda x: x)(response_content) + hook = self._store.get("response_hook") + return hook(self, response_content) if hook else response_content def get(self, **kwargs): response = self._request("GET", params=kwargs) @@ -180,7 +181,7 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses "session": session, "serializer": serializer, "token": token, - "response_hook": response_hook or (lambda x: x) + "response_hook": response_hook } # Do some Checks for Required Values From 48a494b7ea6954f0df1a4a08c2b074d3190a125a Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 12:50:56 +0100 Subject: [PATCH 22/25] Make subclassing easier --- slumber/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 272e503..2da2274 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -32,8 +32,10 @@ def __getattr__(self, item): raise AttributeError(item) kwargs = self._store.copy() kwargs["base_url"] = url_join(self._store["base_url"], item) + return self._get_resource(**kwargs) - return Resource(**kwargs) + def _get_resource(self, **kwargs): + return self.__class__(**kwargs) class Resource(ResourceAttributesMixin, object): @@ -165,6 +167,8 @@ def delete(self, **kwargs): class API(ResourceAttributesMixin, object): + resource_class = Resource + def __init__(self, base_url=None, auth=None, format=None, append_slash=True, session=None, serializer=None, token=None, response_hook=None): if serializer is None: @@ -187,3 +191,6 @@ def __init__(self, base_url=None, auth=None, format=None, append_slash=True, ses # Do some Checks for Required Values if self._store.get("base_url") is None: raise exceptions.ImproperlyConfigured("base_url is required") + + def _get_resource(self, **kwargs): + return self.resource_class(**kwargs) From 9c0d11019c71178e2391535c24a744aaa01df8ed Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 12:54:03 +0100 Subject: [PATCH 23/25] Protect against hidden bugs --- slumber/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 2da2274..d66c575 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -46,7 +46,7 @@ class Resource(ResourceAttributesMixin, object): which may or may not have children. """ - def __init__(self, *args, **kwargs): + def __init__(self, **kwargs): self._store = kwargs def __call__(self, id=None, format=None, url_override=None): From e6c5715a68b0a17199051fc5d17994f497aa9973 Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 12:57:12 +0100 Subject: [PATCH 24/25] Update docstrings --- slumber/__init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index d66c575..194f4a1 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -12,7 +12,7 @@ def url_join(base, *args): """ - Helper function to join an arbitrary number of url segments together. + Helper function to join an arbitrary number of URL segments together. """ scheme, netloc, path, query, fragment = urlparse.urlsplit(base) path = path if len(path) else "/" @@ -22,8 +22,8 @@ def url_join(base, *args): class ResourceAttributesMixin(object): """ - A Mixin that makes it so that accessing an undefined attribute on a class - results in returning a Resource Instance. This Instance can then be used + A mixin that makes it so that accessing an undefined attribute on a class + results in returning a Resource instance. This instance can then be used to make calls to the a Resource. """ @@ -41,9 +41,9 @@ def _get_resource(self, **kwargs): class Resource(ResourceAttributesMixin, object): """ Resource provides the main functionality behind slumber. It handles the - attribute -> url, kwarg -> query param, and other related behind the scenes - python to HTTP transformations. It's goal is to represent a single resource - which may or may not have children. + attribute -> URL, kwargs -> query parameters, and other related behind the + scenes python to HTTP transformations. It's goal is to represent a single + resource which may or may not have children. """ def __init__(self, **kwargs): From 4f5febf6a6d331024c433e76ddd968c106022dab Mon Sep 17 00:00:00 2001 From: David Fischer Date: Thu, 15 Jan 2015 13:21:44 +0100 Subject: [PATCH 25/25] Make subclassing easier (bis) --- slumber/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/slumber/__init__.py b/slumber/__init__.py index 194f4a1..3ce94c6 100644 --- a/slumber/__init__.py +++ b/slumber/__init__.py @@ -75,7 +75,7 @@ def __call__(self, id=None, format=None, url_override=None): # but a Location to an object that we need to GET. kwargs["base_url"] = url_override - return self.__class__(**kwargs) + return self._get_resource(**kwargs) def _request(self, method, data=None, files=None, params=None): s = self._store["serializer"]