Skip to content

Commit

Permalink
V2 release
Browse files Browse the repository at this point in the history
  • Loading branch information
sayanarijit committed Mar 28, 2019
1 parent 9254e1d commit e50bbf9
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 65 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -45,6 +45,7 @@ nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache

# Translations
*.mo
Expand Down Expand Up @@ -99,3 +100,6 @@ ENV/

# mypy
.mypy_cache/

# Editors
.vscode/
6 changes: 1 addition & 5 deletions README.md
Expand Up @@ -178,8 +178,4 @@ Once debugging is set to 'True', Every HTTP call will return debug information i

## Exceptions

* As this package uses requests module to perform HTTP calls, most exceptions will be raised by requests module itself.

* In case API server returns HTTP status code outside the range of 200-299, It will raise ***resteasy.HTTPError***

* In case the returned content by API server is not parsable, It will raise ***resteasy.InvalidResponseError***
* As this package uses requests module to perform HTTP calls, so all exceptions will be raised by requests module itself.
143 changes: 90 additions & 53 deletions resteasy.py
Expand Up @@ -8,54 +8,69 @@
from __future__ import absolute_import, unicode_literals
import json
import requests
from requests.packages.urllib3.exceptions import InsecureRequestWarning


class HTTPError(Exception):
'''Status code returned by server not in range 200-299'''
def __init__(self, status, content):
Exception.__init__(self, 'Server returned HTTP status code: %s\n%s' % (status, content))

class InvalidResponseError(Exception):
'''Data returned by server could not be decoded'''
def __init__(self, content):
Exception.__init__(self, 'Server returned incompatible response:\n'+content)
from copy import deepcopy


class RESTEasy(object):
"""REST API client session creator"""

def __init__(self, base_url, auth=None, verify=False, cert=None, timeout=None,
encoder=json.dumps, decoder=json.loads, debug=False):
"""REST API client session creator.
Arguments:
base_url (str): Base URL of the API service.
Optional keyword arguments:
encoder (callable): Encoder used to encode data to be posted.
decoder (callable): Decoder used to decode returned data.
timeout (float): Default request timeout.
debug (bool): Toggle debug mode.
kwargs (dict): Extra arguments to update `requests.Session` object.
"""

def __init__(
self, base_url, encoder=json.dumps, decoder=json.loads,
timeout=None, debug=False, **kwargs):
self.base_url = base_url
self.session = requests.Session()
self.session.auth = auth
self.session.verify = verify
self.session.cert = cert
self.session.headers.update({
'Content-Type': 'application/json',
'Accept': 'application/json'
})
self.session.headers['Content-Type'] = 'application/json'
self.session.headers['Accept'] = 'application/json'
self.session.__dict__.update(kwargs)
self.timeout = timeout
self.encoder = encoder
self.decoder = decoder
self.debug = debug

def route(self, *args):
"""Return endpoint object"""
"""Return endpoint object.
Arguments:
args (list): Route URL path.
Returns:
APIEndpoint: Object that supports CRUD queries.
"""

return APIEndpoint(
endpoint=self.base_url + '/' + ('/'.join(map(str, args))),
session=self.session, timeout=self.timeout,
encoder=self.encoder, decoder=self.decoder, debug=self.debug
)
endpoint='{}/{}'.format(
self.base_url, ('/'.join(map(str, args)))),
session=deepcopy(self.session), timeout=self.timeout,
encoder=self.encoder, decoder=self.decoder, debug=self.debug)


class APIEndpoint(object):
"""API endpoint that supports CRUD operations"""

def __init__(self, endpoint, session, timeout=None,
encoder=json.dumps, decoder=json.loads, debug=False):
"""API endpoint supports CRUD queries.
Arguments:
endpoint (str): Full URL of the API endpoint.
session (requests.Session): A copy of `requests.Session` object.
Optional keyword arguments:
encoder (callable): Encoder used to encode data to be posted.
decoder (callable): Decoder used to decode returned data.
timeout (float): Default request timeout.
debug (bool): Toggle debug mode.
"""

def __init__(self, endpoint, session, encoder=json.dumps,
decoder=json.loads, timeout=None, debug=False):
self.endpoint = endpoint
self.session = session
self.timeout = timeout
Expand All @@ -64,37 +79,59 @@ def __init__(self, endpoint, session, timeout=None,
self.debug = debug

def route(self, *args):
"""Return endpoint object"""
"""Routes to a new endpoint.
Arguments:
args (list): Route URL path.
Returns:
APIEndpoint: Object that supports CRUD queries.
"""

return APIEndpoint(
endpoint=self.endpoint + '/' + ('/'.join(map(str, args))),
endpoint='{}/{}'.format(
self.endpoint, ('/'.join(map(str, args)))),
session=self.session, timeout=self.timeout,
encoder=self.encoder, decoder=self.decoder, debug=self.debug
)
encoder=self.encoder, decoder=self.decoder, debug=self.debug)

