Skip to content

Commit 8b7cb35

Browse files
authored
feat: STAC style pagination (#70)
* fix: add err msg when next & prev url does not generate + update makefile for local * feat: update to STAC specific schema ref from CMR * feat: make pagination class
1 parent 8f7d6ed commit 8b7cb35

File tree

5 files changed

+121
-50
lines changed

5 files changed

+121
-50
lines changed

ci.cd/Makefile

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ export VERSION ?= latest
44

55

66
all: build_lambda upload_lambda update_lambda_function build_docker
7-
7+
local: build_lambda upload_lambda update_lambda_function_1 update_lambda_function_2 update_lambda_function_3
88
build_docker:
99
docker build -t "$(IMAGE_PREFIX)/$(NAME):$(VERSION)" -f docker/Dockerfile .
1010

@@ -19,6 +19,9 @@ build_lambda_public:
1919

2020
upload_lambda:
2121
aws --profile saml-pub s3 cp cumulus_lambda_functions_deployment.zip s3://am-uds-dev-cumulus-tf-state/unity_cumulus_lambda/
22-
23-
update_lambda_function:
24-
aws --profile saml-pub lambda update-function-code --s3-key unity_cumulus_lambda/cumulus_lambda_functions_deployment.zip --s3-bucket am-uds-dev-cumulus-tf-state --function-name arn:aws:lambda:us-west-2:884500545225:function:Test1 --publish
22+
update_lambda_function_1:
23+
aws --profile saml-pub lambda update-function-code --s3-key unity_cumulus_lambda/cumulus_lambda_functions_deployment.zip --s3-bucket am-uds-dev-cumulus-tf-state --function-name arn:aws:lambda:us-west-2:884500545225:function:am-uds-dev-cumulus-cumulus_collections_dapa --publish &>/dev/null
24+
update_lambda_function_2:
25+
aws --profile saml-pub lambda update-function-code --s3-key unity_cumulus_lambda/cumulus_lambda_functions_deployment.zip --s3-bucket am-uds-dev-cumulus-tf-state --function-name arn:aws:lambda:us-west-2:884500545225:function:am-uds-dev-cumulus-cumulus_granules_dapa --publish &>/dev/null
26+
update_lambda_function_3:
27+
aws --profile saml-pub lambda update-function-code --s3-key unity_cumulus_lambda/cumulus_lambda_functions_deployment.zip --s3-bucket am-uds-dev-cumulus-tf-state --function-name arn:aws:lambda:us-west-2:884500545225:function:am-uds-dev-cumulus-cumulus_collections_ingest_cnm_dapa --publish &>/dev/null

cumulus_lambda_functions/cumulus_collections_dapa/cumulus_collections_dapa.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,14 @@ def __get_size(self):
4646
cumulus_size = {'total_size': -1}
4747
return cumulus_size
4848

49+
def __get_pagination_urls(self):
50+
try:
51+
pagination_links = LambdaApiGatewayUtils(self.__event, self.__limit).generate_pagination_links()
52+
except Exception as e:
53+
LOGGER.exception(f'error while generating pagination links')
54+
return [{'message': f'error while generating pagination links: {str(e)}'}]
55+
return pagination_links
56+
4957
def start(self):
5058
try:
5159
cumulus_result = self.__cumulus.query_direct_to_private_api(self.__cumulus_lambda_prefix)
@@ -63,11 +71,11 @@ def start(self):
6371
return {
6472
'statusCode': 200,
6573
'body': json.dumps({
66-
'size': cumulus_size['total_size'],
67-
'rel': {
68-
'next': LambdaApiGatewayUtils.generate_next_url(self.__event, self.__limit),
69-
'prev': LambdaApiGatewayUtils.generate_prev_url(self.__event, self.__limit),
70-
},
74+
'numberMatched': cumulus_size['total_size'],
75+
'numberReturned': len(cumulus_result['results']),
76+
'stac_version': '1.0.0',
77+
'type': 'FeatureCollection',
78+
'links': self.__get_pagination_urls(),
7179
'features': cumulus_result['results'],
7280
})
7381
}

cumulus_lambda_functions/cumulus_granules_dapa/cumulus_granules_dapa.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,14 @@ def __get_size(self):
9696
cumulus_size = {'total_size': -1}
9797
return cumulus_size
9898

99+
def __get_pagination_urls(self):
100+
try:
101+
pagination_links = LambdaApiGatewayUtils(self.__event, self.__limit).generate_pagination_links()
102+
except Exception as e:
103+
LOGGER.exception(f'error while generating pagination links')
104+
return [{'message': f'error while generating pagination links: {str(e)}'}]
105+
return pagination_links
106+
99107
def start(self):
100108
try:
101109
cumulus_result = self.__cumulus.query_direct_to_private_api(self.__cumulus_lambda_prefix)
@@ -113,11 +121,11 @@ def start(self):
113121
return {
114122
'statusCode': 200,
115123
'body': json.dumps({
116-
'size': cumulus_size['total_size'],
117-
'rel': {
118-
'next': LambdaApiGatewayUtils.generate_next_url(self.__event, self.__limit),
119-
'prev': LambdaApiGatewayUtils.generate_prev_url(self.__event, self.__limit),
120-
},
124+
'numberMatched': cumulus_size['total_size'],
125+
'numberReturned': len(cumulus_result['results']),
126+
'stac_version': '1.0.0',
127+
'type': 'FeatureCollection', # TODO correct name?
128+
'links': self.__get_pagination_urls(),
121129
'features': cumulus_result['results']
122130
})
123131
}
Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from copy import deepcopy
22

33
from cumulus_lambda_functions.lib.json_validator import JsonValidator
4-
4+
from cumulus_lambda_functions.lib.lambda_logger_generator import LambdaLoggerGenerator
55

66
API_GATEWAY_EVENT_SCHEMA = {
77
'type': 'object',
@@ -34,43 +34,74 @@
3434
}
3535
}
3636
}
37+
LOGGER = LambdaLoggerGenerator.get_logger(__name__, LambdaLoggerGenerator.get_level_from_env())
3738

