8000 Adds django-style shorthand to filter, sort, and paginate · scuml/server-client-python@e4ab19b · GitHub
[go: up one dir, main page]

Skip to content

Commit e4ab19b

Browse files
committed
Adds django-style shorthand to filter, sort, and paginate
1 parent 188be71 commit e4ab19b

File tree

7 files changed

+204
-8
lines changed

7 files changed

+204
-8
lines changed

.gitignore

10000
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,10 @@ target/
7676
# pyenv
7777
.python-version
7878

79+
# poetry
80+
poetry.lock
81+
pyproject.toml
82+
7983
# celery beat schedule file
8084
celerybeat-schedule
8185

tableauserverclient/server/endpoint/datasources_endpoint.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
from .endpoint import Endpoint, api, parameter_added_in
22
from .exceptions import InternalServerError, MissingRequiredFieldError
3-
from .endpoint import api, parameter_added_in, Endpoint
43
from .permissions_endpoint import _PermissionsEndpoint
5-
from .exceptions import MissingRequiredFieldError
64
from .fileuploads_endpoint import Fileuploads
75
from .resource_tagger import _ResourceTagger
86
from .. import RequestFactory, DatasourceItem, PaginationItem, ConnectionItem
7+
from ..query import QuerySet
98
from ...filesys_helpers import to_filename, make_download_path
10-
from ...models.tag_item import TagItem
119
from ...models.job_item import JobItem
1210
import os
1311
import logging
@@ -54,6 +52,21 @@ def get_by_id(self, datasource_id):
5452
server_response = self.get_request(url)
5553
return DatasourceItem.from_response(server_response.content, self.parent_srv.namespace)[0]
5654

55+
@api(version="2.0")
56+
def filter(self, *args, **kwargs):
57+
queryset = QuerySet(self).filter(**kwargs)
58+
return queryset
59+
60+
@api(version="2.0")
61+
def order_by(self, *args, **kwargs):
62+
queryset = QuerySet(self).order_by(*args)
63+
return queryset
64+
65+
@api(version="2.0")
66+
def paginate(self, **kwargs):
67+
queryset = QuerySet(self).paginate(**kwargs)
68+
return queryset
69+
5770
# Populate datasource item's connections
5871
@api(version="2.0")
5972
def populate_connections(self, datasource_item):

tableauserverclient/server/endpoint/users_endpoint.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from .exceptions import MissingRequiredFieldError
33
from .. import RequestFactory, UserItem, WorkbookItem, PaginationItem
44
from ..pager import Pager
5+
from ..query import QuerySet
56
import logging
67
import copy
78

@@ -34,6 +35,21 @@ def get_by_id(self, user_id):
3435
server_response = self.get_request(url)
3536
return UserItem.from_response(server_response.content, self.parent_srv.namespace).pop()
3637

38+
@api(version="2.0")
39+
def filter(self, *args, **kwargs):
40+
queryset = QuerySet(self).filter(**kwargs)
41+
return queryset
42+
43+
@api(version="2.0")
44+
def order_by(self, *args, **kwargs):
45+
queryset = QuerySet(self).order_by(*args)
46+
return queryset
47+
48+
@api(version="2.0")
49+
def paginate(self, **kwargs):
50+
queryset = QuerySet(self).paginate(**kwargs)
51+
return queryset
52+
3753
# Update user
3854
@api(version="2.0")
3955
def update(self, user_item, password=None):

tableauserverclient/server/endpoint/views_endpoint.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
from .exceptions import MissingRequiredFieldError
33
from .resource_tagger import _ResourceTagger
44
from .permissions_endpoint import _PermissionsEndpoint
5-
from .. import RequestFactory, ViewItem, PaginationItem
6-
from ...models.tag_item import TagItem
5+
from .. import ViewItem, PaginationItem
6+
from ..query import QuerySet
77
import logging
88
from contextlib import closing
99

@@ -36,6 +36,21 @@ def get(self, req_options=None, usage=False):
3636
all_view_items = ViewItem.from_response(server_response.content, self.parent_srv.namespace)
3737
return all_view_items, pagination_item
3838

39+
@api(version="2.0")
40+
def filter(self, *args, **kwargs):
41+
queryset = QuerySet(self).filter(**kwargs)
42+
return queryset
43+
44+
@api(version="2.0")
45+
def order_by(self, *args, **kwargs):
46+
queryset = QuerySet(self).order_by(*args)
47+
return queryset
48+
49+
@api(version="2.0")
50+
def paginate(self, **kwargs):
51+
queryset = QuerySet(self).paginate(**kwargs)
52+
return queryset
53+
3954
@api(version="2.0")
4055
def populate_preview_image(self, view_item):
4156
if not view_item.id or not view_item.workbook_id:

