From 4d06039ad8467ea31c0c4b91df475eb329532d7c Mon Sep 17 00:00:00 2001 From: David Karchmer Date: Sun, 30 Oct 2022 11:57:16 -0700 Subject: [PATCH] Add USE_DASHES (#10) --- CHANGELOG.md | 4 +++ README.md | 1 + drf_client/connection.py | 60 +++++++++++++++++++++------------ drf_client/helpers/base_main.py | 1 + example.py | 1 + setup.py | 6 ++-- tests/api.py | 23 +++++++++++-- tests/resources.py | 13 +++++-- 8 files changed, 79 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e0b5fdf..0d19af8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### v0.6.0 (2022-10-30) + + * Add USE_DASHES option to automatically replace underscores ("_") with dashes ("-") + * Refactor to pass options to Resource class ### v0.5.0 (2022-05-16) diff --git a/README.md b/README.md index 6d23af1..a8cf15b 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ options = { 'USERNAME_KEY': 'username', 'LOGIN': 'auth/login/', 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, # Set to True to tell API to replace undercore ("_") with dashes ("-") } c = RestApi(options) diff --git a/drf_client/connection.py b/drf_client/connection.py index 7c09519..857148e 100644 --- a/drf_client/connection.py +++ b/drf_client/connection.py @@ -15,6 +15,7 @@ 'TOKEN_FORMAT': 'JWT {token}', 'LOGIN': 'auth/api-jwt-auth/', 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, } api = RestApi(options) @@ -34,6 +35,15 @@ DEFAULT_HEADERS = {'Content-Type': 'application/json'} DEFAULT_TOKEN_TYPE = 'jwt' DEFAULT_TOKEN_FORMAT = 'JWT {token}' +DEFAULT_OPTIONS = { + 'DOMAIN': 'http://example.com', + 'API_PREFIX': 'api/v1', + 'TOKEN_TYPE': 'jwt', + 'TOKEN_FORMAT': 'JWT {token}', + 'LOGIN': 'auth/login/', + 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, + } logger = logging.getLogger(__name__) @@ -46,9 +56,14 @@ class RestResource(object): which may or may not have children. """ _store = {} + _options = {} def __init__(self, *args, **kwargs): self._store = kwargs + if 'options' in self._store: + self._options = self._store['options'] + else: + self.options = DEFAULT_OPTIONS if 'use_token' not in self._store: self._store['use_token'] = False @@ -63,13 +78,13 @@ def __call__(self, id=None): kwargs = { 'token': self._store['token'], 'use_token': self._store['use_token'], - 'token_format': self._store['token_format'], - 'base_url': self._store['base_url'] + 'base_url': self._store['base_url'], + 'options': self._options, } new_url = self._store['base_url'] if id is not None: - new_url = '{0}{1}/'.format(new_url, id) + new_url = f'{new_url}{id}/' if not new_url.endswith('/'): new_url += '/' @@ -84,6 +99,8 @@ def __getattr__(self, item): raise AttributeError(item) kwargs = self._copy_kwargs(self._store) + if self._options.get('USE_DASHES', False): + item = item.replace("_", "-") kwargs.update({'base_url': '{0}{1}/'.format(self._store["base_url"], item)}) return self._get_resource(**kwargs) @@ -152,7 +169,7 @@ def _get_header(self): if self._store['use_token']: if not "token" in self._store: raise RestBaseException('No Token') - authorization_str = self._store['token_format'].format(token=self._store["token"]) + authorization_str = self._options['TOKEN_FORMAT'].format(token=self._store["token"]) headers['Authorization'] = authorization_str return headers @@ -211,25 +228,22 @@ def _get_resource(self, **kwargs): class Api(object): token = None - token_type = DEFAULT_TOKEN_TYPE - token_format = DEFAULT_TOKEN_FORMAT resource_class = RestResource use_token = True options = None def __init__(self, options): self.options = options - if 'DOMAIN' not in options: + if 'DOMAIN' not in self.options: raise RestBaseException("DOMAIN is missing in options") - if 'API_PREFIX' not in options: - options['API_PREFIX'] = API_PREFIX - self.base_url = '{0}/{1}'.format(self.options['DOMAIN'], options['API_PREFIX'] ) - if 'TOKEN_TYPE' in options: - self.token_type = options['TOKEN_TYPE'] - if 'TOKEN_FORMAT' in options: - self.token_format = options['TOKEN_FORMAT'] - + if 'API_PREFIX' not in self.options: + self.options['API_PREFIX'] = API_PREFIX + self.base_url = '{0}/{1}'.format(self.options['DOMAIN'], self.options['API_PREFIX'] ) + if 'TOKEN_TYPE' not in self.options: + self.options['TOKEN_TYPE'] = DEFAULT_TOKEN_TYPE + if 'TOKEN_FORMAT' not in self.options: + self.options['TOKEN_FORMAT'] = DEFAULT_TOKEN_FORMAT def set_token(self, token): self.token = token @@ -247,7 +261,7 @@ def login(self, password, username=None): r = requests.post(url, data=payload, headers=DEFAULT_HEADERS) if r.status_code in [200, 201]: content = json.loads(r.content.decode()) - self.token = content.get(self.token_type) + self.token = content.get(self.options['TOKEN_TYPE']) if self.token is None: # Default to "token" if token_type is not used by server self.token = content.get('token') @@ -259,13 +273,13 @@ def login(self, password, username=None): def logout(self): assert('LOGOUT' in self.options) - url = '{0}/{1}'.format(self.base_url, self.options['LOGOUT']) + url = f"{self.base_url}/{self.options['LOGOUT']}" headers = DEFAULT_HEADERS - headers['Authorization'] = self.token_format.format(token=self.token) + headers['Authorization'] = self.options['TOKEN_FORMAT'].format(token=self.token) r = requests.post(url, headers=headers) if r.status_code == 204: - logger.info('Goodbye @{0}'.format(self.username)) + logger.info(f'Goodbye @{self.username}') self.username = None self.token = None else: @@ -282,13 +296,15 @@ def __getattr__(self, item): if item.startswith("_"): raise AttributeError(item) + if self.options.get('USE_DASHES', False): + item = item.replace("_", "-") + kwargs = { 'token': self.token, - 'base_url': self.base_url, + 'base_url': f'{self.base_url}/{item}/', 'use_token': self.use_token, - 'token_format': self.token_format, + 'options': self.options, } - kwargs.update({'base_url': '{0}/{1}/'.format(kwargs['base_url'], item)}) return self._get_resource(**kwargs) diff --git a/drf_client/helpers/base_main.py b/drf_client/helpers/base_main.py index f489310..bc9af33 100644 --- a/drf_client/helpers/base_main.py +++ b/drf_client/helpers/base_main.py @@ -21,6 +21,7 @@ class BaseMain(object): 'USERNAME_KEY': 'username', 'LOGIN': 'auth/login/', 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, } logging_level = logging.INFO diff --git a/example.py b/example.py index 4717f21..9d83135 100644 --- a/example.py +++ b/example.py @@ -19,6 +19,7 @@ 'USERNAME_KEY': 'username', 'LOGIN': 'auth/login/', 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, } c = RestApi(options) diff --git a/setup.py b/setup.py index d58f5b6..066317b 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ README = (HERE / "README.md").read_text() setup(name='django-rest-framework-client', - version='0.5.0', + version='0.6.0', description='Python client for a DjangoRestFramework based web site', long_description=README, long_description_content_type="text/markdown", @@ -23,15 +23,15 @@ install_requires=[ 'requests', ], - python_requires=">=3.7,<4", + python_requires=">=3.8,<4", keywords=["django", "djangorestframework", "drf", "rest-client",], classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Programming Language :: Python :: Implementation :: PyPy", "Development Status :: 4 - Beta", "License :: OSI Approved :: MIT License", diff --git a/tests/api.py b/tests/api.py index 9bfe5eb..0489878 100644 --- a/tests/api.py +++ b/tests/api.py @@ -20,6 +20,7 @@ def setUp(self): 'TOKEN_FORMAT': 'JWT {token}', 'LOGIN': 'auth/login/', 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, } self.api = Api(options=options) @@ -31,7 +32,8 @@ def test_init(self): self.assertEqual(self.api.base_url, 'https://example.com/api/v1') self.assertTrue(self.api.use_token) - self.assertEqual(self.api.token_type, 'jwt') + self.assertEqual(self.api.options['TOKEN_TYPE'], 'jwt') + self.assertEqual(self.api.options['TOKEN_FORMAT'], 'JWT {token}') def test_set_token(self): @@ -98,11 +100,26 @@ def test_get_detail_with_action(self, m): } m.get('https://example.com/api/v1/test/my-detail/action/', text=json.dumps(payload)) - resp = self.api.test('my-detail').action.url() - self.assertEqual(resp, 'https://example.com/api/v1/test/my-detail/action/') + # resp = self.api.test('my-detail').action.url() + # self.assertEqual(resp, 'https://example.com/api/v1/test/my-detail/action/') resp = self.api.test('my-detail').action.get() self.assertEqual(resp, {'a': 'b', 'c': 'd'}) + @requests_mock.Mocker() + def test_get_with_use_dashes(self, m): + """test that we can replace underscore with dashes.""" + self.api.options['USE_DASHES'] = True + payload = { + "a": "b", + "c": "d" + } + m.get('https://example.com/api/v1/test-one/my-detail/action/', text=json.dumps(payload)) + + # resp = self.api.test('my-detail').action.url() + # self.assertEqual(resp, 'https://example.com/api/v1/test/my-detail/action/') + resp = self.api.test_one.my_detail.action.get() + self.assertEqual(resp, {'a': 'b', 'c': 'd'}) + @requests_mock.Mocker() def test_get_detail_with_extra_args(self, m): payload = { diff --git a/tests/resources.py b/tests/resources.py index 5330cd1..27a50ee 100644 --- a/tests/resources.py +++ b/tests/resources.py @@ -12,10 +12,19 @@ class ResourceTestCase(unittest.TestCase): def setUp(self): + self.options = { + 'DOMAIN': "https://example.com", + 'API_PREFIX': 'api/v1', + 'TOKEN_TYPE': 'jwt', + 'TOKEN_FORMAT': 'JWT {token}', + 'USERNAME_KEY': 'username', + 'LOGIN': 'auth/login/', + 'LOGOUT': 'auth/logout/', + 'USE_DASHES': False, + } self.base_resource = RestResource(base_url="https://example.com/api/v1/test/", use_token=True, - token_format='JWT {token}', - token_type='jwt', + options=self.options, token='my-token') def test_url(self):