3839

3940
class LambdaApiGatewayUtils:
40-
@staticmethod
41-
def generate_requesting_url(event: dict):
41+
def __init__(self, event: dict, default_limit: int = 10):
42+
self.__event = event
43+
self.__default_limit = default_limit
4244
api_gateway_event_validator_result = JsonValidator(API_GATEWAY_EVENT_SCHEMA).validate(event)
4345
if api_gateway_event_validator_result is not None:
4446
raise ValueError(f'invalid event: {api_gateway_event_validator_result}. event: {event}')
45-
requesting_url = f"https://{event['headers']['Host']}{event['requestContext']['path']}"
47+
48+
def __get_current_page(self):
49+
try:
50+
requesting_base_url = f"https://{self.__event['headers']['Host']}{self.__event['requestContext']['path']}"
51+
new_queries = deepcopy(self.__event['queryStringParameters']) if 'queryStringParameters' in self.__event and self.__event[
52+
'queryStringParameters'] is not None else {}
53+
limit = int(new_queries['limit'] if 'limit' in new_queries else self.__default_limit)
54+
offset = int(new_queries['offset'] if 'offset' in new_queries else 0)
55+
new_queries['limit'] = limit
56+
new_queries['offset'] = offset
57+
requesting_url = f"{requesting_base_url}?{'&'.join([f'{k}={v}' for k, v in new_queries.items()])}"
58+
except Exception as e:
59+
LOGGER.exception(f'error while getting current page URL')
60+
return f'unable to get current page URL, {str(e)}'
4661
return requesting_url
4762

