From dacc5894360bd7682d3402a91e91e59c29ffcd16 Mon Sep 17 00:00:00 2001 From: Thearcane Date: Thu, 4 Jun 2026 01:06:11 +0530 Subject: [PATCH 1/7] feat: pagination in services and types --- .../api/20201108.e0358db.openapi.yaml | 67 ++++++++++++++-- cloud_registry/ga4gh/registry/server.py | 79 ++++++++++++++++--- 2 files changed, 131 insertions(+), 15 deletions(-) diff --git a/cloud_registry/api/20201108.e0358db.openapi.yaml b/cloud_registry/api/20201108.e0358db.openapi.yaml index 4b8b6d1..8bf11e6 100644 --- a/cloud_registry/api/20201108.e0358db.openapi.yaml +++ b/cloud_registry/api/20201108.e0358db.openapi.yaml @@ -20,15 +20,30 @@ paths: operationId: getServices tags: - services + parameters: + - name: page + in: query + required: false + schema: + type: integer + example: 1 + - name: page_size + in: query + required: false + schema: + type: integer + example: 10 responses: '200': description: 'List of services' content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/ExternalService' + oneOf: + - type: array + items: + $ref: '#/components/schemas/ExternalService' + - $ref: '#/components/schemas/PaginatedExternalService' '401': $ref: '#/components/responses/Unauthorized' '403': @@ -80,9 +95,11 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/ServiceType' + oneOf: + - type: array + items: + $ref: '#/components/schemas/ServiceType' + - $ref: '#/components/schemas/PaginatedServiceType' '401': $ref: '#/components/responses/Unauthorized' '403': @@ -198,3 +215,41 @@ components: required: - status - title + PaginationMetadata: + description: 'Pagination Metadata' + type: object + required: + - page + - page_size + - total + properties: + page: + type: integer + description: "Current page of the response" + example: 1 + page_size: + type: integer + description: "Number of entries in a page" + example: 10 + total: + type: integer + description: "Total number of entries for the query" + example: 30 + PaginatedExternalService: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/ExternalService' + pagination: + $ref: '#/components/schemas/PaginationMetadata' + PaginatedServiceType: + type: object + properties: + results: + type: array + items: + $ref: '#/components/schemas/ServiceType' + pagination: + $ref: '#/components/schemas/PaginationMetadata' diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index fac812a..cf160dd 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -1,13 +1,14 @@ """Controllers for service endpoints.""" import logging +from math import floor from typing import Dict, List, Tuple +from cloud_registry.exceptions import BadRequest, NotFound +from cloud_registry.ga4gh.registry.service import RegisterService +from cloud_registry.ga4gh.registry.service_info import RegisterServiceInfo from flask import current_app, request from foca.utils.logging import log_traffic -from cloud_registry.exceptions import NotFound, BadRequest -from cloud_registry.ga4gh.registry.service_info import RegisterServiceInfo -from cloud_registry.ga4gh.registry.service import RegisterService logger = logging.getLogger(__name__) @@ -18,18 +19,49 @@ def getServices(**kwargs) -> List: """List all services. Returns: - List of services. + List of services or Paginated list of services. """ + + # Get pagination data + page = request.args.get("page", type=int) + page_size = request.args.get("page_size", type=int) + foca_conf = current_app.config.foca # type: ignore[attr-defined] db_collection_service = ( foca_conf.db.dbs["serviceStore"].collections["services"].client ) + + # return list if no pagination query found + if page == None and page_size == None: + records = db_collection_service.find( + filter={}, + projection={"_id": False}, + ) + return list(records) + + # Return paginated response + page = page or 1 + page_size = page_size or 10 + total_count = db_collection_service.count_documents({}) + total_pages = floor(total_count / page_size) + (1 if total_count % page_size > 0 else 0) + + if page < 1 or page > total_pages: + raise BadRequest + + skip_items = (page - 1) * page_size records = db_collection_service.find( filter={}, projection={"_id": False}, - ) - return list(records) + ).skip(skip_items).limit(page_size) + return { + "results": list(records), + "pagination": { + "page": page, + "page_size": page_size, + "total": total_count, + } + } # GET /services/{serviceId} @log_traffic @@ -61,12 +93,41 @@ def getServiceTypes(**kwargs) -> List: Returns: List of distinct service types. """ + + # Get pagination data + page = request.args.get("page", type=int) + page_size = request.args.get("page_size", type=int) + services = getServices.__wrapped__() + if isinstance(services, dict): + services = services["results"] types = [s["type"] for s in services] uniq_types = [dict(t) for t in {tuple(sorted(d.items())) for d in types}] - - return uniq_types - + + # return list if no pagination query found + if page == None and page_size == None: + return uniq_types + + # return paginated response + page = page or 1 + page_size = page_size or 10 + total_count = len(uniq_types) + total_pages = (total_count // page_size) + (1 if total_count % page_size > 0 else 0) + + if page < 1 or (total_count > 0 and page > total_pages): + raise BadRequest + + skip_items = (page - 1) * page_size + paginated_types = uniq_types[skip_items : skip_items + page_size] + + return { + "results": paginated_types, + "pagination": { + "page": page, + "page_size": page_size, + "total": total_count, + } + } # GET /service-info @log_traffic From 3ebf5be6e21e0ac3f9c5ec9fc303beeeb20360a8 Mon Sep 17 00:00:00 2001 From: Thearcane Date: Thu, 4 Jun 2026 01:30:08 +0530 Subject: [PATCH 2/7] fix: minimum page and page_size validation --- cloud_registry/api/20201108.e0358db.openapi.yaml | 2 ++ cloud_registry/ga4gh/registry/server.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud_registry/api/20201108.e0358db.openapi.yaml b/cloud_registry/api/20201108.e0358db.openapi.yaml index 8bf11e6..81c0ab1 100644 --- a/cloud_registry/api/20201108.e0358db.openapi.yaml +++ b/cloud_registry/api/20201108.e0358db.openapi.yaml @@ -26,12 +26,14 @@ paths: required: false schema: type: integer + minimum: 1 example: 1 - name: page_size in: query required: false schema: type: integer + minimum: 1 example: 10 responses: '200': diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index cf160dd..8008f33 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -45,7 +45,7 @@ def getServices(**kwargs) -> List: total_count = db_collection_service.count_documents({}) total_pages = floor(total_count / page_size) + (1 if total_count % page_size > 0 else 0) - if page < 1 or page > total_pages: + if page < 1 or (total_count > 0 and page > total_pages): raise BadRequest skip_items = (page - 1) * page_size @@ -112,7 +112,7 @@ def getServiceTypes(**kwargs) -> List: page = page or 1 page_size = page_size or 10 total_count = len(uniq_types) - total_pages = (total_count // page_size) + (1 if total_count % page_size > 0 else 0) + total_pages = floor(total_count / page_size) + (1 if total_count % page_size > 0 else 0) if page < 1 or (total_count > 0 and page > total_pages): raise BadRequest From aa681e99cbd230e2dc2f7e8950092edbac83df79 Mon Sep 17 00:00:00 2001 From: Thearcane Date: Thu, 4 Jun 2026 01:51:18 +0530 Subject: [PATCH 3/7] refactor: updated pages calculation --- cloud_registry/api/20201108.e0358db.openapi.yaml | 9 +++++++-- cloud_registry/ga4gh/registry/server.py | 12 +++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/cloud_registry/api/20201108.e0358db.openapi.yaml b/cloud_registry/api/20201108.e0358db.openapi.yaml index 81c0ab1..6d0d091 100644 --- a/cloud_registry/api/20201108.e0358db.openapi.yaml +++ b/cloud_registry/api/20201108.e0358db.openapi.yaml @@ -223,7 +223,8 @@ components: required: - page - page_size - - total + - total_count + - total_pages properties: page: type: integer @@ -233,7 +234,11 @@ components: type: integer description: "Number of entries in a page" example: 10 - total: + total_count: + type: integer + description: "Total number of entries for the query" + example: 30 + total_pages: type: integer description: "Total number of entries for the query" example: 30 diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index 8008f33..5ad9c68 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -1,7 +1,7 @@ """Controllers for service endpoints.""" import logging -from math import floor +from math import ceil from typing import Dict, List, Tuple from cloud_registry.exceptions import BadRequest, NotFound @@ -43,7 +43,7 @@ def getServices(**kwargs) -> List: page = page or 1 page_size = page_size or 10 total_count = db_collection_service.count_documents({}) - total_pages = floor(total_count / page_size) + (1 if total_count % page_size > 0 else 0) + total_pages = ceil(total_count / page_size) if page < 1 or (total_count > 0 and page > total_pages): raise BadRequest @@ -59,7 +59,8 @@ def getServices(**kwargs) -> List: "pagination": { "page": page, "page_size": page_size, - "total": total_count, + "total_count": total_count, + "total_pages": total_pages } } @@ -112,7 +113,7 @@ def getServiceTypes(**kwargs) -> List: page = page or 1 page_size = page_size or 10 total_count = len(uniq_types) - total_pages = floor(total_count / page_size) + (1 if total_count % page_size > 0 else 0) + total_pages = ceil(total_count / page_size) if page < 1 or (total_count > 0 and page > total_pages): raise BadRequest @@ -125,7 +126,8 @@ def getServiceTypes(**kwargs) -> List: "pagination": { "page": page, "page_size": page_size, - "total": total_count, + "total_count": total_count, + "total_pages": total_pages } } From e8de4861a3aa3b5609126485c8c5bce06e002c7c Mon Sep 17 00:00:00 2001 From: Thearcane Date: Fri, 5 Jun 2026 22:30:33 +0530 Subject: [PATCH 4/7] chore: lint and tests updated --- cloud_registry/ga4gh/registry/server.py | 26 +++++++++++++------------ tests/ga4gh/registry/test_server.py | 14 ++++++++++--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index 5ad9c68..d27e318 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -30,15 +30,15 @@ def getServices(**kwargs) -> List: db_collection_service = ( foca_conf.db.dbs["serviceStore"].collections["services"].client ) - + # return list if no pagination query found - if page == None and page_size == None: + if page is None and page_size is None: records = db_collection_service.find( filter={}, projection={"_id": False}, ) return list(records) - + # Return paginated response page = page or 1 page_size = page_size or 10 @@ -47,7 +47,7 @@ def getServices(**kwargs) -> List: if page < 1 or (total_count > 0 and page > total_pages): raise BadRequest - + skip_items = (page - 1) * page_size records = db_collection_service.find( filter={}, @@ -64,6 +64,7 @@ def getServices(**kwargs) -> List: } } + # GET /services/{serviceId} @log_traffic def getServiceById(serviceId: str, **kwargs) -> Dict: @@ -94,21 +95,21 @@ def getServiceTypes(**kwargs) -> List: Returns: List of distinct service types. """ - + # Get pagination data page = request.args.get("page", type=int) page_size = request.args.get("page_size", type=int) - + services = getServices.__wrapped__() if isinstance(services, dict): services = services["results"] types = [s["type"] for s in services] uniq_types = [dict(t) for t in {tuple(sorted(d.items())) for d in types}] - + # return list if no pagination query found - if page == None and page_size == None: + if page is None and page_size is None: return uniq_types - + # return paginated response page = page or 1 page_size = page_size or 10 @@ -117,10 +118,10 @@ def getServiceTypes(**kwargs) -> List: if page < 1 or (total_count > 0 and page > total_pages): raise BadRequest - + skip_items = (page - 1) * page_size - paginated_types = uniq_types[skip_items : skip_items + page_size] - + paginated_types = uniq_types[skip_items:skip_items + page_size] + return { "results": paginated_types, "pagination": { @@ -131,6 +132,7 @@ def getServiceTypes(**kwargs) -> List: } } + # GET /service-info @log_traffic def getServiceInfo(**kwargs) -> Dict: diff --git a/tests/ga4gh/registry/test_server.py b/tests/ga4gh/registry/test_server.py index 5d65364..97f5371 100644 --- a/tests/ga4gh/registry/test_server.py +++ b/tests/ga4gh/registry/test_server.py @@ -55,8 +55,11 @@ def test_getServices(): data.append(mock_resp) # check whether getServices returns the same list - with app.app_context(): + with app.test_request_context("services"): res = getServices.__wrapped__() + # For paginated response extract the data present in result field + if "results" in res: + res = res["results"] assert res == data @@ -111,8 +114,10 @@ def test_getServiceTypes_duplicates(): "services" ].client.insert_one(mock_resp) - with app.app_context(): + with app.test_request_context("services"): res = getServiceTypes.__wrapped__() + if "results" in res: + res = res["results"] # All written services have same type, we expect list of length 1 assert res == [MOCK_TYPE] @@ -139,8 +144,11 @@ def test_getServiceTypes_distinct(): "services" ].client.insert_one(mock_resp) - with app.app_context(): + with app.test_request_context("services"): res = getServiceTypes.__wrapped__() + # For paginated response extract the data present in result field + if "results" in res: + res = res["results"] # All written services have distinct types, we expect a list of the # same length as there are entries in the database collection assert len(res) == len(services) From abf7bb54b5a187a6d2756ae0259905b92755f625 Mon Sep 17 00:00:00 2001 From: Thearcane Date: Fri, 5 Jun 2026 22:45:58 +0530 Subject: [PATCH 5/7] fix: updated typing --- cloud_registry/ga4gh/registry/server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index d27e318..920c42b 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -2,7 +2,7 @@ import logging from math import ceil -from typing import Dict, List, Tuple +from typing import Dict, List, Tuple, Union from cloud_registry.exceptions import BadRequest, NotFound from cloud_registry.ga4gh.registry.service import RegisterService @@ -15,7 +15,7 @@ # GET /services @log_traffic -def getServices(**kwargs) -> List: +def getServices(**kwargs) -> Union[List, Dict]: """List all services. Returns: @@ -89,7 +89,7 @@ def getServiceById(serviceId: str, **kwargs) -> Dict: # GET /services/types @log_traffic -def getServiceTypes(**kwargs) -> List: +def getServiceTypes(**kwargs) -> Union[List, Dict]: """List types of services. Returns: From 12cc38e2846e2d2d4350b6827045e546717c9235 Mon Sep 17 00:00:00 2001 From: Thearcane Date: Fri, 5 Jun 2026 23:01:28 +0530 Subject: [PATCH 6/7] chore: formatter --- cloud_registry/ga4gh/registry/server.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index 920c42b..505a172 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -49,10 +49,14 @@ def getServices(**kwargs) -> Union[List, Dict]: raise BadRequest skip_items = (page - 1) * page_size - records = db_collection_service.find( - filter={}, - projection={"_id": False}, - ).skip(skip_items).limit(page_size) + records = ( + db_collection_service.find( + filter={}, + projection={"_id": False}, + ) + .skip(skip_items) + .limit(page_size) + ) return { "results": list(records), @@ -60,8 +64,8 @@ def getServices(**kwargs) -> Union[List, Dict]: "page": page, "page_size": page_size, "total_count": total_count, - "total_pages": total_pages - } + "total_pages": total_pages, + }, } @@ -120,7 +124,8 @@ def getServiceTypes(**kwargs) -> Union[List, Dict]: raise BadRequest skip_items = (page - 1) * page_size - paginated_types = uniq_types[skip_items:skip_items + page_size] + end = skip_items + page_size + paginated_types = uniq_types[skip_items:end] return { "results": paginated_types, @@ -128,8 +133,8 @@ def getServiceTypes(**kwargs) -> Union[List, Dict]: "page": page, "page_size": page_size, "total_count": total_count, - "total_pages": total_pages - } + "total_pages": total_pages, + }, } From 81ba31054f0bd5a520ce81d8ba000e4c0c727f70 Mon Sep 17 00:00:00 2001 From: Thearcane Date: Mon, 8 Jun 2026 23:21:40 +0530 Subject: [PATCH 7/7] fix: inconsistent results in service-types --- cloud_registry/ga4gh/registry/server.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/cloud_registry/ga4gh/registry/server.py b/cloud_registry/ga4gh/registry/server.py index 505a172..b314d57 100644 --- a/cloud_registry/ga4gh/registry/server.py +++ b/cloud_registry/ga4gh/registry/server.py @@ -104,9 +104,17 @@ def getServiceTypes(**kwargs) -> Union[List, Dict]: page = request.args.get("page", type=int) page_size = request.args.get("page_size", type=int) - services = getServices.__wrapped__() - if isinstance(services, dict): - services = services["results"] + # get all the services + foca_conf = current_app.config.foca # type: ignore[attr-defined] + db_collection_service = ( + foca_conf.db.dbs["serviceStore"].collections["services"].client + ) + services = list( + db_collection_service.find( + filter={}, + projection={"_id": False}, + ) + ) types = [s["type"] for s in services] uniq_types = [dict(t) for t in {tuple(sorted(d.items())) for d in types}]