Skip to content

Commit

Permalink
Merge pull request #63 from ASFHyP3/handle-no-neighbors
Browse files Browse the repository at this point in the history
Handle no neighbors in find_new.get_neighbors
  • Loading branch information
jtherrmann authored May 10, 2022
2 parents 8697a31 + fd43d3b commit 5ff178a
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 18 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [PEP 440](https://www.python.org/dev/peps/pep-0440/)
and uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.12]
### Fixed
- Handle the case where a product has no temporal neighbors.

## [0.0.11]
### Added
- Added CORS support to product bucket in order to support Vertex's "Download All" feature.
Expand Down
2 changes: 1 addition & 1 deletion find_new/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
asf-search==3.0.6
asf-search==3.1.3
hyp3-sdk>=1.3.2
python-dateutil
requests
Expand Down
43 changes: 27 additions & 16 deletions find_new/src/find_new.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,29 +75,40 @@ def add_invalid_product_record(event_id, granule, message):


def get_neighbors(product_name: str, max_neighbors: int = 2) -> List[dict]:
if max_neighbors < 1:
raise ValueError(f"max_neighbors must be >= 1 but got {max_neighbors}")

results = asf_search.product_search([product_name])
assert len(results) == 1
granule: asf_search.ASFProduct = results[0]

stack = asf_search.baseline_search.stack_from_product(granule)
stack = [item for item in stack if item.properties['temporalBaseline'] < 0]
neighbors = [item.properties['fileID'] for item in stack[-max_neighbors:]]

response = requests.post(
SEARCH_URL,
params={
'product_list': ','.join(neighbors),
'output': 'jsonlite'
}
)

status_code = str(response.status_code)
if status_code[0] == '4':
raise asf_search.ASFSearch4xxError()
elif status_code[0] == '5':
raise asf_search.ASFSearch5xxError()
neighbor_names = [item.properties['fileID'] for item in stack[-max_neighbors:]]

if len(neighbor_names) == 0:
neighbors = []
else:
response = requests.post(
SEARCH_URL,
params={
'product_list': ','.join(neighbor_names),
'output': 'jsonlite'
}
)

return response.json()['results']
status_code = str(response.status_code)
if status_code[0] == '4':
raise asf_search.ASFSearch4xxError()
elif status_code[0] == '5':
raise asf_search.ASFSearch5xxError()

neighbors = response.json()['results']

assert len(neighbors) <= max_neighbors
assert len(neighbors) == len(neighbor_names)

return neighbors


def submit_jobs_for_granule(hyp3, event_id, granule):
Expand Down
134 changes: 133 additions & 1 deletion tests/test_find_new.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from os import environ
from unittest.mock import patch
from unittest.mock import MagicMock, NonCallableMagicMock, call, patch
from uuid import uuid4

import asf_search
Expand Down Expand Up @@ -204,6 +204,138 @@ def test_add_invalid_product_record(tables):
assert response[0]['event_id'] == 'event_id1'


@patch('asf_search.baseline_search.stack_from_product')
@patch('asf_search.product_search')
@responses.activate
def test_get_neighbors(mock_product_search: MagicMock, mock_stack_from_product: MagicMock):
mock_granule = NonCallableMagicMock()
mock_product_search.return_value = [mock_granule]

mock_stack_from_product.return_value = [
NonCallableMagicMock(properties={'fileID': 'file-0', 'temporalBaseline': -3}),
NonCallableMagicMock(properties={'fileID': 'file-1', 'temporalBaseline': -2}),
NonCallableMagicMock(properties={'fileID': 'file-2', 'temporalBaseline': -1}),
NonCallableMagicMock(properties={'fileID': 'file-3', 'temporalBaseline': 0}),
NonCallableMagicMock(properties={'fileID': 'file-4', 'temporalBaseline': 1}),
NonCallableMagicMock(properties={'fileID': 'file-5', 'temporalBaseline': 2}),
]

mock_response_1 = {'results': [{'granuleName': 'granule2'}]}
params_1 = {'product_list': 'file-2', 'output': 'jsonlite'}
responses.post(
url=find_new.SEARCH_URL,
body=json.dumps(mock_response_1),
match=[responses.matchers.query_param_matcher(params_1)]
)

mock_response_2 = {'results': [{'granuleName': 'granule1'}, {'granuleName': 'granule2'}]}
params_2 = {'product_list': 'file-1,file-2', 'output': 'jsonlite'}
responses.post(
url=find_new.SEARCH_URL,
body=json.dumps(mock_response_2),
match=[responses.matchers.query_param_matcher(params_2)]
)