48-
@staticmethod
49-
def generate_next_url(event: dict, default_limit: int = 10):
50-
requesting_base_url = LambdaApiGatewayUtils.generate_requesting_url(event)
51-
new_queries = deepcopy(event['queryStringParameters']) if 'queryStringParameters' in event and event['queryStringParameters'] is not None else {}
52-
limit = int(new_queries['limit'] if 'limit' in new_queries else default_limit)
53-
if limit == 0:
54-
return ''
55-
offset = int(new_queries['offset'] if 'offset' in new_queries else 0)
56-
offset += limit
57-
new_queries['limit'] = limit
58-
new_queries['offset'] = offset
59-
requesting_url = f"{requesting_base_url}?{'&'.join([f'{k}={v}' for k, v in new_queries.items()])}"
63+
def __get_next_page(self):
64+
try:
65+
requesting_base_url = f"https://{self.__event['headers']['Host']}{self.__event['requestContext']['path']}"
66+
new_queries = deepcopy(self.__event['queryStringParameters']) if 'queryStringParameters' in self.__event and self.__event[
67+
'queryStringParameters'] is not None else {}
68+
limit = int(new_queries['limit'] if 'limit' in new_queries else self.__default_limit)
69+
if limit == 0:
70+
return ''
71+
offset = int(new_queries['offset'] if 'offset' in new_queries else 0)
72+
offset += limit
73+
new_queries['limit'] = limit
74+
new_queries['offset'] = offset
75+
requesting_url = f"{requesting_base_url}?{'&'.join([f'{k}={v}' for k, v in new_queries.items()])}"
76+
except Exception as e:
77+
LOGGER.exception(f'error while getting next page URL')
78+
return f'unable to get next page URL, {str(e)}'
6079
return requesting_url
6180

62-
@staticmethod
63-
def generate_prev_url(event: dict, default_limit: int = 10):
64-
requesting_base_url = LambdaApiGatewayUtils.generate_requesting_url(event)
65-
new_queries = deepcopy(event['queryStringParameters']) if 'queryStringParameters' in event and event['queryStringParameters'] is not None else {}
66-
limit = int(new_queries['limit'] if 'limit' in new_queries else default_limit)
67-
if limit == 0:
68-
return ''
69-
offset = int(new_queries['offset'] if 'offset' in new_queries else 0)
70-
offset -= limit
71-
if offset < 0:
72-
offset = 0
73-
new_queries['limit'] = limit
74-
new_queries['offset'] = offset
75-
requesting_url = f"{requesting_base_url}?{'&'.join([f'{k}={v}' for k, v in new_queries.items()])}"
81+
def __get_prev_page(self):
82+
try:
83+
requesting_base_url = f"https://{self.__event['headers']['Host']}{self.__event['requestContext']['path']}"
84+
new_queries = deepcopy(self.__event['queryStringParameters']) if 'queryStringParameters' in self.__event and self.__event[
85+
'queryStringParameters'] is not None else {}
86+
limit = int(new_queries['limit'] if 'limit' in new_queries else self.__default_limit)
87+
if limit == 0:
88+
return ''
89+
offset = int(new_queries['offset'] if 'offset' in new_queries else 0)
90+
offset -= limit
91+
if offset < 0:
92+
offset = 0
93+
new_queries['limit'] = limit
94+
new_queries['offset'] = offset
95+
requesting_url = f"{requesting_base_url}?{'&'.join([f'{k}={v}' for k, v in new_queries.items()])}"
96+
except Exception as e:
97+
LOGGER.exception(f'error while getting previous page URL')
98+
return f'unable to get previous page URL, {str(e)}'
7699
return requesting_url
100+
101+
def generate_pagination_links(self):
102+
return [
103+
{'rel': 'self', 'href': self.__get_current_page()},
104+
{'rel': 'root', 'href': f"https://{self.__event['headers']['Host']}"},
105+
{'rel': 'next', 'href': self.__get_next_page()},
106+
{'rel': 'prev', 'href': self.__get_prev_page()},
107+
]

