Skip to content

Commit eb41433

Browse files
authored
ENH: add data release reprocessing to the release command as another release type (#325)
* ENH: add data release reprocessing to the release command as another release type * adding sleep between upload and release API call to ensure to not have race cases
1 parent e9f45fa commit eb41433

4 files changed

Lines changed: 187 additions & 5 deletions

File tree

imap_data_access/cli.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -734,7 +734,9 @@ def main():
734734
" files to release early.\n"
735735
"- 'unrelease': Unrelease previously released files due to\n"
736736
" various causes and reasons. Use --manifest-file to specify\n"
737-
" files to unrelease."
737+
" files to unrelease.\n"
738+
"- 'reprocess': Trigger reprocessing for the specified "
739+
"--release-number."
738740
),
739741
choices=[e.value for e in ReleaseType],
740742
)
@@ -743,7 +745,10 @@ def main():
743745
type=int,
744746
required=False,
745747
metavar="NUMBER",
746-
help="Release number (required only when --release-type is 'release'). ",
748+
help=(
749+
"Release number (required only when --release-type is "
750+
"'release' or 'reprocess')."
751+
),
747752
)
748753
parser_release.add_argument(
749754
"--exclude-file",

imap_data_access/io.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import logging
55
import os
66
from pathlib import Path
7+
from time import sleep
78
from typing import Optional, Union
89

910
import requests
@@ -565,7 +566,7 @@ def release(
565566
End date in YYYYMMDD format
566567
release_number : int, optional
567568
Release number. Defaults to ``None``. Required if release_type is
568-
'release' and should be an integer value.
569+
'release' or 'reprocess' and should be an integer value.
569570
exclude_file : str, optional
570571
Path to exclude file containing list of files to exclude from public release.
571572
manifest_file : str, optional
@@ -624,16 +625,26 @@ def release(
624625
f"'{release_type}' release type."
625626
)
626627

628+
if release_type == ReleaseType.REPROCESS.value and release_number is None:
629+
raise ValueError(
630+
"The 'release_number' parameter is required for 'reprocess' release type."
631+
)
627632
# Handle exclude file upload if provided
628633
if exclude_file is not None:
629634
# Upload the exclude file using the standard upload function
630635
upload(exclude_file)
636+
# Sleep few seconds to ensure file is uploaded and indexed
637+
# before the release API tries to access it.
638+
sleep(10)
631639
logger.info("Exclude file uploaded successfully")
632640

633641
# Handle manifest file upload if provided
634642
if manifest_file is not None:
635643
# Upload the manifest file using the standard upload function
636644
upload(manifest_file)
645+
# Sleep few seconds to ensure file is uploaded and indexed
646+
# before the release API tries to access it.
647+
sleep(10)
637648
logger.info("Manifest file uploaded successfully")
638649

639650
# Build release parameters
@@ -644,8 +655,8 @@ def release(
644655
"end_date": end_date,
645656
}
646657

647-
# Add release_number only if release_type is 'release'
648-
if release_type == ReleaseType.RELEASE.value:
658+
# Add release_number only if release_type is 'release' or 'reprocess'
659+
if release_type in {ReleaseType.RELEASE.value, ReleaseType.REPROCESS.value}:
649660
release_params["release_number"] = release_number
650661

651662
# Add optional parameters if provided

imap_data_access/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ class ReleaseType(Enum):
99
RELEASE = "release"
1010
EARLY_RELEASE = "early-release"
1111
UNRELEASE = "unrelease"
12+
REPROCESS = "reprocess"

tests/test_io.py

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@
1616
test_science_path = "imap/swe/l1/2010/01/" + test_science_filename
1717

1818

19+
@pytest.fixture
20+
def api_key(monkeypatch):
21+
"""Set a dummy API key so release() doesn't raise on missing key."""
22+
monkeypatch.setitem(imap_data_access.config, "API_KEY", "test-api-key")
23+
24+
1925
@pytest.mark.parametrize(
2026
("url", "api_key", "access_token", "expected"),
2127
[
@@ -794,3 +800,162 @@ def test_reprocess_bad_instrument(mock_send_request):
794800
)
795801
# Should not have made any calls to urlopen
796802
assert mock_send_request.call_count == 0
803+
804+
805+
@pytest.mark.parametrize(
806+
"release_params",
807+
[
808+
{
809+
"instrument": "hit",
810+
"release_type": "release",
811+
"start_date": "20260401",
812+
"end_date": "20260430",
813+
"release_number": 1,
814+
},
815+
{
816+
"instrument": "mag",
817+
"release_type": "release",
818+
"start_date": "20260101",
819+
"end_date": "20260131",
820+
"release_number": 0,
821+
},
822+
],
823+
)
824+
def test_release(mock_send_request, api_key, release_params):
825+
"""Test a successful call to the release API with release type.
826+
827+
Parameters
828+
----------
829+
mock_send_request : unittest.mock.MagicMock
830+
Mock object for requests.Session
831+
api_key : fixture
832+
Sets a dummy API key in the config.
833+
release_params : dict
834+
Parameters passed to release().
835+
"""
836+
mock_response = MagicMock()
837+
mock_response.json.return_value = {}
838+
mock_send_request.return_value = mock_response
839+
840+
imap_data_access.release(**release_params)
841+
842+
mock_send_request.assert_called_once()
843+
sent_request = mock_send_request.call_args[0][0]
844+
assert "/release" in sent_request.url
845+
assert sent_request.method == "GET"
846+
for key, value in release_params.items():
847+
assert f"{key}={value}" in sent_request.url
848+
849+
850+
def test_release_no_api_key(mock_send_request):
851+
"""Test that release() raises when no API key is set."""
852+
with pytest.raises(ValueError, match="API key is required"):
853+
imap_data_access.release(
854+
instrument="hit",
855+
release_type="release",
856+
start_date="20260401",
857+
end_date="20260430",
858+
release_number=1,
859+
)
860+
assert mock_send_request.call_count == 0
861+
862+
863+
def test_release_invalid_release_type(mock_send_request, api_key):
864+
"""Test that release() raises on an unrecognised release_type."""
865+
with pytest.raises(ValueError, match="Not a valid release type"):
866+
imap_data_access.release(
867+
instrument="hit",
868+
release_type="bad-type",
869+
start_date="20260401",
870+
end_date="20260430",
871+
)
872+
assert mock_send_request.call_count == 0
873+
874+
875+
def test_release_invalid_instrument(mock_send_request, api_key):
876+
"""Test that release() raises on an invalid instrument for release type."""
877+
with pytest.raises(ValueError, match="Not a valid instrument"):
878+
imap_data_access.release(
879+
instrument="bad_instrument",
880+
release_type="release",
881+
start_date="20260401",
882+
end_date="20260430",
883+
release_number=1,
884+
)
885+
assert mock_send_request.call_count == 0
886+
887+
888+
def test_release_missing_release_number(mock_send_request, api_key):
889+
"""Test that release() raises when release_number is missing for release type."""
890+
with pytest.raises(ValueError, match=r"release_number.*required"):
891+
imap_data_access.release(
892+
instrument="hit",
893+
release_type="release",
894+
start_date="20260401",
895+
end_date="20260430",
896+
)
897+
assert mock_send_request.call_count == 0
898+
899+
900+
def test_release_invalid_start_date(mock_send_request, api_key):
901+
"""Test that release() raises on a bad start_date."""
902+
with pytest.raises(ValueError, match="Not a valid start date"):
903+
imap_data_access.release(
904+
instrument="hit",
905+
release_type="release",
906+
start_date="bad-date",
907+
end_date="20260430",
908+
release_number=1,
909+
)
910+
assert mock_send_request.call_count == 0
911+
912+
913+
def test_release_invalid_end_date(mock_send_request, api_key):
914+
"""Test that release() raises on a bad end_date."""
915+
with pytest.raises(ValueError, match="Not a valid end date"):
916+
imap_data_access.release(
917+
instrument="hit",
918+
release_type="release",
919+
start_date="20260401",
920+
end_date="bad-date",
921+
release_number=1,
922+
)
923+
assert mock_send_request.call_count == 0
924+
925+
926+
@pytest.mark.parametrize("release_type", ["early-release", "unrelease"])
927+
def test_release_missing_manifest_file(mock_send_request, api_key, release_type):
928+
"""Test that early-release and unrelease require a manifest_file."""
929+
with pytest.raises(ValueError, match=r"manifest_file.*required"):
930+
imap_data_access.release(release_type=release_type)
931+
assert mock_send_request.call_count == 0
932+
933+
934+
def test_release_reprocess_missing_release_number(mock_send_request, api_key):
935+
"""Test that reprocess release type requires release_number."""
936+
with pytest.raises(ValueError, match=r"release_number.*required"):
937+
imap_data_access.release(release_type="reprocess")
938+
assert mock_send_request.call_count == 0
939+
940+
941+
def test_release_early_release_with_manifest(mock_send_request, api_key, tmp_path):
942+
"""Test early-release type uploads manifest and calls the release endpoint."""
943+
manifest = tmp_path / "imap_hit_unrelease_20260101_20260131_v001.txt"
944+
manifest.write_text("imap_hit_l2_standard-intensity_20260101_v001.cdf\n")
945+
946+
# upload() makes two requests: first returns an S3 presigned URL string,
947+
# second (PUT to S3) and the final release GET each return an empty dict.
948+
s3_response = MagicMock()
949+
s3_response.json.return_value = "https://s3-test-bucket.com/manifest"
950+
put_response = MagicMock()
951+
put_response.json.return_value = {}
952+
release_response = MagicMock()
953+
release_response.json.return_value = {}
954+
mock_send_request.side_effect = [s3_response, put_response, release_response]
955+
956+
imap_data_access.release(release_type="early-release", manifest_file=manifest)
957+
958+
assert mock_send_request.call_count == 3
959+
last_request = mock_send_request.call_args[0][0]
960+
assert "/release" in last_request.url
961+
assert "manifest_file=" in last_request.url

0 commit comments

Comments
 (0)