Skip to content

Add trailing_slash argument to the session #51

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/jsonapi_client/document.py
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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