tests/cumulus_lambda_functions/lib/utils/test_lambda_api_gateway_utils.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,36 @@ def test_01(self):
5252
'body': None,
5353
'isBase64Encoded': False
5454
}
55+
lambda_pagination = LambdaApiGatewayUtils(sample_event, 10)
56+
links = lambda_pagination.generate_pagination_links()
57+
links_dict = {k['rel']: k['href'] for k in links}
58+
self.assertTrue('next' in links_dict, f'next missing in {links}')
59+
self.assertTrue('prev' in links_dict, f'prev missing in {links}')
60+
self.assertTrue('self' in links_dict, f'self missing in {links}')
61+
self.assertTrue('root' in links_dict, f'root missing in {links}')
5562
next_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=10&offset=10'
63+
self.assertEqual(sorted(next_url), sorted(links_dict['next']), f'wrong next url. {next_url} vs {links_dict["next"]}')
5664
prev_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=10&offset=0'
57-
self.assertEqual(sorted(next_url), sorted(LambdaApiGatewayUtils.generate_next_url(sample_event, 10)), f'wrong next url')
58-
self.assertEqual(sorted(prev_url), sorted(LambdaApiGatewayUtils.generate_prev_url(sample_event, 10)), f'wrong prev url')
59-
self.assertEqual('', LambdaApiGatewayUtils.generate_next_url(sample_event, 0), f'wrong next empty url')
60-
self.assertEqual('', LambdaApiGatewayUtils.generate_prev_url(sample_event, 0), f'wrong next empty url')
65+
self.assertEqual(sorted(prev_url), sorted(links_dict['prev']), f'wrong next url. {prev_url} vs {links_dict["prev"]}')
66+
current_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=10&offset=0'
67+
self.assertEqual(sorted(current_url), sorted(links_dict['self']), f'wrong self url. {current_url} vs {links_dict["self"]}')
68+
69+
lambda_pagination = LambdaApiGatewayUtils(sample_event, 0)
70+
links = lambda_pagination.generate_pagination_links()
71+
links_dict = {k['rel']: k['href'] for k in links}
72+
self.assertEqual('', links_dict['next'], f'wrong next empty url. {links_dict["next"]}')
73+
self.assertEqual('', links_dict['prev'], f'wrong next empty url. {links_dict["prev"]}')
74+
current_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=0&offset=0'
75+
self.assertEqual(sorted(current_url), sorted(links_dict['self']), f'wrong self url. {current_url} vs {links_dict["self"]}')
76+
6177
sample_event['queryStringParameters']['offset'] = 10
78+
lambda_pagination = LambdaApiGatewayUtils(sample_event, 5)
79+
links = lambda_pagination.generate_pagination_links()
80+
links_dict = {k['rel']: k['href'] for k in links}
6281
next_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=5&offset=15'
82+
self.assertEqual(sorted(next_url), sorted(links_dict['next']), f'wrong next url. {next_url} vs {links_dict["next"]}')
6383
prev_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=5&offset=5'
64-
self.assertEqual(sorted(next_url), sorted(LambdaApiGatewayUtils.generate_next_url(sample_event, 5)), f'wrong next url 2')
65-
self.assertEqual(sorted(prev_url), sorted(LambdaApiGatewayUtils.generate_prev_url(sample_event, 5)), f'wrong prev url 2')
84+
self.assertEqual(sorted(prev_url), sorted(links_dict['prev']), f'wrong next url. {prev_url} vs {links_dict["prev"]}')
85+
current_url = 'https://k3a3qmarxh.execute-api.us-west-2.amazonaws.com/dev/am-uds-dapa/collections/L0_SNPP_ATMS_SCIENCE___1/items?datetime=1990-01-01T00:00:00Z/2021-01-03T00:00:00Z&limit=5&offset=10'
86+
self.assertEqual(sorted(current_url), sorted(links_dict['self']), f'wrong self url. {current_url} vs {links_dict["self"]}')
6687
return

0 commit comments

Comments
 (0)