Skip to content

Commit c8f6040

Browse files
committed
LITE-28409: Adding pagination capability
1 parent 057d365 commit c8f6040

10 files changed

+431
-9
lines changed

connect_ext_ppr/pagination.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
from fastapi import Query
2+
from fastapi_pagination import LimitOffsetPage, LimitOffsetParams, set_page
3+
from fastapi_pagination.api import resolve_params
4+
from fastapi_pagination.ext.sqlalchemy import paginate
5+
6+
7+
set_page(LimitOffsetPage)
8+
9+
10+
class PaginationParams(LimitOffsetParams):
11+
"""Here we can redefine default size value"""
12+
limit: int = Query(1000, ge=1, le=1000, description="Page size")
13+
14+
15+
def apply_pagination(query, db, params, response):
16+
"""Apply pagination for the query
17+
* Paginate query according to applied parameters (size and page)
18+
* Add pagination headers to the response
19+
Don't filter or remove elements from query after the pagination
20+
"""
21+
paginated = paginate(db, query, params)
22+
resolve_params(params)
23+
response.headers['X-Total-Count'] = str(paginated.total)
24+
init = paginated.offset
25+
# If we are selecting a page that it's out of range, we return the same start and end for on
26+
# header range, copying the same behavior as connect.
27+
end = init
28+
if paginated.items:
29+
end += len(paginated.items) - 1
30+
31+
response.headers['Content-Range'] = f'items {init}-{end}/{paginated.total}'
32+
return paginated.items

