-
Notifications
You must be signed in to change notification settings - Fork 70
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
Add dataset.services()
method to list available Harmony services
#500
base: main
Are you sure you want to change the base?
Conversation
dataset.services()
method to list available Harmony services
It seems like the existing integration tests are failing due to credentials which I had set up as environment variables and in a |
Hope you don't mind the rename, was finding the old name difficult to remember in my notifications :) |
@betolink @andypbarrett I think this may need documenting! As a developer, how do I run integration tests on my laptop? |
…ature/issue-447
…ccess into feature/issue-447
@mfisher87 - I think that I was able to fix the integration test and also modified the services unit and integration tests to use VCR. All unit tests are passing. I ran the integration tests locally and here is the summary: ==================== short test summary info ================================= FAILED tests/integration/test_auth.py::test_auth_can_read_from_netrc_file - AssertionError: False is not true ====== 7 failed, 71 passed, 1 skipped in 496.39s (0:08:16) ========================================== I wonder if these tests need to be run in the same AWS region as the data? Other than that I think this PR can be merged but let me know how that typically works for |
Will you be at hack day this coming week? I may not have time to review this before then 😬 |
@mfisher87 - Unfortunately I am on travel this coming week so I can't make it but I do plan to attend the next earthaccess hack day in a few weeks. We can discuss then if it's easier! 😄 |
Sounds like a good plan :) Safe and fun travels! |
@nikki-t I'll only attend 2nd half of hack day this week, had to schedule a dentist appointment at that time. I may be co-working on that call (I've used my funded Openscapes allocation), but feel free to interrupt me so we can have a quick chat. I love the how-to you added 🤩 |
Co-authored-by: Jessica Scheick <[email protected]>
Co-authored-by: Jessica Scheick <[email protected]>
Co-authored-by: Jessica Scheick <[email protected]>
@JessicaS11 - Thank you for the suggested updates, I have applied them! @mfisher87 , @betolink - Do you think this PR is ready to be merged? |
Hi @nikki-t the code looks good to me, there are some failing tests due a print statement which I'm not opposed to, @mfisher87do you know if there is a way to skip this rule on a file? |
Ahh right @chuckwondo cc @nikki-t |
earthaccess/services.py
Outdated
|
||
page_size = min(limit, 2000) | ||
url = self._build_url() | ||
|
||
results = [] # type: List[str] | ||
page = 1 | ||
while len(results) < limit: | ||
params = {"page_size": page_size, "page_num": page} | ||
if self._debug: | ||
print(f"Fetching: {url}") | ||
# TODO: implement caching | ||
response = self.session.get(url, params=params) | ||
|
||
try: | ||
response.raise_for_status() | ||
except exceptions.HTTPError as ex: | ||
raise RuntimeError(ex.response.text) | ||
|
||
if self._format == "json": | ||
latest = response.json()["items"] | ||
else: | ||
latest = [response.text] | ||
|
||
if len(latest) == 0: | ||
break | ||
|
||
results.extend(latest) | ||
page += 1 | ||
|
||
return results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This code already exists in ServiceQuery
, so simply call the superclass method. Ideally, this method doesn't need to be here at all, but for now we do this simply for generating docs.
page_size = min(limit, 2000) | |
url = self._build_url() | |
results = [] # type: List[str] | |
page = 1 | |
while len(results) < limit: | |
params = {"page_size": page_size, "page_num": page} | |
if self._debug: | |
print(f"Fetching: {url}") | |
# TODO: implement caching | |
response = self.session.get(url, params=params) | |
try: | |
response.raise_for_status() | |
except exceptions.HTTPError as ex: | |
raise RuntimeError(ex.response.text) | |
if self._format == "json": | |
latest = response.json()["items"] | |
else: | |
latest = [response.text] | |
if len(latest) == 0: | |
break | |
results.extend(latest) | |
page += 1 | |
return results | |
return super.get(limit) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this needs to be a call to super like this: super().get(limit)
. Will test and implement in code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, hang on. We want umm_json
, not json
, so we do need to implement this here because python_cmr
currently parses only when the format is json
, not umm_json
.
However, we already implement this generally in search.get_results
, so this should likely be something like so:
from .search import get_results
def get(self, limit: int = 2000) -> List[Any]:
return search.get_results(self.session, self, limit)
However, this will need you to tweak search.py
as well, as follows:
First, change from cmr import CollectionQuery, GranuleQuery
to from cmr import CollectionQuery, GranuleQuery, ServiceQuery
Next, change this:
def get_results(
session: requests.Session,
query: Union[CollectionQuery, GranuleQuery],
limit: int = 2000,
) -> List[Any]:
to this:
def get_results(
session: requests.Session,
query: Union[CollectionQuery, GranuleQuery, ServiceQuery],
limit: int = 2000,
) -> List[Any]:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I make these changes I get a circular dependency as services.py tries to import from .search
while results.py
is trying to import DataCollection, DataGranule
from .search
. The results module uses DataService
to query and parse results. Also see this comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmmm, yeah, I see the circularity. We may need to rethink where to put things to avoid circularities.
earthaccess/results.py
Outdated
if services: | ||
for service in services: | ||
if earthaccess.__auth__.authenticated: | ||
query = DataService(auth=earthaccess.__auth__).parameters( | ||
concept_id=service | ||
) | ||
else: | ||
query = DataService().parameters(concept_id=service) | ||
results = query.get(query.hits()) | ||
parsed[service] = self._parse_service_result(results) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if services: | |
for service in services: | |
if earthaccess.__auth__.authenticated: | |
query = DataService(auth=earthaccess.__auth__).parameters( | |
concept_id=service | |
) | |
else: | |
query = DataService().parameters(concept_id=service) | |
results = query.get(query.hits()) | |
parsed[service] = self._parse_service_result(results) | |
for service in services: | |
if earthaccess.__auth__.authenticated: | |
query = DataService(auth=earthaccess.__auth__).parameters( | |
concept_id=service | |
) | |
else: | |
query = DataService().parameters(concept_id=service) | |
results = query.get(query.hits()) | |
parsed[service] = self._parse_service_result(results) |
earthaccess/results.py
Outdated
"provider-id": result_json["items"][0]["meta"]["provider-id"], | ||
"umm": result_json["items"][0]["umm"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we getting only the item at index 0?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the results response, the data always seems to be returned under a list of one element which contains all of the metadata. In order to provide some filtering, I chose only to return the provider_id and the UMM JSON response for each service. See attached sample-results-response.json.
It also looks like this is the case in the CMR API documentation but to be on the safe side I will figure out how to iterate over the list to make sure we don't miss anything.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you address my suggestion I just added to the get
method, you should not need to use json.loads
because the get
method will returned parsed results.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@chuckwondo - I can update the code to iterate over the "items" list in the CMR service query response but then I end up returning a list of lists to the end user. See attached parsed.json. What do you think of returning a list of lists that contains the items?
Here is an example:
"S2839491596-XYZ_PROV": [
[
{
"provider-id": "XYZ_PROV",
"umm": {
"URL": {
"Description": "https://harmony.earthdata.nasa.gov",
"URLValue": "This is the Harmony root endpoint."
},
"Type": "Harmony",
"ServiceKeywords": [
{
"ServiceCategory": "EARTH SCIENCE SERVICES",
"ServiceTopic": "DATA MANAGEMENT/DATA HANDLING",
"ServiceTerm": "DATA ACCESS/RETRIEVAL"
},
{
"ServiceCategory": "EARTH SCIENCE SERVICES",
"ServiceTopic": "DATA MANAGEMENT/DATA HANDLING",
"ServiceTerm": "DATA INTEROPERABILITY",
"ServiceSpecificTerm": "DATA REFORMATTING"
}
],
"ServiceOrganizations": [
{
"Roles": [
"DEVELOPER",
"PUBLISHER",
"SERVICE PROVIDER"
],
"ShortName": "NASA/GSFC/EOS/EOSDIS/EMD",
"LongName": "Maintenance and Development, Earth Observing System Data and Information System, Earth Observing System,Goddard Space Flight Center, NASA"
}
],
"Description": "Backend NetCDF-to-Zarr service option description for Harmony data transformations. Cannot be chained with other operations from this record.",
"VersionDescription": "Semantic version number for the NetCDF-to-Zarr Docker image used by Harmony in production.",
"Version": "1.2.0",
"Name": "Harmony NetCDF-to-Zarr Service",
"ContactPersons": [
{
"Roles": [
"DEVELOPER"
],
"FirstName": "Owen",
"LastName": "Littlejohns",
"ContactInformation": {
"ContactMechanisms": [
{
"Type": "Email",
"Value": "[email protected]"
}
]
}
},
{
"Roles": [
"SERVICE PROVIDER"
],
"FirstName": "David",
"LastName": "Auty",
"ContactInformation": {
"ContactMechanisms": [
{
"Type": "Email",
"Value": "[email protected]"
}
]
}
}
],
"ServiceOptions": {
"Aggregation": {
"Concatenate": {
"ConcatenateDefault": "False"
}
},
"SupportedReformattings": [
{
"SupportedInputFormat": "NETCDF-4",
"SupportedOutputFormats": [
"ZARR"
]
}
]
},
"MetadataSpecification": {
"URL": "https://cdn.earthdata.nasa.gov/umm/service/v1.5.3",
"Name": "UMM-S",
"Version": "1.5.3"
},
"LongName": "Harmony NetCDF-to-Zarr Service"
}
}
]
]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there meaning to the nested list structure? If not, we could use itertools.chain()
to flatten it out, IIRC.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See #500 (comment)
Co-authored-by: Chuck Daniels <[email protected]>
Co-authored-by: Chuck Daniels <[email protected]>
earthaccess/services.py
Outdated
|
||
page_size = min(limit, 2000) | ||
url = self._build_url() | ||
|
||
results = [] # type: List[str] | ||
page = 1 | ||
while len(results) < limit: | ||
params = {"page_size": page_size, "page_num": page} | ||
if self._debug: | ||
print(f"Fetching: {url}") | ||
# TODO: implement caching | ||
response = self.session.get(url, params=params) | ||
|
||
try: | ||
response.raise_for_status() | ||
except exceptions.HTTPError as ex: | ||
raise RuntimeError(ex.response.text) | ||
|
||
if self._format == "json": | ||
latest = response.json()["items"] | ||
else: | ||
latest = [response.text] | ||
|
||
if len(latest) == 0: | ||
break | ||
|
||
results.extend(latest) | ||
page += 1 | ||
|
||
return results |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, hang on. We want umm_json
, not json
, so we do need to implement this here because python_cmr
currently parses only when the format is json
, not umm_json
.
However, we already implement this generally in search.get_results
, so this should likely be something like so:
from .search import get_results
def get(self, limit: int = 2000) -> List[Any]:
return search.get_results(self.session, self, limit)
However, this will need you to tweak search.py
as well, as follows:
First, change from cmr import CollectionQuery, GranuleQuery
to from cmr import CollectionQuery, GranuleQuery, ServiceQuery
Next, change this:
def get_results(
session: requests.Session,
query: Union[CollectionQuery, GranuleQuery],
limit: int = 2000,
) -> List[Any]:
to this:
def get_results(
session: requests.Session,
query: Union[CollectionQuery, GranuleQuery, ServiceQuery],
limit: int = 2000,
) -> List[Any]:
earthaccess/results.py
Outdated
"provider-id": result_json["items"][0]["meta"]["provider-id"], | ||
"umm": result_json["items"][0]["umm"], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you address my suggestion I just added to the get
method, you should not need to use json.loads
because the get
method will returned parsed results.
earthaccess/results.py
Outdated
results = query.get(query.hits()) | ||
parsed[service] = self._parse_service_result(results) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Once you make the change I suggested in the get
method, you should be able to eliminate the _parse_service_result
method and just do this:
results = query.get(query.hits()) | |
parsed[service] = self._parse_service_result(results) | |
parsed[service] = query.get(query.hits()) |
from .auth import Auth | ||
|
||
|
||
class DataService(ServiceQuery): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Any reason this is not in search.py
, like DataCollections
and DataGranules
are?
I'm not opposed to keeping this in a separate file, but splitting each type of query class into their own modules might perhaps be left as a task for a separate issue (after discussing whether or not we want to do so).
For consistency with existing code, I suggest moving this to search.py
and also renaming it to be pluralized: DataServices
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had initially wanted to place DataService
in search.py
but because the results module uses the DataService
class to query and parse results, a circular dependency is created between the search and results modules. I also thought the DataService
class might grow as we add in the plugin architecture and could serve as the main entrypoint for loading and accessing plugins.
I can look into re-architecting the code so that there is a DataServices
class in search.py
and DataService
class in results.py
to be more consistent with the Collections and Granules structure. I was initially thinking that the services would be returned for a collection rather than having a separate search_services
function.
RIght now the end user can query services like this:
datasets = search_datasets(
short_name="MUR-JPL-L4-GLOB-v4.1",
cloud_hosted=True,
temporal=("2024-02-27T00:00:00Z", "2024-02-29T00:00:00Z"),
)
for dataset in datasets:
print(dataset.services())
Making it pretty easy to return service data for a collection. If I re-architect as mentioned above. The user would search a service like this:
datasets = search_datasets(
short_name="MUR-JPL-L4-GLOB-v4.1",
cloud_hosted=True,
temporal=("2024-02-27T00:00:00Z", "2024-02-29T00:00:00Z"),
)
for dataset in datasets:
services = dataset["meta"]["associations"]["services"]
for service in services:
print(search_services(service))
I like how easy it is to return service data in the first code snippet but also want to make sure we are building a codebase that is consistent and easy to modify (add to) in the future as I think we want to build off of this with the plugin architecture.
Open to suggestions and/or discussing at the next hackday.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see the circular dependency you're referring to.
I still suggest your rename DataService
(singular) to DataServices
(plural) making it consistent with DataCollections
and DataGranules
.
Regarding the "easier" code you mention above, I agree, but that doesn't preclude providing a search_services
method as well. Users can then do both things: (a) call dataset.services()
to get the services associated with a dataset, or (b) call search_services
to search for services more generally, not necessarily specific to a dataset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Users can then do both things: (a) call dataset.services() to get the services associated with a dataset, or (b) call search_services to search for services more generally, not necessarily specific to a dataset.
Should we consider these separate features and follow-up later to add even more convenience?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think so. In fact, I suggest that dataset.services()
simply invoke search_services
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds reasonable to me. My only request is to not name anything "utils." It's a pet peeve of mine because it's such a generic name, as to have no meaning. Happy to iron out kinks with you at the next hack day.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
My only request is to not name anything "utils."
I love this and feel so called out 🤣 I'm very prone to creating a utils
subpackage but I always regret it later and am trying to get better at it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Totally agree on utils
😄 but it does look like there is already a utils
directory. Should we consider moving that to a different name? Or maybe I am misunderstanding!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I hadn't noticed that there's already a utils
. Oh well. We can worry about that another time.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If that was me, sorry 🤣
- Modify services.py to use get_results function - Update results.DataCollection.services function to retrieve and return UMM-JSON for services - Update unit and integration tests
…ature/issue-447
Suggested modifications have been made and unit and integration tests have been updated. Unit Test Summary ================================================================================================================ short test summary info =================================================================================================================
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[2001-12-12-2001-12-21-2001-12-12T00:00:00Z,2001-12-21T23:59:59Z] - ValueError: time data '2001-12-12' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[2021-02-01--2021-02-01T00:00:00Z,] - ValueError: time data '2021-02-01' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[1999-02-01 06:00-2009-01-01-1999-02-01T06:00:00Z,2009-01-01T23:59:59Z] - ValueError: time data '1999-02-01 06:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[2019-03-10T00:00:00Z-2019-03-10T00:00:00-01:00-2019-03-10T00:00:00Z,2019-03-10T01:00:00Z] - ValueError: time data '2019-03-10T00:00:00-01:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[2001-12-12-2001-12-21-2001-12-12T00:00:00Z,2001-12-21T23:59:59Z] - ValueError: time data '2001-12-12' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[2021-02-01--2021-02-01T00:00:00Z,] - ValueError: time data '2021-02-01' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[1999-02-01 06:00-2009-01-01-1999-02-01T06:00:00Z,2009-01-01T23:59:59Z] - ValueError: time data '1999-02-01 06:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[2019-03-10T00:00:00Z-2019-03-10T00:00:00-01:00-2019-03-10T00:00:00Z,2019-03-10T01:00:00Z] - ValueError: time data '2019-03-10T00:00:00-01:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_results.py::TestResults::test_data_links - ValueError: time data '2020' does not match format '%Y-%m-%dT%H:%M:%SZ'
============================================================================================================== 9 failed, 31 passed in 2.56s ============================================================================================================== Integration Test Summary ================================================================================================================ short test summary info =================================================================================================================
FAILED tests/integration/test_auth.py::test_auth_can_read_earthdata_env_variables - AttributeError: 'Auth' object has no attribute 'username'
FAILED tests/integration/test_auth.py::test_auth_can_read_from_netrc_file - AssertionError: False is not true
FAILED tests/integration/test_auth.py::test_auth_populates_attrs - AssertionError: False is not true
FAILED tests/integration/test_auth.py::test_auth_can_fetch_s3_credentials - AssertionError: False is not true
FAILED tests/integration/test_auth.py::test_get_s3_credentials_lowercase_location[location0] - assert {}
FAILED tests/integration/test_auth.py::test_get_s3_credentials_lowercase_location[location1] - assert {}
FAILED tests/integration/test_onprem_download.py::test_earthaccess_can_download_onprem_collection_granules[daac1] - AssertionError: False is not true
FAILED tests/integration/test_onprem_download.py::test_earthaccess_can_download_onprem_collection_granules[daac3] - AssertionError: 0 not greater than 3
FAILED tests/integration/test_onprem_open.py::test_earthaccess_can_open_onprem_collection_granules[daac3] - AssertionError: 0 not greater than 2
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[2001-12-12-2001-12-21-2001-12-12T00:00:00Z,2001-12-21T23:59:59Z] - ValueError: time data '2001-12-12' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[2021-02-01--2021-02-01T00:00:00Z,] - ValueError: time data '2021-02-01' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[1999-02-01 06:00-2009-01-01-1999-02-01T06:00:00Z,2009-01-01T23:59:59Z] - ValueError: time data '1999-02-01 06:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_collection_queries.py::test_query_can_parse_single_dates[2019-03-10T00:00:00Z-2019-03-10T00:00:00-01:00-2019-03-10T00:00:00Z,2019-03-10T01:00:00Z] - ValueError: time data '2019-03-10T00:00:00-01:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[2001-12-12-2001-12-21-2001-12-12T00:00:00Z,2001-12-21T23:59:59Z] - ValueError: time data '2001-12-12' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[2021-02-01--2021-02-01T00:00:00Z,] - ValueError: time data '2021-02-01' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[1999-02-01 06:00-2009-01-01-1999-02-01T06:00:00Z,2009-01-01T23:59:59Z] - ValueError: time data '1999-02-01 06:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_granule_queries.py::test_query_can_parse_single_dates[2019-03-10T00:00:00Z-2019-03-10T00:00:00-01:00-2019-03-10T00:00:00Z,2019-03-10T01:00:00Z] - ValueError: time data '2019-03-10T00:00:00-01:00' does not match format '%Y-%m-%dT%H:%M:%SZ'
FAILED tests/unit/test_results.py::TestResults::test_data_links - ValueError: time data '2020' does not match format '%Y-%m-%dT%H:%M:%SZ'
============================================================================================ 18 failed, 66 passed, 1 skipped, 1 warning in 596.56s (0:09:56) ============================================================================================= It looks like a lot of the tests are failing due to the date format. I pulled in the most recent changes from main to see if they had been fixed but it doesn't look like. I also typically see some test failures when running the integration tests locally. @mfisher87 or @chuckwondo - Do you mind approving the workflows so that they can run? If these pass I think we might be in good shape to merge the PR. |
Workflows kicked off! You should not have to worry about that... I sent you an invitation with "triage" rights (includes ability to add labels, close issues, etc., but not quite as much as "maintainer") you can accept or decline :) |
Github Issue: 447
Description
List available Harmony services for a collection. As a first step to facilitating the use of services in earthaccess, earthaccess was modified so that it can list the available services for a collection.
Overview of work done
A new services module was created that performs service queries. The results module was updated to include a
services
function which will use the services module to query a collection's services and return theprovider-id
andumm
JSON for the service.Sample services results:
Overview of verification done
Tested new service functionality on four collections:
Overview of integration done
Created a new unit test to test DataService
get
. Unit test coverage:Create a new integration test that tests the services functionality from the service query to the parsing of query results. Integration test coverage:
PR checklist:
Pending:
📚 Documentation preview 📚: https://earthaccess--500.org.readthedocs.build/en/500/