tableauserverclient/server/endpoint/workbooks_endpoint.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
from .endpoint import Endpoint, api, parameter_added_in
22
from .exceptions import InternalServerError, MissingRequiredFieldError
33
from .permissions_endpoint import _PermissionsEndpoint
4-
from .exceptions import MissingRequiredFieldError
54
from .fileuploads_endpoint import Fileuploads
65
from .resource_tagger import _ResourceTagger
76
from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem
7+
from ..query import QuerySet
88
from ...models.tag_item import TagItem
99
from ...models.job_item import JobItem
1010
from ...filesys_helpers import to_filename, make_download_path
@@ -39,8 +39,10 @@ def get(self, req_options=None):
3939
logger.info('Querying all workbooks on site')
4040
url = self.baseurl
4141
server_response = self.get_request(url, req_options)
42-
pagination_item = PaginationItem.from_response(server_response.content, self.parent_srv.namespace)
43-
all_workbook_items = WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)
42+
pagination_item = PaginationItem.from_response(
43+
server_response.content, self.parent_srv.namespace)
44+
all_workbook_items = WorkbookItem.from_response(
45+
server_response.content, self.parent_srv.namespace)
4446
return all_workbook_items, pagination_item
4547

4648
# Get 1 workbook
@@ -54,6 +56,21 @@ def get_by_id(self, workbook_id):
5456
server_response = self.get_request(url)
5557
return WorkbookItem.from_response(server_response.content, self.parent_srv.namespace)[0]
5658

59+
@api(version="2.0")
60+
def filter(self, *args, **kwargs):
61+
queryset = QuerySet(self).filter(**kwargs)
62+
return queryset
63+
64+
@api(version="2.0")
65+
def order_by(self, *args, **kwargs):
66+
queryset = QuerySet(self).order_by(*args)
67+
return queryset
68+
69+
@api(version="2.0")
70+
def paginate(self, **kwargs):
71+
queryset = QuerySet(self).paginate(**kwargs)
72+
return queryset
73+
5774
@api(version="2.8")
5875
def refresh(self, workbook_id):
5976
id_ = getattr(workbook_id, 'id', workbook_id)

tableauserverclient/server/query.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
from .request_options import RequestOptions
2+
from .filter import Filter
3+
from .sort import Sort
4+
5+
6+
def to_camel_case(word):
7+
return word.split('_')[0] + ''.join(x.capitalize() or '_' for x in word.split('_')[1:])
8+
9+
class QuerySet:
10+
11+
def __init__(self, model):
12+
self.model = model
13+
self.request_options = RequestOptions()
14+
self._result_cache = None
15+
self._pagination_item = None
16+
17+
def __iter__(self):
18+
self._fetch_all()
19+
return iter(self._result_cache)
20+
21+
def __getitem__(self, k):
22+
return list(self)[k]
23+
24+
def _fetch_all(self):
25+
"""
26+
Retrieve the data and store result and pagination item in cache
27+
"""
28+
if self._result_cache is None:
29+
self._result_cache, self._pagination_item = self.model.get(self.request_options)
30+
31+
@property
32+
def < 2851 span class=pl-en>total_available(self):
33+
self._fetch_all()
34+
return self._pagination_item.total_available
35+
36+
@property
37+
def page_number(self):
38+
self._fetch_all()
39+
return se F438 lf._pagination_item.page_number
40+
41+
@property
42+
def page_size(self):
43+
self._fetch_all()
44+
return self._pagination_item.page_size
45+
46+
def filter(self, **kwargs):
47+
for kwarg_key, value in kwargs.items():
48+
field_name, operator = self._parse_shorthand_filter(kwarg_key)
49+
self.request_options.filter.add(Filter(field_name, operator, value))
50+
return self
51+
52+
def order_by(self, *args):
53+
for arg in args:
54+
field_name, direction = self._parse_shorthand_sort(arg)
55+
self.request_options.sort.add(Sort(field_name, direction))
56+
return self
57+
58+
def paginate(self, **kwargs):
59+
if "page_number" in kwargs:
60+
self.request_options.pagenumber = kwargs["page_number"]
61+
if "page_size" in kwargs:
62+
self.request_options.pagesize = kwargs["page_size"]
63+
return self
64+
65+
def _parse_shorthand_filter(self, key):
66+
tokens = key.split("__", 1)
67+
if len(tokens) == 1:
68+
operator = RequestOptions.Operator.Equals
69+
else:
70+
operator = tokens[1]
71+
if operator not in RequestOptions.Operator.__dict__.values():
72+
raise ValueError("Operator `{}` is not valid.".format(operator))
73+
74+
field = to_camel_case(tokens[0])
75+
if field not in RequestOptions.Field.__dict__.values():
76+
raise ValueError("Field name `{}` is not valid.".format(field))
77+
return (field, operator)
78+
79+
def _parse_shorthand_sort(self, key):
80+
direction = RequestOptions.Direction.Asc
81+
if key.startswith("-"):
82+
direction = RequestOptions.Direction.Desc
83+
key = key[1:]
84+
85+
key = to_camel_case(key)
86+
if key not in RequestOptions.Field.__dict__.values():
87+
raise ValueError("Sort key name %s is not valid.", key)
88+
return (key, direction)

