Skip to content

Commit 222365b

Browse files
authored
Merge pull request #129 from podaac/release/0.10.0
Release/0.10.0
2 parents 705ac78 + 5f8e1b5 commit 222365b

File tree

15 files changed

+1766
-1266
lines changed

15 files changed

+1766
-1266
lines changed

.github/workflows/build-pipeline.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ jobs:
8585
run: |
8686
poetry run pytest --junitxml=build/reports/pytest.xml --cov=podaac/ --cov-report=xml:build/reports/coverage.xml -m "not aws and not integration" tests/
8787
- name: SonarCloud Scan
88-
uses: sonarsource/sonarcloud-github-action@master
88+
uses: sonarsource/sonarqube-scan-action@v4
8989
env:
9090
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
9191
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
@@ -243,6 +243,29 @@ jobs:
243243
pull: true
244244
tags: ${{ steps.meta.outputs.tags }}
245245
labels: ${{ steps.meta.outputs.labels }}
246+
- name: Deploy Harmony
247+
env:
248+
ENV: ${{ env.venue }}
249+
CMR_USER: ${{ secrets.CMR_USER }}
250+
CMR_PASS: ${{ secrets.CMR_PASS }}
251+
if: |
252+
github.ref == 'refs/heads/main' ||
253+
startsWith(github.ref, 'refs/heads/release')
254+
working-directory: deployment
255+
run:
256+
poetry run python harmony_deploy.py --tag ${{ env.software_version }}
257+
- name: Create Release
258+
id: create_release
259+
if: |
260+
github.ref == 'refs/heads/main'
261+
uses: softprops/action-gh-release@v2
262+
with:
263+
tag_name: "${{ env.software_version }}" # Use the tag that triggered the action
264+
release_name: Release v{{ env.software_version }}
265+
draft: false
266+
generate_release_notes: true
267+
token: ${{ secrets.GITHUB_TOKEN }}
268+
246269
# As of 2023/01/23 these steps below for scanning the Docker image with Snyk are failing. I've tried both the official Snyk
247270
# action https://github.com/snyk/actions/tree/master/docker and this method below of manually calling the CLI.
248271
# The error when using the official Snyk action is

.pylintrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -416,7 +416,8 @@ disable=raw-checker-failed,
416416
useless-suppression,
417417
deprecated-pragma,
418418
use-symbolic-message-instead,
419-
too-many-arguments
419+
too-many-arguments,
420+
too-many-positional-arguments
420421

421422
# Enable the message, report, category or checker with the given id(s). You can
422423
# either give multiple identifier separated by comma (,) or put this option

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
### Fixed
1414

1515