def request(self, method, **kwargs):
"""A shortcut to the `self.session.request` method.
Arguments:
method (str): HTTP request method.
kwargs (dict): Arguments to pass to the `self.session.request` method.
Returns:
requests.Response|dict: Raw response object or dictionary in case of debug.
"""
if self.debug:
return dict(kwargs, endpoint=self.endpoint, method=method)
return self.session.request(method, self.endpoint, **kwargs)

def do(self, method, kwargs={}):
"""Do the HTTP request"""

method = method.upper()
"""Do the HTTP request.
Arguments:
method (str): HTTP request method.
kwargs (dict): Request parameters in case of GET/DELETE, else request data.
Returns:
self.decoder(str): Decoded response object.
"""

if method == 'GET' or method == 'DELETE':
response = self.request(method, params=kwargs, timeout=self.timeout)
else:
response = self.request(
method, data=self.encoder(kwargs), timeout=self.timeout)

if self.debug:
return dict(endpoint=self.endpoint, method=method, kwargs=kwargs, session=self.session, timeout=self.timeout)
return response

if method == 'GET':
response = self.session.get(self.endpoint,
params=kwargs, timeout=self.timeout)
else:
response = self.session.request(method, self.endpoint,
data=self.encoder(kwargs), timeout=self.timeout)
response.raise_for_status()

content = response.content.decode('latin1')
if response.status_code not in range(200,300):
raise HTTPError(response.status_code, content)

try:
return self.decoder(content)
except Exception:
raise InvalidResponseError(content)
return self.decoder(content)

def get(self, **kwargs): return self.do('GET', kwargs)
def post(self, **kwargs): return self.do('POST', kwargs)
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -3,7 +3,7 @@
from os import path


VERSION = 'v1.0.5'
VERSION = 'v2.0.0'

here = path.abspath(path.dirname(__file__))

Expand Down
14 changes: 8 additions & 6 deletions tests/crud_test.py
@@ -1,3 +1,4 @@
import json
import unittest
from resteasy import RESTEasy

Expand All @@ -15,7 +16,7 @@ def test_get(self):
req = tasks.get(filter='shop')
self.assertEqual(req['endpoint'], 'https://fakesite.com/api/tasks')
self.assertEqual(req['method'], 'GET')
self.assertEqual(req['kwargs'], {'filter': 'shop'})
self.assertEqual(req['params'], {'filter': 'shop'})
self.assertEqual(req['timeout'], 60)

tasks.timeout = 30
Expand All @@ -29,16 +30,16 @@ def test_post(self):
req = tasks.post(text='test note')
self.assertEqual(req['endpoint'], 'https://fakesite.com/api/tasks')
self.assertEqual(req['method'], 'POST')
self.assertEqual(req['kwargs'], {'text': 'test note'})
self.assertEqual(req['data'], json.dumps({'text': 'test note'}))
self.assertEqual(req['timeout'], 60)

def test_put(self):
'''POST requests'''
'''PUT requests'''

req = tasks.route(1).put(text='test note')
self.assertEqual(req['endpoint'], 'https://fakesite.com/api/tasks/1')
self.assertEqual(req['method'], 'PUT')
self.assertEqual(req['kwargs'], {'text': 'test note'})
self.assertEqual(req['data'], json.dumps({'text': 'test note'}))
self.assertEqual(req['timeout'], 60)

def test_patch(self):
Expand All @@ -47,15 +48,16 @@ def test_patch(self):
req = tasks.route(1).patch(text='test note')
self.assertEqual(req['endpoint'], 'https://fakesite.com/api/tasks/1')
self.assertEqual(req['method'], 'PATCH')
self.assertEqual(req['kwargs'], {'text': 'test note'})
self.assertEqual(req['data'], json.dumps({'text': 'test note'}))
self.assertEqual(req['timeout'], 60)

def test_delete(self):
'''DELETE requests'''

req = tasks.route(1).delete()
req = tasks.route(1).delete(id=1)
self.assertEqual(req['endpoint'], 'https://fakesite.com/api/tasks/1')
self.assertEqual(req['method'], 'DELETE')
self.assertEqual(req['params'], {'id': 1})
self.assertEqual(req['timeout'], 60)


Expand Down

0 comments on commit e50bbf9

Please sign in to comment.