Skip to content

Commit

Permalink
Add trailing_slash argument to the session that helps to avoid issu…
Browse files Browse the repository at this point in the history
…es with Django Rest Framework redirects
  • Loading branch information
iamanikeev committed May 27, 2020
1 parent 9df28f5 commit 0398ee0
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 18 deletions.
14 changes: 8 additions & 6 deletions src/jsonapi_client/document.py
@@ -1,5 +1,5 @@
"""
JSON API Python client
JSON API Python client
https://github.com/qvantel/jsonapi-client
(see JSON API specification in http://jsonapi.org/)
Expand Down Expand Up @@ -62,6 +62,8 @@ def __init__(self, session: 'Session',
no_cache: bool=False) -> None:
self._no_cache = no_cache # if true, do not store resources to session cache
self._url = url
if session.trailing_slash and not self._url.endswith('/'):
self._url = self._url + '/'
super().__init__(session, json_data)

@property
Expand Down Expand Up @@ -113,11 +115,11 @@ def __str__(self):
def _iterator_sync(self) -> 'Iterator[ResourceObject]':
# if we currently have no items on the page, then there's no need to yield items
# and check the next page
# we do this because there are APIs that always have a 'next' link, even when
# we do this because there are APIs that always have a 'next' link, even when
# there are no items on the page
if len(self.resources) == 0:
return

yield from self.resources

if self.links.next:
Expand All @@ -127,11 +129,11 @@ def _iterator_sync(self) -> 'Iterator[ResourceObject]':
async def _iterator_async(self) -> 'AsyncIterator[ResourceObject]':
# if we currently have no items on the page, then there's no need to yield items
# and check the next page
# we do this because there are APIs that always have a 'next' link, even when
# we do this because there are APIs that always have a 'next' link, even when
# there are no items on the page
if len(self.resources) == 0:
return

for res in self.resources:
yield res

Expand All @@ -158,4 +160,4 @@ def mark_invalid(self):
"""
super().mark_invalid()
for r in self.resources:
r.mark_invalid()
r.mark_invalid()
4 changes: 3 additions & 1 deletion src/jsonapi_client/objects.py
Expand Up @@ -78,6 +78,8 @@ def _handle_data(self, data):
self.meta = Meta(self.session, data.get('meta', {}))
else:
self.href = ''
if self.session.trailing_slash and self.href and not self.href.endswith('/'):
self.href = self.href + '/'

def __eq__(self, other):
return self.href == other.href
Expand Down Expand Up @@ -149,7 +151,7 @@ def _handle_data(self, data):

@property
def url(self):
return f'{self.session.url_prefix}/{self.type}/{self.id}'
return f'{self.session.url_prefix}/{self.type}/{self.id}{self.session.trailing_slash}'

def __str__(self):
return f'{self.type}: {self.id}'
Expand Down
6 changes: 3 additions & 3 deletions src/jsonapi_client/resourceobject.py
@@ -1,5 +1,5 @@
"""
JSON API Python client
JSON API Python client
https://github.com/qvantel/jsonapi-client
(see JSON API specification in http://jsonapi.org/)
Expand Down Expand Up @@ -277,7 +277,7 @@ def _determine_class(self, data: dict, relation_type: str=None):
"""
From data and/or provided relation_type, determine Relationship class
to be used.
:param data: Source data dictionary
:param relation_type: either 'to-one' or 'to-many'
"""
Expand Down Expand Up @@ -474,7 +474,7 @@ def dirty_fields(self):
@property
def url(self) -> str:
url = str(self.links.self)
return url or self.id and f'{self.session.url_prefix}/{self.type}/{self.id}'
return url or self.id and f'{self.session.url_prefix}/{self.type}/{self.id}/{self.session.trailing_slash}'

@property
def post_url(self) -> str:
Expand Down
10 changes: 7 additions & 3 deletions src/jsonapi_client/session.py
Expand Up @@ -123,7 +123,8 @@ def __init__(self, server_url: str=None,
schema: dict=None,
request_kwargs: dict=None,
loop: 'AbstractEventLoop'=None,
use_relationship_iterator: bool=False,) -> None:
use_relationship_iterator: bool=False,
trailing_slash=False,) -> None:
self._server: ParseResult
self.enable_async = enable_async

Expand All @@ -143,6 +144,7 @@ def __init__(self, server_url: str=None,
import aiohttp
self._aiohttp_session = aiohttp.ClientSession(loop=loop)
self.use_relationship_iterator = use_relationship_iterator
self.trailing_slash = '/' if trailing_slash else ''

def add_resources(self, *resources: 'ResourceObject') -> None:
"""
Expand All @@ -151,6 +153,8 @@ def add_resources(self, *resources: 'ResourceObject') -> None:
for res in resources:
self.resources_by_resource_identifier[(res.type, res.id)] = res
lnk = res.links.self.url if res.links.self else res.url
if self.trailing_slash and not lnk.endswith(self.trailing_slash):
lnk = lnk + '/'
if lnk:
self.resources_by_link[lnk] = res

