Skip to content

Commit 79e4841

Browse files
authored
Merge pull request #395 from HTTP-APIs/develop
2 parents b8b155a + d56108d commit 79e4841

File tree

7 files changed

+226
-32
lines changed

7 files changed

+226
-32
lines changed

cli.py

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
from hydrus.app_factory import app_factory
55
from hydrus.utils import (set_session, set_doc, set_hydrus_server_url,
6-
set_token, set_api_name, set_authentication)
6+
set_token, set_api_name, set_authentication,
7+
set_page_size, set_pagination)
78
from hydrus.data import doc_parse
89
from hydra_python_core import doc_maker
910
from hydrus.data.db_models import Base
@@ -33,13 +34,17 @@
3334
type=str)
3435
@click.option("--port", "-p", default=8080,
3536
help="The port the app is hosted at.", type=int)
37+
@click.option("--pagesize", "-ps", default=10,
38+
help="Maximum size of a page(view)", type=int)
39+
@click.option("--pagination/--no-pagination", default=True,
40+
help="Enable or disable pagination.")
3641
@click.option("--token/--no-token", default=True,
3742
help="Toggle token based user authentication.")
3843
@click.option("--serverurl", default="http://localhost",
3944
help="Set server url", type=str)
4045
@click.argument("serve", required=True)
41-
def startserver(adduser: Tuple, api: str, auth: bool, dburl: str,
42-
hydradoc: str, port: int, serverurl: str, token: bool,
46+
def startserver(adduser: Tuple, api: str, auth: bool, dburl: str, pagination: bool,
47+
hydradoc: str, port: int, pagesize: int, serverurl: str, token: bool,
4348
serve: None) -> None:
4449
"""
4550
Python Hydrus CLI
@@ -165,17 +170,21 @@ def startserver(adduser: Tuple, api: str, auth: bool, dburl: str,
165170
with set_hydrus_server_url(app, HYDRUS_SERVER_URL):
166171
# Set the Database session
167172
with set_session(app, session):
168-
# Start the Hydrus app
169-
http_server = WSGIServer(('', port), app)
170-
click.echo("Server running at:")
171-
click.echo(
172-
"{}{}".format(
173-
HYDRUS_SERVER_URL,
174-
API_NAME))
175-
try:
176-
http_server.serve_forever()
177-
except KeyboardInterrupt:
178-
pass
173+
# Enable/disable pagination
174+
with set_pagination(app, pagination):
175+
# Set page size of a collection view
176+
with set_page_size(app, pagesize):
177+
# Start the hydrus app
178+
http_server = WSGIServer(('', port), app)
179+
click.echo("Server running at:")
180+
click.echo(
181+
"{}{}".format(
182+
HYDRUS_SERVER_URL,
183+
API_NAME))
184+
try:
185+
http_server.serve_forever()
186+
except KeyboardInterrupt:
187+
pass
179188

180189

181190
if __name__ == "__main__":

hydrus/data/crud.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@
4343
PropertyNotFound,
4444
NotInstanceProperty,
4545
NotAbstractProperty,
46-
InstanceNotFound)
46+
InstanceNotFound,
47+
PageNotFound)
4748
# from sqlalchemy.orm.session import Session
4849
from sqlalchemy.orm.scoping import scoped_session
4950
from typing import Dict, Optional, Any, List
@@ -514,19 +515,30 @@ def update(id_: str,
514515
def get_collection(API_NAME: str,
515516
type_: str,
516517
session: scoped_session,
517-
path: str = None) -> Dict[str,
518-
Any]:
518+
paginate: bool,
519+
page_size: int,
520+
page: int = 1,
521+
path: str = None) -> Dict[str, Any]:
519522
"""Retrieve a type of collection from the database.
520523
:param API_NAME: api name specified while starting server
521524
:param type_: type of object to be updated
522525
:param session: sqlalchemy scoped session
526+
:param paginate: Enable/disable pagination
527+
:param page_size: Number maximum elements showed in a page
528+
:param page: page number
523529
:param path: endpoint
524-
:return: response containing all the objects of that particular type_
530+
:return: response containing a page of the objects of that particular type_
525531
526532
Raises:
527-
ClassNotFound: If `type_` does not represt a valid/defined RDFClass.
533+
ClassNotFound: If `type_` does not represent a valid/defined RDFClass.
528534
529535
"""
536+
# Check for valid page value
537+
try:
538+
page = int(page)
539+
except ValueError:
540+
raise PageNotFound(page)
541+
530542
if path is not None:
531543
collection_template = {
532544
"@id": "/{}/{}/".format(API_NAME, path),
@@ -548,8 +560,13 @@ def get_collection(API_NAME: str,
548560
raise ClassNotFound(type_=type_)
549561

550562
try:
551-
instances = session.query(Instance).filter(
552-
Instance.type_ == rdf_class.id).all()
563+
if paginate is True:
564+
offset = (page - 1) * page_size
565+
instances = session.query(Instance).filter(
566+
Instance.type_ == rdf_class.id).limit(page_size).offset(offset)
567+
else:
568+
instances = session.query(Instance).filter(
569+
Instance.type_ == rdf_class.id).all()
553570
except NoResultFound:
554571
instances = list()
555572

@@ -563,6 +580,37 @@ def get_collection(API_NAME: str,
563580
object_template = {
564581
"@id": "/{}/{}Collection/{}".format(API_NAME, type_, instance_.id), "@type": type_}
565582
collection_template["members"].append(object_template)
583+
584+
# If pagination is disabled then stop and return the collection template
585+
if paginate is False:
586+
return collection_template
587+
588+
number_of_instances = len(collection_template["members"])
589+
# If we are on the first page and there are fewer elements than the
590+
# page size then there is no need to make an extra DB call to get count
591+
if page == 1 and number_of_instances < page_size:
592+
total_items = number_of_instances
593+
else:
594+
total_items = session.query(Instance).filter(
595+
Instance.type_ == rdf_class.id).count()
596+
collection_template["totalItems"] = total_items
597+
# Calculate last page number
598+
if total_items != 0 and total_items % page_size == 0:
599+
last = total_items // page_size
600+
else:
601+
last = total_items // page_size + 1
602+
if page < 1 or page > last:
603+
raise PageNotFound(str(page))
604+
collection_template["view"] = {
605+
"@id": "/{}/{}?page={}".format(API_NAME, path, page),
606+
"@type": "PartialCollectionView",
607+
"first": "/{}/{}?page=1".format(API_NAME, path),
608+
"last": "/{}/{}?page={}".format(API_NAME, path, last)
609+
}
610+
if page != 1:
611+
collection_template["view"]["previous"] = "/{}/{}?page={}".format(API_NAME, path, page-1)
612+
if page != last:
613+
collection_template["view"]["next"] = "/{}/{}?page={}".format(API_NAME, path, page + 1)
566614
return collection_template
567615

568616

hydrus/data/exceptions.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,16 @@ def get_HTTP(self) -> Tuple[int, Dict[str, str]]:
114114
"""Return the HTTP response for the Exception."""
115115
return 400, {
116116
"message": "The User with ID {} is not a valid/defined User".format(self.id_)}
117+
118+
119+
class PageNotFound(Exception):
120+
"""Error when the User is not found."""
121+
122+
def __init__(self, page_id: str) -> None:
123+
"""Constructor."""
124+
self.page_id = page_id
125+
126+
def get_HTTP(self) -> Tuple[int, Dict[str, str]]:
127+
"""Return the HTTP response for the Exception."""
128+
return 400, {
129+
"message": "The page with ID {} not found".format(self.page_id)}

hydrus/resources.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@
3939
ClassNotFound,
4040
InstanceExists,
4141
PropertyNotFound,
42-
InstanceNotFound)
42+
InstanceNotFound,
43+
PageNotFound)
4344
from hydrus.helpers import (
4445
set_response_headers,
4546
checkClassOp,
@@ -52,7 +53,12 @@
5253
check_read_only_props,
5354
check_required_props,
5455
finalize_response)
55-
from hydrus.utils import get_session, get_doc, get_api_name, get_hydrus_server_url
56+
from hydrus.utils import (get_session,
57+
get_doc,
58+
get_api_name,
59+
get_hydrus_server_url,
60+
get_page_size,
61+
get_pagination)
5662

5763

5864
class Index(Resource):
@@ -226,6 +232,7 @@ def get(self, path: str) -> Response:
226232
"""
227233
Retrieve a collection of items from the database.
228234
"""
235+
page = request.args.get('page')
229236
auth_response = check_authentication_response()
230237
if isinstance(auth_response, Response):
231238
return auth_response
@@ -239,11 +246,24 @@ def get(self, path: str) -> Response:
239246
collection = get_doc().collections[path]["collection"]
240247
try:
241248
# Get collection details from the database
242-
response = crud.get_collection(
243-
get_api_name(), collection.class_.title, session=get_session(), path=path)
249+
if get_pagination():
250+
# Get paginated response
251+
if page is None:
252+
response = crud.get_collection(
253+
get_api_name(), collection.class_.title, session=get_session(),
254+
paginate=True, page_size=get_page_size(), path=path)
255+
else:
256+
response = crud.get_collection(
257+
get_api_name(), collection.class_.title, session=get_session(),
258+
paginate=True, page=page, page_size=get_page_size(), path=path)
259+
else:
260+
# Get whole collection
261+
response = crud.get_collection(
262+
get_api_name(), collection.class_.title, session=get_session(),
263+
paginate=False, path=path)
244264
return set_response_headers(jsonify(hydrafy(response, path=path)))
245265

246-
except ClassNotFound as e:
266+
except (ClassNotFound, PageNotFound) as e:
247267
status_code, message = e.get_HTTP()
248268
return set_response_headers(jsonify(message), status_code=status_code)
249269

hydrus/tests/test_app.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import re
77
import uuid
88
from hydrus.app_factory import app_factory
9-
from hydrus.utils import set_session, set_doc, set_api_name
9+
from hydrus.utils import set_session, set_doc, set_api_name, set_page_size
1010
from hydrus.data import doc_parse, crud
1111
from hydra_python_core import doc_maker
1212
from hydrus.samples import doc_writer_sample
@@ -44,6 +44,7 @@ def setUpClass(self):
4444

4545
self.session = session
4646
self.API_NAME = "demoapi"
47+
self.page_size = 1
4748
self.HYDRUS_SERVER_URL = "http://hydrus.com/"
4849

4950
self.app = app_factory(self.API_NAME)
@@ -60,10 +61,11 @@ def setUpClass(self):
6061

6162
print("Classes and properties added successfully.")
6263

63-
print("Setting up Hydrus utilities... ")
64+
print("Setting up hydrus utilities... ")
6465
self.api_name_util = set_api_name(self.app, self.API_NAME)
6566
self.session_util = set_session(self.app, self.session)
6667
self.doc_util = set_doc(self.app, self.doc)
68+
self.page_size_util = set_page_size(self.app, self.page_size)
6769
self.client = self.app.test_client()
6870

6971
print("Creating utilities context... ")
@@ -85,8 +87,14 @@ def tearDownClass(self):
8587

8688
def setUp(self):
8789
for class_ in self.doc.parsed_classes:
88-
if class_ not in self.doc.collections:
89-
dummy_obj = gen_dummy_object(class_, self.doc)
90+
dummy_obj = gen_dummy_object(class_, self.doc)
91+
crud.insert(
92+
dummy_obj,
93+
id_=str(
94+
uuid.uuid4()),
95+
session=self.session)
96+
# If it's a collection class then add an extra object so we can test pagination thoroughly.
97+
if class_ in self.doc.collections:
9098
crud.insert(
9199
dummy_obj,
92100
id_=str(
@@ -166,6 +174,30 @@ def test_Collections_GET(self):
166174
assert "@type" in response_get_data
167175
assert "members" in response_get_data
168176

177+
def test_pagination(self):
178+
"""Test basic pagination"""
179+
index = self.client.get("/{}".format(self.API_NAME))
180+
assert index.status_code == 200
181+
endpoints = json.loads(index.data.decode('utf-8'))
182+
for endpoint in endpoints:
183+
collection_name = "/".join(endpoints[endpoint].split(
184+
"/{}/".format(self.API_NAME))[1:])
185+
if collection_name in self.doc.collections:
186+
response_get = self.client.get(endpoints[endpoint])
187+
assert response_get.status_code == 200
188+
response_get_data = json.loads(
189+
response_get.data.decode('utf-8'))
190+
assert "view" in response_get_data
191+
assert "first" in response_get_data["view"]
192+
assert "last" in response_get_data["view"]
193+
if "next" in response_get_data["view"]:
194+
response_next = self.client.get(response_get_data["view"]["next"])
195+
assert response_next.status_code == 200
196+
response_next_data = json.loads(
197+
response_next.data.decode('utf-8'))
198+
assert "previous" in response_next_data["view"]
199+
break
200+
169201
def test_Collections_PUT(self):
170202
"""Test insert data to the collection."""
171203
index = self.client.get("/{}".format(self.API_NAME))

0 commit comments

Comments
 (0)