16+
## [0.10.0]
17+
18+
### Added
19+
- Update Github Actions
20+
- Added harmony deployment into github actions.
21+
### Changed
22+
- [issue #117](https://github.com/podaac/concise/issues/117): Add part of URL to output file name
23+
- Update python libraries
24+
- Update harmony service lib that changed project structure
25+
- Add Concise exception to propogate up to harmony api calls.
26+
### Deprecated
27+
### Removed
28+
### Fixed
29+
- Variable Merging
30+
- Fixed way we merge variables when granules in a collection have varying variables.
31+
32+
1633
## [0.9.0]
1734

1835
### Added

LICENSE

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@
186186
same "printed page" as the copyright notice for easier
187187
identification within third-party archives.
188188

189-
Copyright [yyyy] [name of copyright owner]
189+
Copyright 2024 California Institute of Technology
190190

191191
Licensed under the Apache License, Version 2.0 (the "License");
192192
you may not use this file except in compliance with the License.

deployment/harmony_deploy.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import os
2+
import requests
3+
import json
4+
import logging
5+
import argparse
6+
from requests.auth import HTTPBasicAuth
7+
8+
# Environment variables
9+
ENV = os.getenv('ENV')
10+
CMR_USER = os.getenv('CMR_USER')
11+
CMR_PASS = os.getenv('CMR_PASS')
12+
13+
def bearer_token() -> str:
14+
tokens = []
15+
headers = {'Accept': 'application/json'}
16+
url = f"https://{'uat.' if ENV == 'uat' else ''}urs.earthdata.nasa.gov/api/users"
17+
18+
# First just try to get a token that already exists
19+
try:
20+
resp = requests.get(url + "/tokens", headers=headers, auth=HTTPBasicAuth(CMR_USER, CMR_PASS))
21+
response_content = json.loads(resp.content)
22+
23+
for x in response_content:
24+
tokens.append(x['access_token'])
25+
26+
except Exception: # noqa E722
27+
logging.warning("Error getting the token - check user name and password", exc_info=True)
28+
29+
# No tokens exist, try to create one
30+
if not tokens:
31+
try:
32+
resp = requests.post(url + "/token", headers=headers, auth=HTTPBasicAuth(CMR_USER, CMR_PASS))
33+
response_content = json.loads(resp.content)
34+
tokens.append(response_content['access_token'])
35+
except Exception: # noqa E722
36+
logging.warning("Error getting the token - check user name and password", exc_info=True)
37+
38+
# If still no token, then we can't do anything
39+
if not tokens:
40+
raise RuntimeError("Unable to get bearer token from EDL")
41+
42+
return next(iter(tokens))
43+
44+
if __name__ == "__main__":
45+
46+
parser = argparse.ArgumentParser(description="Update the service image tag.")
47+
parser.add_argument("--tag", help="The new tag version to update.", required=True)
48+
args = parser.parse_args()
49+
50+
url = f"https://harmony.{'uat.' if ENV == 'uat' else ''}earthdata.nasa.gov/service-image-tag/podaac-concise"
51+
token = bearer_token()
52+
53+
headers = {
54+
"Authorization": f"Bearer {token}",
55+
"Content-type": "application/json"
56+
}
57+
data = {
58+
"tag": args.tag
59+
}
60+
61+
response = requests.put(url, headers=headers, json=data)
62+
response.raise_for_status()
63+
64+
print(f"Response JSON: {response.json()}")

podaac/merger/harmony/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"""A Harmony CLI wrapper around Concise"""
22

33
from argparse import ArgumentParser
4-
import harmony
4+
import harmony_service_lib as harmony
55
from podaac.merger.harmony.service import ConciseService
66

77

podaac/merger/harmony/download_worker.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
import re
99
from urllib.parse import urlparse
1010

11-
from harmony.logging import build_logger
12-
from harmony.util import download
11+
from harmony_service_lib.logging import build_logger
12+
from harmony_service_lib.util import download
1313

1414

1515
def multi_core_download(urls, destination_dir, access_token, cfg, process_count=None):

podaac/merger/harmony/service.py

Lines changed: 96 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,12 @@
66
from shutil import copyfile
77
from urllib.parse import urlsplit
88
from uuid import uuid4
9+
import traceback
10+
import sys
911

10-
from harmony.adapter import BaseHarmonyAdapter
11-
from harmony.util import bbox_to_geometry, stage
12+
from harmony_service_lib.adapter import BaseHarmonyAdapter
13+
from harmony_service_lib.util import bbox_to_geometry, stage
14+
from harmony_service_lib.exceptions import HarmonyException
1215
from pystac import Catalog, Item
1316
from pystac.item import Asset
1417

@@ -20,6 +23,37 @@
2023
NETCDF4_MIME = 'application/x-netcdf4' # pylint: disable=invalid-name
2124

2225

26+
class ConciseException(HarmonyException):
27+
"""Concise Exception class for custom error messages to see in harmony api calls."""
28+
def __init__(self, original_exception):
29+
# Ensure we can extract traceback information
30+
if original_exception.__traceback__ is None:
31+
# Capture the current traceback if not already present
32+
try:
33+
raise original_exception
34+
except type(original_exception):
35+
original_exception.__traceback__ = sys.exc_info()[2]
36+
37+
# Extract the last traceback entry (most recent call) for the error location
38+
tb = traceback.extract_tb(original_exception.__traceback__)[-1]
39+
40+
# Get the error details: file, line, function, and message
41+
filename = tb.filename
42+
lineno = tb.lineno
43+
funcname = tb.name
44+
error_msg = str(original_exception)
45+
46+
# Format the error message to be more readable
47+
readable_message = (f"Error in file '{filename}', line {lineno}, in function '{funcname}': "
48+
f"{error_msg}")
49+
50+
# Call the parent class constructor with the formatted message and category
51+
super().__init__(readable_message, 'podaac/concise')
52+
53+
# Store the original exception for potential further investigation
54+
self.original_exception = original_exception
55+
56+
2357
class ConciseService(BaseHarmonyAdapter):
2458
"""
2559
A harmony-service-lib wrapper around the Concise module. This wrapper does
@@ -32,7 +66,7 @@ def invoke(self):
3266
Primary entrypoint into the service wrapper. Overrides BaseHarmonyAdapter.invoke
3367
"""
3468
if not self.catalog:
35-
# Message-only support is being depreciated in Harmony so we should expect to
69+
# Message-only support is being depreciated in Harmony, so we should expect to
3670
# only see requests with catalogs when invoked with a newer Harmony instance
3771
# https://github.com/nasa/harmony-service-lib-py/blob/21bcfbda17caf626fb14d2ac4f8673be9726b549/harmony/adapter.py#L71
3872
raise RuntimeError('Invoking CONCISE without a STAC catalog is not supported')
@@ -42,7 +76,7 @@ def invoke(self):
4276
def process_catalog(self, catalog: Catalog):
4377
"""
4478
Recursively process a catalog and all its children. Adapted from
45-
BaseHarmonyAdapter._process_catalog_recursive to specfifically
79+
BaseHarmonyAdapter._process_catalog_recursive to specifically
4680
support our particular use case for many-to-one
4781
4882
Parameters
@@ -55,60 +89,68 @@ def process_catalog(self, catalog: Catalog):
5589
pystac.Catalog
5690
A new catalog containing the results from the merge
5791
"""
58-
result = catalog.clone()
59-
result.id = str(uuid4())
60-
result.clear_children()
6192

62-
# Get all the items from the catalog, including from child or linked catalogs
63-
items = list(self.get_all_catalog_items(catalog))
93+
try:
94+
result = catalog.clone()
95+
result.id = str(uuid4())
96+
result.clear_children()
97+
98+
# Get all the items from the catalog, including from child or linked catalogs
99+
items = list(self.get_all_catalog_items(catalog))
100+
101+
# Quick return if catalog contains no items
102+
if len(items) == 0:
103+
return result
104+
105+
# -- Process metadata --
106+
bbox = []
107+
granule_urls = []
108+
datetimes = [
109+
datetime.max.replace(tzinfo=timezone.utc), # start
110+
datetime.min.replace(tzinfo=timezone.utc) # end
111+
]
112+
113+
for item in items:
114+
get_bbox(item, bbox)
115+
get_granule_url(item, granule_urls)
116+
get_datetime(item, datetimes)
117+
118+
# Items did not have a bbox; valid under spec
119+
if len(bbox) == 0:
120+
bbox = None
121+
122+
# -- Perform merging --
123+
collection = self._get_item_source(items[0]).collection
124+
first_granule_url = []
125+
get_granule_url(items[0], first_granule_url)
126+
first_url_name = Path(first_granule_url[0]).stem
127+
filename = f'{first_url_name}_{datetimes[1].strftime("%Y%m%dT%H%M%SZ")}_{collection}_merged.nc4'
128+
129+
with TemporaryDirectory() as temp_dir:
130+
self.logger.info('Starting granule downloads')
131+
input_files = multi_core_download(granule_urls, temp_dir, self.message.accessToken, self.config)
132+
self.logger.info('Finished granule downloads')
133+
134+
output_path = Path(temp_dir).joinpath(filename).resolve()
135+
merge_netcdf_files(input_files, output_path, granule_urls, logger=self.logger)
136+
staged_url = self._stage(str(output_path), filename, NETCDF4_MIME)
137+
138+
# -- Output to STAC catalog --
139+
result.clear_items()
140+
properties = {
141+
"start_datetime": datetimes[0].isoformat(),
142+
"end_datetime": datetimes[1].isoformat()
143+
}
144+
145+
item = Item(str(uuid4()), bbox_to_geometry(bbox), bbox, None, properties)
146+
asset = Asset(staged_url, title=filename, media_type=NETCDF4_MIME, roles=['data'])
147+
item.add_asset('data', asset)
148+
result.add_item(item)
64149

65-
# Quick return if catalog contains no items
66-
if len(items) == 0:
67150
return result
68151

69-
# -- Process metadata --
70-
bbox = []
71-
granule_urls = []
72-
datetimes = [
73-
datetime.max.replace(tzinfo=timezone.utc), # start
74-
datetime.min.replace(tzinfo=timezone.utc) # end
75-
]
76-
77-
for item in items:
78-
get_bbox(item, bbox)
79-
get_granule_url(item, granule_urls)
80-
get_datetime(item, datetimes)
81-
82-
# Items did not have a bbox; valid under spec
83-
if len(bbox) == 0:
84-
bbox = None
85-
86-
# -- Perform merging --
87-
collection = self._get_item_source(items[0]).collection
88-
filename = f'{collection}_merged.nc4'
89-
90-
with TemporaryDirectory() as temp_dir:
91-
self.logger.info('Starting granule downloads')
92-
input_files = multi_core_download(granule_urls, temp_dir, self.message.accessToken, self.config)
93-
self.logger.info('Finished granule downloads')
94-
95-
output_path = Path(temp_dir).joinpath(filename).resolve()
96-
merge_netcdf_files(input_files, output_path, granule_urls, logger=self.logger)
97-
staged_url = self._stage(str(output_path), filename, NETCDF4_MIME)
98-
99-
# -- Output to STAC catalog --
100-
result.clear_items()
101-
properties = {
102-
"start_datetime": datetimes[0].isoformat(),
103-
"end_datetime": datetimes[1].isoformat()
104-
}
105-
106-
item = Item(str(uuid4()), bbox_to_geometry(bbox), bbox, None, properties)
107-
asset = Asset(staged_url, title=filename, media_type=NETCDF4_MIME, roles=['data'])
108-
item.add_asset('data', asset)
109-
result.add_item(item)
110-
111-
return result
152+
except Exception as ex:
153+
raise ConciseException(ex) from ex
112154

113155
def _stage(self, local_filename, remote_filename, mime):
114156
"""

podaac/merger/merge.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def is_file_empty(parent_group: nc.Dataset | nc.Group) -> bool:
2424
return True
2525

2626

27-
def merge_netcdf_files(original_input_files: list[Path], # pylint: disable=too-many-locals
27+
def merge_netcdf_files(original_input_files: list[Path], # pylint: disable=too-many-locals,too-many-positional-arguments
2828
output_file: str,
2929
granule_urls,
3030
logger=getLogger(__name__),

podaac/merger/merge_worker.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def max_var_memory(file_list: list[Path], var_info: dict, max_dims) -> int:
5858
return max_var_mem
5959

6060

61+
# pylint: disable=too-many-positional-arguments
6162
def run_merge(merged_dataset: nc.Dataset,
6263
file_list: list[Path],
6364
var_info: dict,

0 commit comments

Comments
 (0)