Skip to content

Commit e2f142c

Browse files
authored
Merge pull request #87 from podaac/release/0.3.0
Release/0.3.0
2 parents 7e0529d + 6f53e96 commit e2f142c

17 files changed

Lines changed: 2925 additions & 762 deletions

.github/workflows/cicd-pipeline.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,6 @@ jobs:
279279
TF_VAR_gibs_account_id: ${{ secrets.GIBS_ACCOUNT_ID }}
280280
if: |
281281
github.ref == 'refs/heads/develop' ||
282-
github.ref == 'refs/heads/main' ||
283282
startsWith(github.ref, 'refs/heads/release') ||
284283
github.event_name == 'workflow_dispatch'
285284
steps:

CHANGELOG.md

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

16+
## [0.3.0]
17+
### Added
18+
- [issues/59](https://github.com/podaac/bignbit/issues/59): A new pair of keywords (`dataDayStrategy` and `singleDayNumber`) have been added to the DatasetConfiguration for BIG to enable proper image metadata for annual products. These keywords allow a dataset to override the umm-g date info.
19+
- [issues/84](https://github.com/podaac/bignbit/issues/84): New parameter in dataset config `subdaily` that sends DataDateTime to GIBS instead of DataDay.
20+
- Added optional `concept_id` keyword to dataset config to provide an override for finding the proper CMR collection concept ID when testing.
21+
### Changed
22+
### Deprecated
23+
### Removed
24+
### Fixed
25+
- [issues/82](https://github.com/podaac/bignbit/issues/82): Fixed date parsing bug where ISO-8601 format dates, the default for UMM-G, were not handled properly.
26+
- Update gibs_response_queue visibility timeout to match aws_lambda_function handle_gitc_response timeout
27+
### Security
28+
1629
## [0.2.4]
1730
### Added
1831
- [issues/71](https://github.com/podaac/bignbit/issues/71): New module parameter `cmr_environment` is used to determine which environment to use for CMR requests when processing GIBS responses.

README.md

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,28 @@ should define the bignbit module and the bignbit step function state machine. Se
9999

100100

101101
# Configuring a collection
102+
In order to configure a collection for use with bignbit the following must be done:
102103

103104
1. Add config file to the `config_bucket`. The file should be named "_collection shortname_.cfg" and the contents should be JSON
104105
2. Associate the UMM-C record to the appropriate Harmony service (HyBIG, net2cog, etc...)
105106

107+
The contents of the configuration file should be a valid json object with the following attributes:
108+
109+
| Name | Type | Description |
110+
|--------------------|--------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
111+
| sendToHarmony | boolean | true/false if this collection should be processed using Harmony to generate browse images |
112+
| operaHLSTreatment | boolean | true/false if this collection should have special OPERA_L3_DSWX-HLS processing applied to it (see [apply_opera_hls_treatment](bignbit/apply_opera_hls_treatment.py)) |
113+
| imageFilenameRegex | string | Regular expression used to identify which file in a granule should be used as the image file. Uses first if multiple files match |
114+
| imgVariables | list(object) | List of JSON objects with at least one attribute called `id` whose value is the name of a variable to generate an image for. `all` can be used in cases where the collection does not have variables or all variables in the collection should have images generated |
115+
| height | int | Controls the height of the output image from Harmony (see https://github.com/nasa/harmony-browse-image-generator?tab=readme-ov-file#dimensions--scale-sizes) |
116+
| width | int | Controls the width of the output image from Harmony (see https://github.com/nasa/harmony-browse-image-generator?tab=readme-ov-file#dimensions--scale-sizes) |
117+
| dataDayStrategy | string | [OPTIONAL] (Default: "") If this keyword is set to "single_day_of_year", bignbit will override the date from the granule metadata with the one specified in "singleDayNumber" |
118+
| singleDayNumber | string | [OPTIONAL] (Default: "") If using the "dataDayStrategy" keyword, all granules in this dataset will use the day of year specified in this keyword. ex: "001" for January 1st |
119+
| subdaily | boolean | [OPTIONAL] (Default: False) Set to true if granules contain subdaily data. This will send `DataDateTime` metadata to GIBS as described in the GIBS ICD |
120+
| concept_id | string | [OPTIONAL] (Default: "") Overrides the concept id derived from the granule metadata with this value. ex: "C1996881146-POCLOUD" |
121+
122+
A few example configurations can be found in the [podaac/bignbit-config](https://github.com/podaac/bignbit-config) repository. NOTE: some of the example configurations have other options specified (e.g. `variables`, `latVar`, `lonVar`, etc...) that are no longer supported by this module. The table above are the attributes that are still in use.
123+
106124
## Harmony requests
107125

108126
> [!IMPORTANT]
@@ -189,11 +207,12 @@ _Visual representation of the bignbit step function state machine:_
189207
# Local Development
190208
## MacOS
191209

192-
1. Install miniconda (or conda) and [poetry](https://python-poetry.org/)
210+
1. Install miniconda (or conda) and [poetry](https://python-poetry.org/). On MacOS, you can `brew install miniconda` and `brew install poetry`.
193211
2. Run `conda env create -f conda-environment.yaml` to install GDAL
194-
3. Activate the bignbit conda environment `conda activate bignbit`
195-
4. Install python package and dependencies `poetry install`
196-
5. Verify tests pass `poetry run pytest tests/`
212+
3. **(For MacOS users)** If you have a MacOS system and use zsh, you will need to run `conda init zsh`, close your terminal, and re-open before activating the environment in the next step.
213+
4. Activate the bignbit conda environment `conda activate bignbit`
214+
5. Install python package and dependencies `poetry install`
215+
6. Verify tests pass `poetry run pytest tests/`
197216

198217
> [!IMPORTANT]
199218
> If developing on a `darwin_arm64` based mac, running terraform locally may result
@@ -203,4 +222,4 @@ _Visual representation of the bignbit step function state machine:_
203222
> to compile a local arm-based version of the hashicorp/null provider.
204223
>
205224
> This is only necessary as long as cumulus core requires `~>2.1` version of hashicorp/null because
206-
> v3.x of the provider does have support for `darwin_arm64` platforms
225+
> v3.x of the provider does have support for `darwin_arm64` platforms

bignbit/generate_image_metadata.py

Lines changed: 101 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pathlib
77
import uuid
88
import xml.etree.ElementTree as ET
9-
from datetime import datetime
9+
from datetime import datetime, timedelta
1010
from typing import Dict, List
1111

1212
from cumulus_logger import CumulusLogger
@@ -53,14 +53,36 @@ def process(self) -> List[Dict]:
5353
cma_file_list = [item for sublist in cma_file_list for item in sublist]
5454
granule_umm_json = self.input['granule_umm_json']
5555

56-
file_metadata_list = generate_metadata(cma_file_list, granule_umm_json, pathlib.Path(f"{self.path}"))
56+
# Parse dataset-level overrides
57+
dataset_config = self.input['datasetConfigurationForBIG']['config']
58+
data_day_strat = dataset_config.get('dataDayStrategy')
59+
if data_day_strat is not None and data_day_strat == 'single_day_of_year':
60+
static_data_day = dataset_config.get('singleDayNumber', 1)
61+
# Will throw TypeError on bad configuration
62+
static_data_day = int(static_data_day)
63+
else:
64+
static_data_day = None
65+
66+
subdaily = dataset_config.get('subdaily', False)
67+
68+
file_metadata_list = generate_metadata(
69+
cma_file_list, granule_umm_json,
70+
pathlib.Path(f"{self.path}"),
71+
static_data_day,
72+
subdaily
73+
)
5774
del self.input['granule_umm_json']
5875
del self.input['big']
5976
self.input['big'] = file_metadata_list
6077
return self.input
6178

6279

63-
def generate_metadata(cma_file_list: List[Dict], granule_umm_json: dict, temp_dir: pathlib.Path) -> List[Dict]:
80+
def generate_metadata(
81+
cma_file_list: List[Dict],
82+
granule_umm_json: Dict,
83+
temp_dir: pathlib.Path,
84+
static_data_day: int | None = None,
85+
subdaily: bool = False) -> List[Dict]:
6486
"""
6587
For each file in the list, create an ImageMetadata-v1.2 xml file and upload it to s3 in the same
6688
bucket and path as the image file.
@@ -75,6 +97,11 @@ def generate_metadata(cma_file_list: List[Dict], granule_umm_json: dict, temp_di
7597
umm-json document for the granule being processed
7698
temp_dir
7799
Temporary location to write xml file to prior to upload to s3
100+
static_data_day
101+
Optionally, the DatasetConfiguration can override the date metadata in the
102+
granule umm-json
103+
subdaily
104+
boolean flag if product is subdaily (if True, add DataDateTime to metadata)
78105
79106
Returns
80107
-------
@@ -86,8 +113,14 @@ def generate_metadata(cma_file_list: List[Dict], granule_umm_json: dict, temp_di
86113
granule_filename = cma_file_meta['filename'] if 'filename' in cma_file_meta else cma_file_meta['fileName']
87114
CUMULUS_LOGGER.info(f'Processing file {granule_filename}')
88115

89-
# Get date information from umm-g
90-
begin, mid, end, dataday = extract_granule_dates(granule_umm_json)
116+
if static_data_day is not None and (static_data_day < 1 or static_data_day > 366):
117+
CUMULUS_LOGGER.warning(
118+
f"Specified data day override {static_data_day} is not logical "
119+
"as a day of year. Defaulting to doy 001."
120+
)
121+
static_data_day = 1
122+
# Get date information from umm-g, if static_data_day is None it will be ignored
123+
begin, mid, end, dataday = extract_granule_dates(granule_umm_json, static_data_day)
91124

92125
# Determine type and subtype for CNM
93126
granule_extension = pathlib.Path(granule_filename).suffix
@@ -105,15 +138,15 @@ def generate_metadata(cma_file_list: List[Dict], granule_umm_json: dict, temp_di
105138
except KeyError:
106139
# No partial id for this granule
107140
partial_id = None
108-
CUMULUS_LOGGER.warn(f"No partial id found in metadata for {granule_filename}. Leaving partial id blank.")
141+
CUMULUS_LOGGER.warning(f"No partial id found in metadata for {granule_filename}. Leaving partial id blank.")
109142

110143
# Convert from CMA file dict to CNM file dict
111144
cnm_file_meta = transform_files_to_cnm_product_files(cma_file_meta, granule_type, granule_subtype, dataday)
112145
file_metadata_results.append(cnm_file_meta)
113146

114147
# If this is a browse image, generate image metadata for it and upload it to s3
115148
if granule_type == 'browse':
116-
image_metadata_xml = create_metadata_xml(begin, mid, end, dataday, partial_id)
149+
image_metadata_xml = create_metadata_xml(begin, mid, end, dataday, subdaily, partial_id)
117150
temp_xml_path = write_image_metadata_xml(image_metadata_xml, temp_dir)
118151
image_metadata_xml_metadata = get_file_metadata_for_image_metadta_xml(temp_xml_path, cnm_file_meta)
119152
s3_uri = upload_image_metadata_xml(image_metadata_xml_metadata, temp_xml_path)
@@ -247,7 +280,7 @@ def transform_files_to_cnm_product_files(cma_file_meta: Dict, file_type: str, su
247280
return cnm_file_meta
248281

249282

250-
def extract_granule_dates(granule_umm_json: dict) -> (str, str, str, str):
283+
def extract_granule_dates(granule_umm_json: dict, static_data_day: int | None = None) -> tuple[str, str, str, str]:
251284
"""
252285
Parse the begin, midpoint, end, and dataday for this granule
253286
@@ -266,19 +299,28 @@ def extract_granule_dates(granule_umm_json: dict) -> (str, str, str, str):
266299
dataday
267300
day this granule applies to as string formatted "%Y%j
268301
"""
269-
time_range_dict = granule_umm_json['TemporalExtent']['RangeDateTime']
302+
time_range_dict = granule_umm_json["TemporalExtent"]["RangeDateTime"]
270303

271304
beginning_time_dt = parse_datetime(time_range_dict["BeginningDateTime"])
272305
ending_time_dt = parse_datetime(time_range_dict["EndingDateTime"])
273306
middle_time_dt = beginning_time_dt + (ending_time_dt - beginning_time_dt) / 2
274307

275-
begin = beginning_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
276-
mid = middle_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
277-
end = ending_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
278-
279-
middle_year = middle_time_dt.strftime("%Y")
280-
day_of_year = middle_time_dt.strftime('%j')
281-
dataday = middle_year + day_of_year
308+
if static_data_day is None:
309+
begin = beginning_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
310+
mid = middle_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
311+
end = ending_time_dt.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
312+
313+
middle_year = middle_time_dt.strftime("%Y")
314+
day_of_year = middle_time_dt.strftime("%j")
315+
dataday = middle_year + day_of_year
316+
else:
317+
# If the static_data_day override is set, parse the year from the midpoint
318+
# of the granule, and set all metadata dates to the doy that was set.
319+
data_year = middle_time_dt.year
320+
begin = parse_doy(data_year, static_data_day)
321+
mid = begin
322+
end = begin
323+
dataday = middle_time_dt.strftime("%Y") + f"{static_data_day:03d}"
282324

283325
return begin, mid, end, dataday
284326

@@ -297,14 +339,46 @@ def parse_datetime(datetime_str: str) -> datetime:
297339
datetime
298340
a datetime object
299341
"""
300-
try:
301-
return datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%S.%fZ")
302-
except ValueError:
303-
return datetime.strptime(datetime_str, "%Y-%m-%dT%H:%M:%SZ")
342+
formats = [
343+
"%Y-%m-%dT%H:%M:%S.%fZ", # 2023-01-01T12:30:45.123456Z
344+
"%Y-%m-%dT%H:%M:%SZ", # 2023-01-01T12:30:45Z
345+
"%Y-%m-%dT%H:%M:%S.%f%z", # 2023-01-01T12:30:45.123456+00:00
346+
"%Y-%m-%dT%H:%M:%S%z", # 2023-01-01T12:30:45+00:00
347+
]
348+
349+
for fmt in formats:
350+
try:
351+
return datetime.strptime(datetime_str, fmt)
352+
except ValueError:
353+
continue
354+
355+
# If none of the formats worked
356+
raise ValueError(f"Unable to parse datetime string: {datetime_str}")
357+
358+
359+
def parse_doy(year: int, doy: int) -> str:
360+
"""
361+
Parses a year and day of year into a string.
362+
363+
Parameters
364+
----------
365+
year
366+
integer year (parsed from midpoint of granule time range)
367+
doy
368+
static data day provided in DatasetConfiguration
369+
370+
Returns
371+
-------
372+
str
373+
a static Y-m-d format date string with the time set to midnight UTC
374+
"""
375+
jan_1 = datetime(year, 1, 1)
376+
result_dt = jan_1 + timedelta(days=doy - 1)
377+
return result_dt.strftime("%Y-%m-%dT00:00:00.000000Z")
304378

305379

306380
def create_metadata_xml(beginning_time: str, middle_time: str, ending_time: str, dataday: str,
307-
partial_id: str = None) -> ET.ElementTree:
381+
subdaily: bool = False, partial_id: str | None = None) -> ET.ElementTree:
308382
"""
309383
Create an ImageMetadata-v1.2 XML Element tree
310384
@@ -318,6 +392,8 @@ def create_metadata_xml(beginning_time: str, middle_time: str, ending_time: str,
318392
formatted datetime string for data end date time
319393
dataday
320394
string if format %Y%j for day of year the data represents
395+
subdaily
396+
boolean flag if product is subdaily (if True, add DataDateTime to metadata)
321397
partial_id
322398
partial id associated with data
323399
@@ -334,7 +410,10 @@ def create_metadata_xml(beginning_time: str, middle_time: str, ending_time: str,
334410
ET.SubElement(imagery_metadata, "DataStartDateTime").text = beginning_time
335411
ET.SubElement(imagery_metadata, "DataMidDateTime").text = middle_time
336412
ET.SubElement(imagery_metadata, "DataEndDateTime").text = ending_time
337-
ET.SubElement(imagery_metadata, "DataDay").text = dataday
413+
if subdaily:
414+
ET.SubElement(imagery_metadata, "DataDateTime").text = beginning_time
415+
else:
416+
ET.SubElement(imagery_metadata, "DataDay").text = dataday
338417
if partial_id:
339418
ET.SubElement(imagery_metadata, "PartialId").text = partial_id
340419

bignbit/get_collection_concept_id.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,11 @@ def process(self) -> dict:
3333
collection_shortname = self.config['collection_shortname']
3434
cmr_provider = self.config['cmr_provider']
3535
cmr_environment = self.config['cmr_environment']
36-
collection_id = get_collection_concept_id(collection_shortname, cmr_provider, cmr_environment)
36+
dataset_config = self.input['datasetConfigurationForBIG']['config']
37+
# Use override for collection concept id from dataset config if provided
38+
collection_id = dataset_config.get('concept_id')
39+
if collection_id is None:
40+
collection_id = get_collection_concept_id(collection_shortname, cmr_provider, cmr_environment)
3741
self.input['collection_concept_id'] = collection_id
3842
return self.input
3943

examples/cumulus-tf/bin/fake_gitc.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def handler(event, context):
1414
logger.info(f"Received event {json.dumps(event)}")
1515

1616
for message in event["Records"]:
17-
response_topic_arn = message['messageAttributes']['response_topic_arn']
17+
response_topic_arn = message['messageAttributes']['response_topic_arn']['stringValue']
1818
message_body = loads(message["body"])
1919
logger.info(f"Processing message {json.dumps(message_body)}")
2020

examples/cumulus-tf/browse_image_workflow.tf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ module "bignbit_module" {
2121
count = 1
2222

2323
stage = var.bignbit_stage
24-
cmr_environment = var.bignbit_stage
24+
cmr_environment = var.bignbit_cmr_environment
2525
prefix = var.prefix
2626

2727
data_buckets = [aws_s3_bucket.protected.id, aws_s3_bucket.public.id, aws_s3_bucket.private.id]

examples/cumulus-tf/tfvars/sit.tfvars

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# stage: dev, sandbox, sit, uat, prod
22
bignbit_stage = "sit"
3+
bignbit_cmr_environment = "UAT"
34
prefix = "podaac-sit-svc"
45

56
gibs_region="mocked"

examples/cumulus-tf/tfvars/uat.tfvars

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# stage: dev, sandbox, sit, uat, prod
22
bignbit_stage = "uat"
3+
bignbit_cmr_environment = "UAT"
34
prefix = "podaac-uat-svc"
45

56
gibs_region="us-east-1"

examples/cumulus-tf/variable.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ variable "bignbit_stage" {
2323
type = string
2424
description = "environment used for tagging resources"
2525
}
26+
27+
variable "bignbit_cmr_environment" {
28+
type = string
29+
description = "cmr environment used for bignbit"
30+
}
31+
2632
variable "big_throttled_execution_limit" {
2733
type = number
2834
default = 50

0 commit comments

Comments
 (0)