mock_response_3 = {
'results': [{'granuleName': 'granule0'}, {'granuleName': 'granule1'}, {'granuleName': 'granule2'}]
}
params_3 = {'product_list': 'file-0,file-1,file-2', 'output': 'jsonlite'}
responses.post(
url=find_new.SEARCH_URL,
body=json.dumps(mock_response_3),
match=[responses.matchers.query_param_matcher(params_3)]
)

assert find_new.get_neighbors('test-product', max_neighbors=1) == mock_response_1['results']
assert find_new.get_neighbors('test-product', max_neighbors=2) == mock_response_2['results']
assert find_new.get_neighbors('test-product', max_neighbors=3) == mock_response_3['results']
assert find_new.get_neighbors('test-product', max_neighbors=100) == mock_response_3['results']

assert mock_product_search.mock_calls == [call(['test-product']) for _ in range(4)]
assert mock_stack_from_product.mock_calls == [call(mock_granule) for _ in range(4)]


@patch('asf_search.baseline_search.stack_from_product')
@patch('asf_search.product_search')
@responses.activate
def test_get_neighbors_response_400(mock_product_search: MagicMock, mock_stack_from_product: MagicMock):
mock_granule = NonCallableMagicMock()
mock_product_search.return_value = [mock_granule]

mock_stack_from_product.return_value = [
NonCallableMagicMock(properties={'fileID': 'file1', 'temporalBaseline': -1})
]

params = {'product_list': 'file1', 'output': 'jsonlite'}
responses.post(
url=find_new.SEARCH_URL,
status=400,
match=[responses.matchers.query_param_matcher(params)]
)

with pytest.raises(asf_search.ASFSearch4xxError):
find_new.get_neighbors('test-product')

assert mock_product_search.mock_calls == [call(['test-product'])]
assert mock_stack_from_product.mock_calls == [call(mock_granule)]


@patch('asf_search.baseline_search.stack_from_product')
@patch('asf_search.product_search')
@responses.activate
def test_get_neighbors_response_500(mock_product_search: MagicMock, mock_stack_from_product: MagicMock):
mock_granule = NonCallableMagicMock()
mock_product_search.return_value = [mock_granule]

mock_stack_from_product.return_value = [
NonCallableMagicMock(properties={'fileID': 'file1', 'temporalBaseline': -1})
]

params = {'product_list': 'file1', 'output': 'jsonlite'}
responses.post(
url=find_new.SEARCH_URL,
status=500,
match=[responses.matchers.query_param_matcher(params)]
)

with pytest.raises(asf_search.ASFSearch5xxError):
find_new.get_neighbors('test-product')

assert mock_product_search.mock_calls == [call(['test-product'])]
assert mock_stack_from_product.mock_calls == [call(mock_granule)]


@patch('asf_search.baseline_search.stack_from_product')
@patch('asf_search.product_search')
@responses.activate
def test_get_neighbors_no_neighbors(mock_product_search: MagicMock, mock_stack_from_product: MagicMock):
mock_granule = NonCallableMagicMock()
mock_product_search.return_value = [mock_granule]

mock_stack_from_product.return_value = []

assert find_new.get_neighbors('test-product') == []

mock_stack_from_product.return_value = [
NonCallableMagicMock(properties={'fileID': 'file-0', 'temporalBaseline': 0}),
NonCallableMagicMock(properties={'fileID': 'file-1', 'temporalBaseline': 1}),
NonCallableMagicMock(properties={'fileID': 'file-2', 'temporalBaseline': 2}),
]

assert find_new.get_neighbors('test-product') == []

assert mock_product_search.mock_calls == [call(['test-product']) for _ in range(2)]
assert mock_stack_from_product.mock_calls == [call(mock_granule) for _ in range(2)]


def test_get_neighbors_max_neighbors_error():
with pytest.raises(ValueError, match=r'.*max_neighbors.*'):
find_new.get_neighbors('test-product', max_neighbors=-1)

with pytest.raises(ValueError, match=r'.*max_neighbors.*'):
find_new.get_neighbors('test-product', max_neighbors=0)


@responses.activate
def test_submit_jobs_for_granule(tables):
responses.add(responses.GET, AUTH_URL)
Expand Down

0 comments on commit 5ff178a

Please sign in to comment.