Skip to content

Commit

Permalink
Merge pull request #21 from Wesmania/include-support
Browse files Browse the repository at this point in the history
Include support
  • Loading branch information
ajjn authored Jul 16, 2018
2 parents 3d1a6fc + bd61cf4 commit 6305260
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 45 deletions.
25 changes: 17 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ Client session
# AsyncIO the same but remember to await:
documents = await s.get('resource_type')
Filtering
---------
Filtering and including
-----------------------
.. code-block:: python
Expand All @@ -92,14 +92,23 @@ Filtering
filter = Filter(attribute='something', attribute2='something_else')
# - filtering some-dict.some-attr == 'something'
filter = Filter(some_dict__some_attr='something'))
# - filtering manually with your server syntax.
filter = Filter('filter[post]=1&filter[author]=2')
# If you have different URL schema for filtering, you can implement your own Filter
# class (derive it from Filter and reimplement format_filter_query).
# Same thing goes for including.
# - including two fields
include = Inclusion('related_field', 'other_related_field')
# Then fetch your filtered document
filtered = s.get('resource_type', filter) # AsyncIO with await
# Custom syntax for request parameters.
# If you have different URL schema for filtering or other GET parameters,
# you can implement your own Modifier class (derive it from Modifier and
# reimplement appended_query).
modifier = Modifier('filter[post]=1&filter[author]=2')
# All above classes subclass Modifier and can be added to concatenate
# parameters
modifier_sum = filter + include + modifier
# Now fetch your document
filtered = s.get('resource_type', modifier_sum) # AsyncIO with await
# To access resources included in document:
r1 = document.resources[0] # first ResourceObject of document.
Expand Down
2 changes: 1 addition & 1 deletion src/jsonapi_client/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import pkg_resources

from .session import Session
from .filter import Filter
from .filter import Filter, Inclusion, Modifier
from .common import ResourceTuple

__version__ = pkg_resources.get_distribution("jsonapi-client").version
76 changes: 64 additions & 12 deletions src/jsonapi_client/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,51 @@

if TYPE_CHECKING:
FilterKeywords = Dict[str, Union[str, Sequence[Union[str, int, float]]]]
IncludeKeywords = Sequence[str]


class Filter:
class Modifier:
"""
Base class for query modifiers.
You can derive your own class and use it if you have custom syntax.
"""
def __init__(self, query_str: str='') -> None:
self._query_str = query_str

def url_with_modifiers(self, base_url: str) -> str:
"""
Returns url with modifiers appended.
Example:
Modifier('filter[attr1]=1,2&filter[attr2]=2').filtered_url('doc')
-> 'GET doc?filter[attr1]=1,2&filter[attr2]=2'
"""
filter_query = self.appended_query()
fetch_url = f'{base_url}?{filter_query}'
return fetch_url

def appended_query(self) -> str:
return self._query_str

def __add__(self, other: 'Modifier') -> 'Modifier':
mods = []
for m in [self, other]:
if isinstance(m, ModifierSum):
mods += m.modifiers
else:
mods.append(m)
return ModifierSum(mods)


class ModifierSum(Modifier):
def __init__(self, modifiers):
self.modifiers = modifiers

def appended_query(self) -> str:
return '&'.join(m.appended_query() for m in self.modifiers)


class Filter(Modifier):
"""
Implements query filtering for Session.get etc.
You can derive your own filter class and use it if you have a
Expand All @@ -48,21 +90,18 @@ def __init__(self, query_str: str='', **filter_kwargs: 'FilterKeywords') -> None
:param filter_kwargs: Specify required conditions on result.
Example: Filter(attribute='1', relation__attribute='2')
"""
self._query_str = query_str
super().__init__(query_str)
self._filter_kwargs = filter_kwargs