connect_ext_ppr/webapp.py

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
from connect_ext_ppr.models.ppr import PPRVersion
4949
from connect_ext_ppr.models.task import Task
5050
from connect_ext_ppr.models.replicas import Product
51+
from connect_ext_ppr.pagination import apply_pagination, PaginationParams
5152
from connect_ext_ppr.service import (
5253
add_deployments,
5354
add_new_deployment_request,
@@ -182,6 +183,8 @@ def add_dep_request(
182183
)
183184
def list_deployment_requests(
184185
self,
186+
pagination_params: PaginationParams = Depends(),
187+
response: Response = None,
185188
client: ConnectClient = Depends(get_installation_client),
186189
db: VerboseBaseSession = Depends(get_db),
187190
installation: dict = Depends(get_installation),
@@ -203,6 +206,7 @@ def list_deployment_requests(
203206
).filter(
204207
DeploymentRequest.deployment_id.in_(deployments),
205208
)
209+
deployment_requests = apply_pagination(deployment_requests, db, pagination_params, response)
206210

207211
response_list = []
208212
for dr in deployment_requests:
@@ -272,6 +276,8 @@ def list_deployment_request_tasks(
272276
def list_deployment_request_marketplaces(
273277
self,
274278
depl_req_id: str,
279+
pagination_params: PaginationParams = Depends(),
280+
response: Response = None,
275281
db: VerboseBaseSession = Depends(get_db),
276282
client: ConnectClient = Depends(get_installation_client),
277283
installation: dict = Depends(get_installation),
@@ -282,6 +288,7 @@ def list_deployment_request_marketplaces(
282288
marketplaces = db.query(MarketplaceConfiguration).options(
283289
selectinload(MarketplaceConfiguration.ppr),
284290
).filter_by(deployment_request=dr.id)
291+
marketplaces = apply_pagination(marketplaces, db, pagination_params, response)
285292

286293
marketplaces_pprs = {m.marketplace: m.ppr for m in marketplaces}
287294
marketplaces_data = get_marketplaces(client, list(marketplaces_pprs.keys()))
@@ -369,6 +376,8 @@ def get_deployment(
369376
def list_requests_for_deployment(
370377
self,
371378
deployment_id: str,
379+
pagination_params: PaginationParams = Depends(),
380+
response: Response = None,
372381
client: ConnectClient = Depends(get_installation_client),
373382
db: VerboseBaseSession = Depends(get_db),
374383
installation: dict = Depends(get_installation),
@@ -384,7 +393,8 @@ def list_requests_for_deployment(
384393
.filter_by(deployment_id=deployment_id)
385394
.order_by(desc(DeploymentRequest.id))
386395
)
387-
for dr in qs:
396+
397+
for dr in apply_pagination(qs, db, pagination_params, response):
388398
response_list.append(get_deployment_request_schema(dr, hub))
389399
return response_list
390400

@@ -396,12 +406,15 @@ def list_requests_for_deployment(
396406
def get_deployments(
397407
self,
398408
deployment_filter: DeploymentFilter = FilterDepends(DeploymentFilter),
409+
pagination_params: PaginationParams = Depends(),
410+
response: Response = None,
399411
client: ConnectClient = Depends(get_installation_client),
400412
db: VerboseBaseSession = Depends(get_db),
401413
installation: dict = Depends(get_installation),
402414
):
403415
deployments = db.query(Deployment).filter_by(account_id=installation['owner']['id'])
404416
deployments = deployment_filter.filter(deployments)
417+
deployments = apply_pagination(deployments, db, pagination_params, response)
405418
listings = get_all_listing_info(client)
406419
vendors = [li['vendor'] for li in listings]
407420
hubs = [hub['hub'] for li in listings for hub in li['contract']['marketplace']['hubs']]
@@ -422,17 +435,19 @@ def get_deployments(
422435
def get_configurations(
423436
self,
424437
deployment_id: str,
438+
pagination_params: PaginationParams = Depends(),
439+
response: Response = None,
425440
db: VerboseBaseSession = Depends(get_db),
426441
installation: dict = Depends(get_installation),
427442
):
428443
get_deployment_by_id(deployment_id, db, installation)
429444

430-
conf_file_list = (
445+
conf_file_qs = (
431446
db.query(Configuration, File)
432447
.filter_by(deployment=deployment_id)
433448
.join(File, Configuration.file == File.id)
434-
.all()
435449
)
450+
conf_file_list = apply_pagination(conf_file_qs, db, pagination_params, response)
436451
response_list = []
437452
for conf, file in conf_file_list:
438453
response_list.append(
@@ -569,20 +584,21 @@ def remove_configuration(
569584
def get_pprs(
570585
self,
571586
deployment_id: str,
587+
pagination_params: PaginationParams = Depends(),
588+
response: Response = None,
572589
db: VerboseBaseSession = Depends(get_db),
573590
installation: dict = Depends(get_installation),
574591
):
575592
get_deployment_by_id(deployment_id, db, installation)
576593

577-
ppr_file_conf_list = (
594+
ppr_file_conf_qs = (
578595
db.query(PPRVersion, File, Configuration)
579596
.filter_by(deployment=deployment_id)
580597
.join(File, PPRVersion.file == File.id)
581598
.outerjoin(Configuration, PPRVersion.configuration == Configuration.id)
582599
.order_by(desc(PPRVersion.version))
583-
.all()
584600
)
585-
601+
ppr_file_conf_list = apply_pagination(ppr_file_conf_qs, db, pagination_params, response)
586602
response_list = []
587603
for ppr, file, conf in ppr_file_conf_list:
588604
response_list.append(
@@ -642,6 +658,8 @@ def add_ppr(
642658
def get_marketplaces_by_deployment(
643659
self,
644660
deployment_id: str,
661+
pagination_params: PaginationParams = Depends(),
662+
response: Response = None,
645663
client: ConnectClient = Depends(get_installation_client),
646664
db: VerboseBaseSession = Depends(get_db),
647665
installation: dict = Depends(get_installation),
@@ -651,6 +669,7 @@ def get_marketplaces_by_deployment(
651669
mkplc_configs = db.query(MarketplaceConfiguration).options(
652670
selectinload(MarketplaceConfiguration.ppr),
653671
).filter_by(deployment_id=deployment_id, active=True)
672+
mkplc_configs = apply_pagination(mkplc_configs, db, pagination_params, response)
654673

655674
mkplc_ids = [m.marketplace for m in mkplc_configs]
656675

@@ -668,19 +687,23 @@ def get_marketplaces_by_deployment(
668687
)
669688
def list_products(
670689
self,
690+
pagination_params: PaginationParams = Depends(),
691+
response: Response = None,
671692
db: VerboseBaseSession = Depends(get_db),
672693
installation: dict = Depends(get_installation),
673694
):
674695
products_ids = db.query(Deployment.product_id).filter_by(
675696
account_id=installation['owner']['id'],
676697
).distinct()
677-
678-
response_list = []
679698
products = db.query(Product).filter(Product.id.in_(products_ids)).options(
680699
selectinload(Product.owner),
681700
)
701+
products = apply_pagination(products, db, pagination_params, response)
702+
703+
response_list = []
682704
for product in products:
683705
response_list.append(get_product_schema(product))
706+
684707
return response_list
685708

686709
@router.get(

poetry.lock

Lines changed: 33 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ pandas = "^2.0.3"
2323
openpyxl = "^3.1.2"
2424
PyJWT = "^2.8.0"
2525
fastapi_filter = "^0.6.1"
26+
fastapi-pagination = "^0.12.8"
2627

2728
[tool.poetry.dev-dependencies]
2829
pytest = ">=6.1.2,<8"

tests/api/test_configurations.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,38 @@ def test_get_configurations(
3232
assert data['state'] == ConfigurationStateChoices.inactive
3333

3434

35+
@pytest.mark.parametrize(
36+
('pagination', 'expected_amount', 'expected_header'),
37+
(
38+
('limit=10&offset=0', 10, 'items 0-9/12'),
39+
('limit=5&offset=10', 2, 'items 10-11/12'),
40+
('limit=5&offset=21', 0, 'items 21-21/12'),
41+
),
42+
)
43+
def test_get_configurations_with_pagination(
44+
pagination,
45+
expected_amount,
46+
expected_header,
47+
deployment_factory,
48+
file_factory,
49+
configuration_factory,
50+
installation,
51+
api_client,
52+
):
53+
deployment = deployment_factory(account_id=installation['owner']['id'])
54+
for i in range(12):
55+
ppr_file = file_factory(id=f'ML-{i}')
56+
configuration_factory(file=ppr_file.id, deployment=deployment.id)
57+
58+
response = api_client.get(
59+
f'/api/deployments/{deployment.id}/configurations?{pagination}',
60+
installation=installation,
61+
)
62+
assert response.status_code == 200
63+
assert len(response.json()) == expected_amount
64+
assert response.headers['Content-Range'] == expected_header
65+
66+
3567
def test_get_configurations_empty(
3668
deployment,
3769
installation,

tests/api/test_deployment_requests.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,46 @@ def test_list_deployments_requests(
6363
assert list(events['created'].keys()) == ['at', 'by']
6464

6565

66+
@pytest.mark.parametrize(
67+
('pagination', 'expected_amount', 'expected_header'),
68+
(
69+
('limit=10&offset=0', 10, 'items 0-9/20'),
70+
('limit=9&offset=18', 2, 'items 18-19/20'),
71+
('limit=5&offset=21', 0, 'items 21-21/20'),
72+
),
73+
)
74+
def test_list_deployments_requests_pagination(
75+
pagination,
76+
expected_amount,
77+
expected_header,
78+
mocker,
79+
deployment_factory,
80+
deployment_request_factory,
81+
installation,
82+
api_client,
83+
):
84+
hub_data = {
85+
'id': 'HB-0000-0000',
86+
'name': 'Another Hub for the best',
87+
}
88+
mocker.patch(
89+
'connect_ext_ppr.webapp.get_hubs',
90+
side_effect=[[hub_data]],
91+
)
92+
93+
for _ in range(20):
94+
dep1 = deployment_factory(account_id=installation['owner']['id'])
95+
deployment_request_factory(deployment=dep1)
96+
97+
response = api_client.get(
98+
f'/api/deployments/requests?{pagination}',
99+
installation=installation,
100+
)
101+
assert response.status_code == 200
102+
assert len(response.json()) == expected_amount
103+
assert response.headers['Content-Range'] == expected_header
104+
105+
66106
def test_get_deployment_request(
67107
mocker,
68108
deployment_factory,

0 commit comments

Comments
 (0)