Expand Down Expand Up @@ -312,9 +316,9 @@ def url_prefix(self) -> str:
def _url_for_resource(self, resource_type: str,
resource_id: str=None,
filter: 'Modifier'=None) -> str:
url = f'{self.url_prefix}/{resource_type}'
url = f'{self.url_prefix}/{resource_type}{self.trailing_slash}'
if resource_id is not None:
url = f'{url}/{resource_id}'
url = f'{url}/{resource_id}{self.trailing_slash}'
if filter:
url = filter.url_with_modifiers(url)
return url
Expand Down
69 changes: 64 additions & 5 deletions tests/test_client.py
Expand Up @@ -198,7 +198,7 @@ async def __call__(self, *args):
def mocked_fetch(mocker):
def mock_fetch(url):
parsed_url = urlparse(url)
file_path = parsed_url.path[1:]
file_path = parsed_url.path[1:].rstrip('/')
query = parsed_url.query
return load(f'{file_path}?{query}' if query else file_path)

Expand All @@ -210,8 +210,8 @@ class MockedFetchAsync:
async def __call__(self, url):
return mock_fetch(url)

m1 = mocker.patch('jsonapi_client.session.Session._fetch_json', new_callable=MockedFetch)
m2 = mocker.patch('jsonapi_client.session.Session._fetch_json_async', new_callable=MockedFetchAsync)
mocker.patch('jsonapi_client.session.Session._fetch_json', new_callable=MockedFetch)
mocker.patch('jsonapi_client.session.Session._fetch_json_async', new_callable=MockedFetchAsync)
return


Expand All @@ -228,7 +228,7 @@ def session():

def test_initialization(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema)
article = s.get('articles')
s.get('articles')
assert s.resources_by_link['http://example.com/articles/1'] is \
s.resources_by_resource_identifier[('articles', '1')]
assert s.resources_by_link['http://example.com/comments/12'] is \
Expand All @@ -239,10 +239,23 @@ def test_initialization(mocked_fetch, article_schema):
s.resources_by_resource_identifier[('people', '9')]


def test_initialization_w_trailing_slash(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema, trailing_slash=True)
s.get('articles')
assert s.resources_by_link['http://example.com/articles/1/'] is \
s.resources_by_resource_identifier[('articles', '1')]
assert s.resources_by_link['http://example.com/comments/12/'] is \
s.resources_by_resource_identifier[('comments', '12')]
assert s.resources_by_link['http://example.com/comments/5/'] is \
s.resources_by_resource_identifier[('comments', '5')]
assert s.resources_by_link['http://example.com/people/9/'] is \
s.resources_by_resource_identifier[('people', '9')]


@pytest.mark.asyncio
async def test_initialization_async(mocked_fetch, article_schema):
s = Session('http://localhost:8080', enable_async=True, schema=article_schema)
article = await s.get('articles')
await s.get('articles')
assert s.resources_by_link['http://example.com/articles/1'] is \
s.resources_by_resource_identifier[('articles', '1')]
assert s.resources_by_link['http://example.com/comments/12'] is \
Expand Down Expand Up @@ -271,6 +284,23 @@ def test_basic_attributes(mocked_fetch, article_schema):
assert my_attrs == attr_set


def test_basic_attributes_w_trailing_slash(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema, trailing_slash=True)
doc = s.get('articles')
assert len(doc.resources) == 3
article = doc.resources[0]
assert article.id == "1"
assert article.type == "articles"
assert article.title.startswith('JSON API paints')

assert doc.links.self.href == 'http://example.com/articles/'
attr_set = {'title', 'author', 'comments', 'nested1', 'comment_or_author', 'comments_or_authors'}

my_attrs = {i for i in dir(article.fields) if not i.startswith('_')}

assert my_attrs == attr_set


def test_resourceobject_without_attributes(mocked_fetch):
s = Session('http://localhost:8080', schema=invitation_schema)
doc = s.get('invitations')
Expand All @@ -286,6 +316,20 @@ def test_resourceobject_without_attributes(mocked_fetch):
assert my_attrs == attr_set


def test_resourceobject_without_attributes_w_trailing_slash(mocked_fetch):
s = Session('http://localhost:8080', schema=invitation_schema, trailing_slash=True)
doc = s.get('invitations')
assert len(doc.resources) == 1
invitation = doc.resources[0]
assert invitation.id == "1"
assert invitation.type == "invitations"
assert doc.links.self.href == 'http://example.com/invitations/'
attr_set = {'host', 'guest'}

my_attrs = {i for i in dir(invitation.fields) if not i.startswith('_')}

assert my_attrs == attr_set


@pytest.mark.asyncio
async def test_basic_attributes_async(mocked_fetch, article_schema):
Expand Down Expand Up @@ -339,6 +383,21 @@ def test_relationships_single(mocked_fetch, article_schema):
assert article3.comment_or_author is None


def test_relationships_single_w_trailing_slash(mocked_fetch, article_schema):
s = Session('http://localhost:8080', schema=article_schema, trailing_slash=True)
article, article2, article3 = s.get('articles').resources
author = article.author
assert {i for i in dir(author.fields) if not i.startswith('_')} \
== {'first_name', 'last_name', 'twitter'}
assert author.type == 'people'
assert author.id == '9'

assert author.first_name == 'Dan'
assert author['first-name'] == 'Dan'
assert author.last_name == 'Gebhardt'
assert article.relationships.author.links.self.href == "http://example.com/articles/1/relationships/author/"


@pytest.mark.asyncio
async def test_relationships_iterator_async(mocked_fetch, article_schema):
s = Session('http://localhost:8080', enable_async=True, schema=article_schema, use_relationship_iterator=True)
Expand Down

0 comments on commit 0398ee0

Please sign in to comment.