def filtered_url(self, base_url: str) -> str:
"""
Returns url with filter parameters appended.
# This and next method prevent any existing subclasses from breaking
def url_with_modifiers(self, base_url: str) -> str:
return self.filtered_url(base_url)

Example:
Filter(attr1__in=[1, 2] attr2='2').filtered_url('doc')
-> 'GET doc?filter[attr1]=1,2&filter[attr2]=2'
"""
def filtered_url(self, base_url: str) -> str:
return super().url_with_modifiers(base_url)

filter_query = self._query_str or self.format_filter_query(**self._filter_kwargs)
fetch_url = f'{base_url}?{filter_query}'
return fetch_url
def appended_query(self) -> str:
return super().appended_query() or self.format_filter_query(**self._filter_kwargs)

def format_filter_query(self, **kwargs: 'FilterKeywords') -> str:
"""
Expand All @@ -73,3 +112,16 @@ def jsonify_key(key):
return key.replace('__', '.').replace('_', '-')
return '&'.join(f'filter[{jsonify_key(key)}]={value}'
for key, value in kwargs.items())


class Inclusion(Modifier):
"""
Implements query inclusion for Session.get etc.
"""
def __init__(self, *include_args: 'IncludeKeywords') -> None:
super().__init__()
self._include_args = include_args

def appended_query(self) -> str:
includes = ','.join(self._include_args)
return f'include={includes}'
18 changes: 9 additions & 9 deletions src/jsonapi_client/relationships.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
R_IDENT_TYPES = Union[str, ResourceObject, ResourceIdentifier, ResourceTuple]

if TYPE_CHECKING:
from .filter import Filter
from .filter import Modifier
from .document import Document
from .session import Session

Expand Down Expand Up @@ -79,24 +79,24 @@ def __init__(self,
def is_single(self) -> bool:
raise NotImplementedError

def _filter_sync(self, filter: 'Filter') -> 'Document':
url = filter.filtered_url(self.url)
def _modify_sync(self, modifier: 'Modifier') -> 'Document':
url = modifier.url_with_modifiers(self.url)
return self.session.fetch_document_by_url(url)

async def _filter_async(self, filter_obj: 'Filter'):
url = filter_obj.filtered_url(self.url)
async def _modify_async(self, modifier: 'Modifier'):
url = modifier.url_with_modifiers(self.url)
return self.session.fetch_document_by_url_async(url)

def filter(self, filter: 'Filter') -> 'Union[Awaitable[Document], Document]':
def filter(self, filter: 'Modifier') -> 'Union[Awaitable[Document], Document]':
"""
Receive filtered list of resources. Use Filter instance.
Receive filtered list of resources. Use Modifier instance.
If in async mode, this needs to be awaited.
"""
if self.session.enable_async:
return self._filter_async(filter)
return self._modify_async(filter)
else:
return self._filter_sync(filter)
return self._modify_sync(filter)

@property
def is_dirty(self) -> bool:
Expand Down
30 changes: 15 additions & 15 deletions src/jsonapi_client/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from .document import Document
from .resourceobject import ResourceObject
from .relationships import ResourceTuple
from .filter import Filter
from .filter import Modifier

logger = logging.getLogger(__name__)
NOT_FOUND = object()
Expand Down Expand Up @@ -311,20 +311,20 @@ def url_prefix(self) -> str:

def _url_for_resource(self, resource_type: str,
resource_id: str=None,
filter: 'Filter'=None) -> str:
filter: 'Modifier'=None) -> str:
url = f'{self.url_prefix}/{resource_type}'
if resource_id is not None:
url = f'{url}/{resource_id}'
if filter:
url = filter.filtered_url(url)
url = filter.url_with_modifiers(url)
return url

@staticmethod
def _resource_type_and_filter(
resource_id_or_filter: 'Union[Filter, str]'=None)\
-> 'Tuple[Optional[str], Optional[Filter]]':
from .filter import Filter
if isinstance(resource_id_or_filter, Filter):
resource_id_or_filter: 'Union[Modifier, str]'=None)\
-> 'Tuple[Optional[str], Optional[Modifier]]':
from .filter import Modifier
if isinstance(resource_id_or_filter, Modifier):
resource_id = None
filter = resource_id_or_filter
else:
Expand All @@ -333,26 +333,26 @@ def _resource_type_and_filter(
return resource_id, filter

def _get_sync(self, resource_type: str,
resource_id_or_filter: 'Union[Filter, str]'=None) -> 'Document':
resource_id_or_filter: 'Union[Modifier, str]'=None) -> 'Document':
resource_id, filter_ = self._resource_type_and_filter(
resource_id_or_filter)
url = self._url_for_resource(resource_type, resource_id, filter_)
return self.fetch_document_by_url(url)

async def _get_async(self, resource_type: str,
resource_id_or_filter: 'Union[Filter, str]'=None) -> 'Document':
resource_id_or_filter: 'Union[Modifier, str]'=None) -> 'Document':
resource_id, filter_ = self._resource_type_and_filter(
resource_id_or_filter)
url = self._url_for_resource(resource_type, resource_id, filter_)
return await self.fetch_document_by_url_async(url)

def get(self, resource_type: str,
resource_id_or_filter: 'Union[Filter, str]'=None) \
resource_id_or_filter: 'Union[Modifier, str]'=None) \
-> 'Union[Awaitable[Document], Document]':
"""
Request (GET) Document from server.
:param resource_id_or_filter: Resource id or Filter instance to filter
:param resource_id_or_filter: Resource id or Modifier instance to filter
resulting resources.
If session is used with enable_async=True, this needs
Expand All @@ -363,18 +363,18 @@ def get(self, resource_type: str,
else:
return self._get_sync(resource_type, resource_id_or_filter)

def _iterate_sync(self, resource_type: str, filter: 'Filter'=None) \
def _iterate_sync(self, resource_type: str, filter: 'Modifier'=None) \
-> 'Iterator[ResourceObject]':
doc = self.get(resource_type, filter)
yield from doc._iterator_sync()

async def _iterate_async(self, resource_type: str, filter: 'Filter'=None) \
async def _iterate_async(self, resource_type: str, filter: 'Modifier'=None) \
-> 'AsyncIterator[ResourceObject]':
doc = await self._get_async(resource_type, filter)
async for res in doc._iterator_async():
yield res

def iterate(self, resource_type: str, filter: 'Filter'=None) \
def iterate(self, resource_type: str, filter: 'Modifier'=None) \
-> 'Union[AsyncIterator[ResourceObject], Iterator[ResourceObject]]':
"""
Request (GET) Document from server and iterate through resources.
Expand All @@ -384,7 +384,7 @@ def iterate(self, resource_type: str, filter: 'Filter'=None) \
If session is used with enable_async=True, this needs to iterated with
async for.
:param filter: Filter instance to filter resulting resources.
:param filter: Modifier instance to filter resulting resources.
"""
if self.enable_async:
return self._iterate_async(resource_type, filter)
Expand Down
28 changes: 28 additions & 0 deletions tests/test_modifiers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from jsonapi_client.filter import Inclusion, Modifier


def test_modifier():
url = 'http://localhost:8080'
query = 'example_attr=1'
m = Modifier(query)
assert m.url_with_modifiers(url) == f'{url}?{query}'


def test_inclusion():
url = 'http://localhost:8080'
f = Inclusion('something', 'something_else')
assert f.url_with_modifiers(url) == f'{url}?include=something,something_else'


def test_modifier_sum():
url = 'http://localhost:8080'
q1 = 'item1=1'
q2 = 'item2=2'
q3 = 'item3=3'
m1 = Modifier(q1)
m2 = Modifier(q2)
m3 = Modifier(q3)

assert ((m1 + m2) + m3).url_with_modifiers(url) == f'{url}?{q1}&{q2}&{q3}'
assert (m1 + (m2 + m3)).url_with_modifiers(url) == f'{url}?{q1}&{q2}&{q3}'
assert (m1 + m2 + m3).url_with_modifiers(url) == f'{url}?{q1}&{q2}&{q3}'

0 comments on commit 6305260

Please sign in to comment.