test/test_request_option.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,17 @@ def test_filter_equals(self):
7676
self.assertEqual('RESTAPISample', matching_workbooks[0].name)
7777
self.assertEqual('RESTAPISample', matching_workbooks[1].name)
7878

79+
def test_filter_equals_shorthand(self):
80+
with open(FILTER_EQUALS, 'rb') as f:
81+
response_xml = f.read().decode('utf-8')
82+
with requests_mock.mock() as m:
83+
m.get(self.baseurl + '/workbooks?filter=name:eq:RESTAPISample', text=response_xml)
84+
matching_workbooks = self.server.workbooks.filter(name='RESTAPISample').order_by("name")
85+
86+
self.assertEqual(2, matching_workbooks.total_available)
87+
self.assertEqual('RESTAPISample', matching_workbooks[0].name)
88+
self.assertEqual('RESTAPISample', matching_workbooks[1].name)
89+
7990
def test_filter_tags_in(self):
8091
with open(FILTER_TAGS_IN, 'rb') as f:
8192
response_xml = f.read().decode('utf-8')
@@ -91,6 +102,22 @@ def test_filter_tags_in(self):
91102
self.assertEqual(set(['safari']), matching_workbooks[1].tags)
92103
self.assertEqual(set(['sample']), matching_workbooks[2].tags)
93104

105+
def test_filter_tags_in_shorthand(self):
106+
with open(FILTER_TAGS_IN, 'rb') as f:
107+
response_xml = f.read().decode('utf-8')
108+
with requests_mock.mock() as m:
109+
m.get(self.baseurl + '/workbooks?filter=tags:in:[sample,safari,weather]', text=response_xml)
110+
matching_workbooks = self.server.workbooks.filter(tags__in=['sample', 'safari', 'weather'])
111+
112+
self.assertEqual(3, matching_workbooks.total_available)
113+
self.assertEqual(set(['weather']), matching_workbooks[0].tags)
114+
self.assertEqual(set(['safari']), matching_workbooks[1].tags)
115+
self.assertEqual(set(['sample']), matching_workbooks[2].tags)
116+
117+
def test_invalid_shorthand_option(self):
118+
with self.assertRaises(ValueError):
119+
self.server.workbooks.filter(nonexistant__in=['sample', 'safari'])
120+
94121
def test_multiple_filter_options(self):
95122
with open(FILTER_MULTIPLE, 'rb') as f:
96123
response_xml = f.read().decode('utf-8')
@@ -107,3 +134,19 @@ def test_multiple_filter_options(self):
107134
for _ in range(100):
108135
matching_workbooks, pagination_item = self.server.workbooks.get(req_option)
109136
self.assertEqual(3, pagination_item.total_available)
137+
138+
def test_multiple_filter_options_shorthand(self):
139+
with open(FILTER_MULTIPLE, 'rb') as f:
140+
response_xml = f.read().decode('utf-8')
141+
# To ensure that this is deterministic, run this a few times
142+
with requests_mock.mock() as m:
143+
# Sometimes pep8 requires you to do things you might not otherwise do
144+
url = ''.join((self.baseurl, '/workbooks?pageNumber=1&pageSize=100&',
145+
'filter=name:eq:foo,tags:in:[sample,safari,weather]'))
146+
m.get(url, text=response_xml)
147+
148+
for _ in range(100):
149+
matching_workbooks = self.server.workbooks.filter(
150+
tags__in=['sample', 'safari', 'weather'], name='foo'
151+
)
152+
self.assertEqual(3, matching_workbooks.total_available)

0 commit comments

Comments
 (0)
0