Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Das 1893 #8

Merged
merged 13 commits into from
Sep 8, 2023
Prev Previous commit
Next Next commit
PR ready
eni-awowale committed Sep 6, 2023
commit b28586e860125f5cdc26ac1d783154ed555c89b4
124 changes: 44 additions & 80 deletions tests/unit/test_umm_var.py
Original file line number Diff line number Diff line change
@@ -11,12 +11,12 @@
from netCDF4 import Dataset
from numpy import int32, float64
import requests
from requests.exceptions import RequestException
from requests.exceptions import Timeout
from cmr import CMR_UAT

from varinfo import CFConfig
from varinfo import VarInfoFromDmr, VarInfoFromNetCDF4, VariableFromDmr
from varinfo.exceptions import InvalidExportDirectory, UmmVarPublicationException
from varinfo.exceptions import InvalidExportDirectory
from varinfo.umm_var import (export_all_umm_var_to_json,
export_umm_var_to_json,get_all_umm_var,
get_dimension_information, get_dimension_size,
@@ -765,34 +765,25 @@ def test_export_all_umm_var_to_json(self):
self.assertDictEqual(saved_json, umm_var_records[umm_var_index])


def _mock_requests(self, status=200, content='VFOO-EEDTEST'):
'''
Mock requests.put module and it's methods content and status.
Return the mock object.
'''
mock_response = Mock(spec=requests.Response)
mock_response.status_code = status
mock_response.content = content
return mock_response


@patch('requests.put')
def test_publish_umm_var(self, mock_requests_put):
''' Check if `publish_umm_var` returns the expected
content for the mocked response.
'''
# Set the `mock_response` and its status_code
mock_response = Mock(spec=requests.Response)
mock_response.status_code = 400

# Set the return_value of `mock_response`
# And set the return_value of `mock_requests_put` to mock_response
mock_response.json.return_value = {'concept-id': 'FOO-EEDTEST'}
mock_requests_put.return_value = mock_response
eni-awowale marked this conversation as resolved.
Show resolved Hide resolved
umm_var_dict = {'Name': 'Test',
'MetadataSpecification':{
'URL': 'https://foo.gov/umm/variable/v1.8.2',
'MetadataSpecification':
{'URL': 'https://foo.gov/umm/variable/v1.8.2',
'Name': 'UMM-Var',
'Version': '1.8.2'}}

# Set the mock_response with the `_mock_requests` object content method'
mock_response = self._mock_requests('VFOO-EEDTEST')

# Set the return_value of `mock_requests_put` to mock_response
mock_requests_put.return_value = mock_response


concept_id = 'C1256535511-EEDTEST'
cmr_env = CMR_UAT
token = 'foo'
@@ -801,72 +792,45 @@ def test_publish_umm_var(self, mock_requests_put):
+ f'{umm_var_dict["MetadataSpecification"]["Version"]}',
'Authorization': f'Bearer {token}',
'Accept': 'application/json'}

url_endpoint = (cmr_env.replace('search', 'ingest') + 'collections/'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would you consider renaming this to expected_url_endpoint and renaming headers_umm_var to expected_headers_umm_var?

That would just help me see that these are going to be tested rather than used as set up input.

you can say no.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So they will be used as the set up input. Unless I am misunderstanding you.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so the two variables I named, are what you expect the mock to be called with, they aren't input to the function called. And like I said, these are suggestions.

f'{concept_id}/variables/{umm_var_dict["Name"]}')

response = publish_umm_var(concept_id, umm_var_dict, 'foo', cmr_env)
response = publish_umm_var(concept_id, umm_var_dict, token, cmr_env)

# Check if `publish_umm_var` was called once with expected parameters
mock_requests_put.assert_called_once_with(url_endpoint,
json=umm_var_dict,
headers=headers_umm_var,
timeout=10)
# print(response)
# print(mock_response.status_code)

# @patch('requests.put')
# def test_publish_umm_var_status_code(self, mock_requests_put):
# mock_response = Mock(spec=requests.Response)
# mock_response.status_code = 200
# umm_var_dict = {'Name': 'Test',
# 'MetadataSpecification':{
# 'URL': 'https://foo.gov/umm/variable/v1.8.2',
# 'Name': 'UMM-Var',
# 'Version': '1.8.2'}}

# # Set the mock_response with the `_mock_requests` object content method'
# mock_response.status_code = 400

# # Set the return_value of `mock_requests_put` to mock_response
# mock_requests_put.return_value = 'VFOO-EEDTEST'

# concept_id = 'C1256535511-EEDTEST'
# cmr_env = CMR_UAT
# token = 'foo'
# headers_umm_var = {
# 'Content-type': 'application/vnd.nasa.cmr.umm+json;version='
# + f'{umm_var_dict["MetadataSpecification"]["Version"]}',
# 'Authorization': f'Bearer {token}',
# 'Accept': 'application/json'}
# url_endpoint = (cmr_env.replace('search', 'ingest') + 'collections/'
# f'{concept_id}/variables/{umm_var_dict["Name"]}')

# print(mock_response.status_code)
# response = publish_umm_var(concept_id, umm_var_dict, 'foo', cmr_env)
# print(response)
#self.assertEqual(response, mock_response.status_code)
with self.subTest('Check response when the status code is 400'):
self.assertEqual(response.status_code, 400)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move the setting of the status code from L775 to here like you do in the next subtest for code 200. Just for symmetry (and setting close to the testing location)

self.assertEqual(response.json(), {'concept-id': 'FOO-EEDTEST'})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of a nit: Nice check for a failed status, but it's a bit weird seeing the JSON response for a successful request with an unsuccessful status.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Side note - is it worth an extra assertion to know that requests.put was called with the expected JSON content and headers?

Copy link
Collaborator Author

@eni-awowale eni-awowale Sep 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit of a nit: Nice check for a failed status, but it's a bit weird seeing the JSON response for a successful request with an unsuccessful status.

Yeah, I agree. I think that was the best way I thought for checking if the response failed but I can try tinker with some other ways.

Side note - is it worth an extra assertion to know that requests.put was called with the expected JSON content and headers?

I think since we already did the mock_requests_put.assert_called_once_with() test we don't need to have it here unless you really thinks it's helpful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have new commit that addresses this!


# Check if the UmmVarPublicationException is raised when
# the `side_effect` for the mock request is an HTTPError
# mock_requests_put.reset_mock()
# with self.subTest('Test if the downloaded file contains '
# 'expected content'):
# mock_requests_put.return_value.side_effect = RequestException('Wrong HTTP')
# with self.assertRaises(UmmVarPublicationException):
# publish_umm_var(concept_id, umm_var_dict, 'foo', cmr_env)


# @patch('requests.put')
# def test_requests_error(self, mock_requests_put):
# ''' Check if the UmmVarPublicationException is raised when
# the `side_effect` for the mock request is an HTTPError
# '''
# link = 'https://foo.gov/example.nc4'
# mock_requests_put.return_value.side_effect = HTTPError('Wrong HTTP')
# with self.assertRaises(UmmVarPublicationException):
# publish_umm_var(link, token='foo')


# import unittest
# if __name__ == "__main__":
# unittest.main()
with self.subTest('Check response when the status code is 200'):
mock_response.status_code = 200
response = publish_umm_var(concept_id, umm_var_dict, token, cmr_env)
self.assertEqual(response, 'FOO-EEDTEST')


def test_publish_all_umm_var(self):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It feels like something should be mocked here - requests.put again, maybe?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can add that!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also - I feel like we need a sub test here for what happens when publishing the first UMM-Var record fails. (I think for now we agreed that we would continue on and try and publish the rest of the records, but I've been out a few days, so might be misremembering)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that is what we agreed on. It continues with the ingest process. I will add another test like that. I wrote a comment below for @flamingbear regarding this.

''' Check if `publish_all_umm_var` returns expected number of
published UMM-Var entries.
'''
umm_var_dict = {'Test_1': {'Name': 'Test_1',
'MetadataSpecification':
{'URL': 'https://foo.gov/umm/variable/v1.8.2',
'Name': 'UMM-Var',
'Version': '1.8.2'}},
'Test_2': {'Name': 'Test_2',
'MetadataSpecification':
{'URL': 'https://foo.gov/umm/variable/v1.8.2',
'Name': 'UMM-Var',
'Version': '1.8.2'}}}

variable_ids = publish_all_umm_var('C1256535511-EEDTEST',
umm_var_dict,
'foo',
CMR_UAT)
self.assertEqual(len(variable_ids), 2)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how error handling works here.

What happens if one of the variables in the loop fails?

Then the return value would be something like

['FOO-EEDTEST', {'concept-id': 'FOO-EEDTEST'}]?

Who is in charge of checking for errors when you run publish_all_umm_var?

Should you raise exceptions rather than returning the full response on error?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great question and something that @owenlittlejohns and other folks have been talking about. We don't want to raise an error because raising an error would stop the whole ingest process if a single variable in a collection has a hundred variables. We want to add it into the list and maybe have something that checks later for if they were any errors. But it is a good idea to add a test for checking if the list contains errors. I will add that.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree we do not want to fail the "put" (ingest), but there is a good chance this will be updated/extended in future releases.

11 changes: 0 additions & 11 deletions varinfo/exceptions.py
Original file line number Diff line number Diff line change
@@ -113,14 +113,3 @@ def __init__(self, directory_creation_exception_message):
super().__init__('DirectoryCreationException',
'directory creation failed with the following error: '
f'{directory_creation_exception_message}')


class UmmVarPublicationException(CustomError):
''' This exception is raised when the `request.put` module fails to
publish a UMM-Var entry to CMR.
'''

def __init__(self, directory_creation_exception_message):
super().__init__('UmmVarPublicationException',
'UMM-Var publication failed with the following error: '
f'{directory_creation_exception_message}')
26 changes: 12 additions & 14 deletions varinfo/umm_var.py
Original file line number Diff line number Diff line change
@@ -40,8 +40,7 @@
from cmr import CMR_UAT


from varinfo.exceptions import (InvalidExportDirectory,
UmmVarPublicationException)
from varinfo.exceptions import InvalidExportDirectory
from varinfo.var_info import VarInfoBase
from varinfo.variable import VariableBase

@@ -365,18 +364,17 @@ def publish_umm_var(collection_id: str,

url_endpoint = (cmr_env.replace('search', 'ingest') + 'collections/'
f'{collection_id}/variables/{umm_var_dict["Name"]}')
try:
response = requests.put(url_endpoint,
json=umm_var_dict,
headers=headers_umm_var,
timeout=10)
if response.status_code == 200:
return response.json()['concept-id']
else:
return response.json()
except Exception as requests_exception:
raise UmmVarPublicationException(
str(requests_exception)) from requests_exception

response = requests.put(url_endpoint,
json=umm_var_dict,
headers=headers_umm_var,
timeout=10)
# Check the status_code of the response
# Return the variable concept-id if response is 200
if response.status_code == 200:
eni-awowale marked this conversation as resolved.
Show resolved Hide resolved
return response.json()['concept-id']
# Return the response if there was an error (response != 200)
return response
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to return the full response object, or maybe the error message? I can't remember the JSON format for a CMR ingest error, but maybe the JSON includes a field for the error message (rather than just using response.text).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to return the full response object. So to get the error it would be something like response.json().['errors']
This is what response.json() looks like if there is an error:

{'errors': ['#: required key [LongName] not found',
  '#: required key [Definition] not found']}



def publish_all_umm_var(collection_id: str,