diff --git a/src/metpy/remote/aws.py b/src/metpy/remote/aws.py
index 0b52540678e..af0db148698 100644
--- a/src/metpy/remote/aws.py
+++ b/src/metpy/remote/aws.py
@@ -176,12 +176,86 @@ def objects(self, prefix):
def _closest_result(self, it, dt):
"""Iterate over a sequence and return a result built from the closest match."""
- try:
- min_obj = min(it,
- key=lambda o: abs((self.dt_from_key(o.key) - dt).total_seconds()))
- except ValueError as e:
- raise ValueError(f'No result found for {dt}') from e
- return self._build_result(min_obj)
+ best_obj = None
+ best_diff = None
+ for obj in it:
+ try:
+ obj_dt = self.dt_from_key(obj.key)
+ diff = abs(obj_dt - dt)
+ if best_diff is None or diff < best_diff:
+ best_obj = obj
+ best_diff = diff
+ except (ValueError, IndexError):
+ pass
+ if best_obj is None:
+ raise ValueError('No matching products found.')
+ return self._build_result(best_obj)
+
+ def _find_best_product(self, objects_iter, dt, filters=None):
+ """Find the best product from a sequence based on time and optional filters.
+
+ Parameters
+ ----------
+ objects_iter : iterable
+ Iterable of S3 objects to search through
+ dt : datetime.datetime
+ Target datetime to match
+ filters : dict, optional
+ Dictionary of attribute names and values to filter objects by.
+ For example, {'sector': 'M1', 'band': '02'}
+
+ Returns
+ -------
+ object
+ The best matching S3 object
+
+ Raises
+ ------
+ ValueError
+ If no matching products are found
+ """
+ best_obj = None
+ best_diff = None
+
+ for obj in objects_iter:
+ try:
+ # Skip if it doesn't match our filters
+ if filters and not self._matches_filters(obj.key, filters):
+ continue
+
+ obj_dt = self.dt_from_key(obj.key)
+ diff = abs(obj_dt - dt)
+ if best_diff is None or diff < best_diff:
+ best_obj = obj
+ best_diff = diff
+ except (ValueError, IndexError):
+ pass
+
+ if best_obj is None:
+ filter_desc = '' if not filters else f' matching filters {filters}'
+ raise ValueError(f'No matching products found{filter_desc}.')
+
+ return self._build_result(best_obj)
+
+ def _matches_filters(self, key, filters):
+ """Check if a key matches all specified filters.
+
+ This is a generic method that should be overridden by subclasses
+ that need specific filtering logic.
+
+ Parameters
+ ----------
+ key : str
+ The S3 object key to check
+ filters : dict
+ Dictionary of attribute names and values to filter by
+
+ Returns
+ -------
+ bool
+ True if the key matches all filters, False otherwise
+ """
+ return True
def _build_result(self, obj):
"""Build a basic product with no reader."""
@@ -470,6 +544,14 @@ class GOESArchive(S3DataStore):
This consists of individual GOES image files stored in netCDF format, across a variety
of sectors, bands, and modes.
+ GOES filenames follow the pattern:
+ OR_ABI-L1b-RadX-MYC##_G##_s########_e########_c########.nc
+
+ Where:
+ - X is the sector (F=Full Disk, C=CONUS, M1=Mesoscale 1, M2=Mesoscale 2)
+ - Y is the mode (3, 4, 6)
+ - ## is the channel/band (01-16)
+
"""
def __init__(self, satellite):
@@ -559,7 +641,22 @@ def get_product(self, product, dt=None, mode=None, band=None):
dt = datetime.now(timezone.utc) if dt is None else ensure_timezone(dt)
time_prefix = self._build_time_prefix(product, dt)
prod_prefix = self._subprod_prefix(time_prefix, mode, band)
- return self._closest_result(self.objects(prod_prefix), dt)
+
+ # Extract sector from product name (e.g., 'M1' from 'ABI-L1b-RadM1')
+ sector = None
+ if product.endswith(('M1', 'M2')):
+ sector = product[-2:]
+
+ # Build filters dictionary for precise matching
+ filters = {}
+ if sector:
+ filters['sector'] = sector
+ if band is not None:
+ filters['band'] = f'{int(band):02d}' if isinstance(band, int) else band
+ if mode is not None:
+ filters['mode'] = str(mode)
+
+ return self._find_best_product(self.objects(prod_prefix), dt, filters)
def get_range(self, product, start, end, mode=None, band=None):
"""Yield products within a particular date/time range.
@@ -589,12 +686,80 @@ def get_range(self, product, start, end, mode=None, band=None):
"""
start = ensure_timezone(start)
end = ensure_timezone(end)
+
+ # Extract sector from product name (e.g., 'M1' from 'ABI-L1b-RadM1')
+ sector = None
+ if product.endswith(('M1', 'M2')):
+ sector = product[-2:]
+
+ # Build filters dictionary for precise matching
+ filters = {}
+ if sector:
+ filters['sector'] = sector
+ if band is not None:
+ filters['band'] = f'{int(band):02d}' if isinstance(band, int) else band
+ if mode is not None:
+ filters['mode'] = str(mode)
+
for dt in date_iterator(start, end, hours=1):
time_prefix = self._build_time_prefix(product, dt)
prod_prefix = self._subprod_prefix(time_prefix, mode, band)
for obj in self.objects(prod_prefix):
- if start <= self.dt_from_key(obj.key) < end:
- yield self._build_result(obj)
+ obj_dt = self.dt_from_key(obj.key)
+ # Check if object is within time range and matches filters
+ matches_time = start <= obj_dt < end
+ matches_filters = not filters or self._matches_filters(obj.key, filters)
+ if matches_time and matches_filters:
+ # Only yield if it matches our filters
+ yield self._build_result(obj)
+
+ def _matches_filters(self, key, filters):
+ """Check if a GOES product key matches all specified filters.
+
+ Parameters
+ ----------
+ key : str
+ The S3 object key to check
+ filters : dict
+ Dictionary of attribute names and values to filter by
+
+ Returns
+ -------
+ bool
+ True if the key matches all filters, False otherwise
+ """
+ # Parse the filename from the key
+ filename = key.split('/')[-1]
+ parts = filename.split('_')
+ if len(parts) < 2:
+ return False
+
+ # Parse product info from filename (e.g., 'OR_ABI-L1b-RadM1-M6C02_G18_s...')
+ product_info = parts[1]
+
+ # Check sector filter (M1, M2, C, F)
+ if 'sector' in filters:
+ sector = filters['sector']
+ # For mesoscale sectors, check if the product has the right sector
+ # Check for mesoscale sectors (M1, M2)
+ if (sector in ('M1', 'M2') and
+ not product_info.endswith(sector + '-') and
+ ('-Rad' + sector + '-') not in product_info):
+ return False
+
+ # Check band filter
+ if 'band' in filters:
+ band = filters['band']
+ if f'C{band}' not in product_info:
+ return False
+
+ # Check mode filter
+ if 'mode' in filters:
+ mode = filters['mode']
+ if f'-M{mode}' not in product_info:
+ return False
+
+ return True
def _build_result(self, obj):
"""Build a product that opens the data using `xarray.open_dataset`."""
diff --git a/test_goes_client.py b/test_goes_client.py
new file mode 100644
index 00000000000..281e3eb93a2
--- /dev/null
+++ b/test_goes_client.py
@@ -0,0 +1,45 @@
+#!/usr/bin/env python
+# Copyright (c) 2015-2025 MetPy Developers.
+"""Test script to verify the robustness of the GOES client at hour boundaries.
+
+This script tests the recursive search implementation for finding products
+across hour boundaries.
+"""
+import logging
+from datetime import datetime, timezone
+
+from metpy.remote import GOESArchive
+
+logger = logging.getLogger(__name__)
+
+def test_goes_hour_boundary():
+ """Test the GOES client's ability to find products across hour boundaries."""
+ # Create a GOES client
+ goes = GOESArchive(16)
+ # Test case 1: Exact hour boundary
+ try:
+ dt = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ goes.get_product('ABI-L1b-RadC', dt, band=1)
+ except Exception:
+ logger.exception('Failed to get product at exact hour boundary')
+ # Test case 2: Just after hour boundary
+ try:
+ dt = datetime(2025, 1, 1, 0, 0, 30, tzinfo=timezone.utc)
+ goes.get_product('ABI-L1b-RadC', dt, band=1)
+ except Exception:
+ logger.exception('Failed to get product just after hour boundary')
+ # Test case 3: Just before hour boundary
+ try:
+ dt = datetime(2025, 1, 1, 0, 59, 30, tzinfo=timezone.utc)
+ goes.get_product('ABI-L1b-RadC', dt, band=1)
+ except Exception:
+ logger.exception('Failed to get product just before hour boundary')
+ # Test case 4: Day boundary
+ try:
+ dt = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ goes.get_product('ABI-L1b-RadC', dt, band=1)
+ except Exception:
+ logger.exception('Failed to get product at day boundary')
+
+if __name__ == '__main__':
+ test_goes_hour_boundary()
diff --git a/tests/remote/fixtures/test_goes_range.yaml b/tests/remote/fixtures/test_goes_range.yaml
index 59e56302fd7..e4561183a6b 100644
--- a/tests/remote/fixtures/test_goes_range.yaml
+++ b/tests/remote/fixtures/test_goes_range.yaml
@@ -4,17 +4,58 @@ interactions:
headers:
User-Agent:
- !!binary |
- Qm90bzMvMS4zNS44OCBtZC9Cb3RvY29yZSMxLjM1Ljg4IHVhLzIuMCBvcy9tYWNvcyMyNC4yLjAg
- bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjcgbWQvcHlpbXBsI0NQeXRob24gY2ZnL3Jl
- dHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuMzUuODggUmVzb3VyY2U=
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEQs
+ YiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS4zOC40MiBSZXNvdXJjZQ==
amz-sdk-invocation-id:
- !!binary |
- OGJhOGU3NmUtOWFkNC00NjQ1LWJkM2EtMGU3NDY2MGNjMTY0
+ N2IyZGMwZTQtNDc2OS00MmJiLWI3OGYtZmFmMzM5ZTk4OWU1
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: GET
- uri: https://noaa-goes16.s3.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadC%2F2024%2F345%2F01%2FOR_ABI-L1b-RadC-&delimiter=_&encoding-type=url
+ uri: https://noaa-goes16.s3.us-east-2.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadC%2F2024%2F345%2F01%2FOR_ABI-L1b-RadC-&delimiter=_&encoding-type=url
+ response:
+ body:
+ string: '
+
+ PermanentRedirectThe bucket you are attempting
+ to access must be addressed using the specified endpoint. Please send all
+ future requests to this endpoint.s3.amazonaws.comnoaa-goes16HY3ZX6QYFMG28GK2RPddz7APKHvZLEGjdwSKVRhVseEXeLF/hKxfJINRaam1M5SbcLTuUn9Uj2nsH6ua6ArnMl3zADQ='
+ headers:
+ Content-Type:
+ - application/xml
+ Date:
+ - Sun, 06 Jul 2025 19:22:34 GMT
+ Server:
+ - AmazonS3
+ Transfer-Encoding:
+ - chunked
+ x-amz-bucket-region:
+ - us-east-1
+ x-amz-id-2:
+ - RPddz7APKHvZLEGjdwSKVRhVseEXeLF/hKxfJINRaam1M5SbcLTuUn9Uj2nsH6ua6ArnMl3zADQ=
+ x-amz-request-id:
+ - HY3ZX6QYFMG28GK2
+ status:
+ code: 301
+ message: Moved Permanently
+- request:
+ body: null
+ headers:
+ User-Agent:
+ - !!binary |
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEQs
+ YiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS4zOC40MiBSZXNvdXJjZQ==
+ amz-sdk-invocation-id:
+ - !!binary |
+ N2IyZGMwZTQtNDc2OS00MmJiLWI3OGYtZmFmMzM5ZTk4OWU1
+ amz-sdk-request:
+ - !!binary |
+ YXR0ZW1wdD0yOyBtYXg9NQ==
+ method: GET
+ uri: https://noaa-goes16.s3.us-east-1.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadC%2F2024%2F345%2F01%2FOR_ABI-L1b-RadC-&delimiter=_&encoding-type=url
response:
body:
string: '
@@ -24,7 +65,7 @@ interactions:
Content-Type:
- application/xml
Date:
- - Fri, 10 Jan 2025 02:09:09 GMT
+ - Sun, 06 Jul 2025 19:22:36 GMT
Server:
- AmazonS3
Transfer-Encoding:
@@ -32,9 +73,9 @@ interactions:
x-amz-bucket-region:
- us-east-1
x-amz-id-2:
- - f2SLO60WyihXD/REdoHXhu++F4YjkDNmyN6viuJblxa1UlTUD4z6VfVbGhg5NRBw0IADG/3vQjou1JOuyGxYpQ==
+ - bOvXRYTQU6dLHyCTKdITuWRXnIB8zE9YFkLDjgJKwV8LZWb9BfyG0yg2FoP1a402y8ALikxuveM=
x-amz-request-id:
- - EJ045912YFMVHY92
+ - HY3N1K4YTXPBV6N3
status:
code: 200
message: OK
@@ -43,17 +84,17 @@ interactions:
headers:
User-Agent:
- !!binary |
- Qm90bzMvMS4zNS44OCBtZC9Cb3RvY29yZSMxLjM1Ljg4IHVhLzIuMCBvcy9tYWNvcyMyNC4yLjAg
- bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjcgbWQvcHlpbXBsI0NQeXRob24gY2ZnL3Jl
- dHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuMzUuODggUmVzb3VyY2U=
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEMs
+ RCxiIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjM4LjQyIFJlc291cmNl
amz-sdk-invocation-id:
- !!binary |
- NjFhZDIzYmMtNGM1Yy00OTY2LWI3ZDgtYzczMTI5OGVkMzFl
+ YjVmMjJhYmItYzg4NC00ZmNmLWI1ZTYtNjQwMWNmMmI2MWFk
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: GET
- uri: https://noaa-goes16.s3.amazonaws.com/?prefix=ABI-L1b-RadC%2F2024%2F345%2F01%2FOR_ABI-L1b-RadC-M6C01&encoding-type=url
+ uri: https://noaa-goes16.s3.us-east-1.amazonaws.com/?prefix=ABI-L1b-RadC%2F2024%2F345%2F01%2FOR_ABI-L1b-RadC-M6C01&encoding-type=url
response:
body:
string: '
@@ -63,7 +104,7 @@ interactions:
Content-Type:
- application/xml
Date:
- - Fri, 10 Jan 2025 02:09:09 GMT
+ - Sun, 06 Jul 2025 19:22:36 GMT
Server:
- AmazonS3
Transfer-Encoding:
@@ -71,9 +112,9 @@ interactions:
x-amz-bucket-region:
- us-east-1
x-amz-id-2:
- - 7mufcJlqux7+INRgofp1gQvd8Vl6shpIjUuhOQec5yuYJu+EMWniJBXMCi+G4pXjXJYJk3/3WLh4eZ2WKo7sxw==
+ - peUA09hzI9ThPM/OgMjtvExQfHU6ato2PZGUJzGdMNYQaeThlOYU3RBl8oJFQB6adqdc0gQU7z8=
x-amz-request-id:
- - EJ0AWGMVW48967NV
+ - HY3QFRBQXGYGA2JM
status:
code: 200
message: OK
@@ -82,17 +123,17 @@ interactions:
headers:
User-Agent:
- !!binary |
- Qm90bzMvMS4zNS44OCBtZC9Cb3RvY29yZSMxLjM1Ljg4IHVhLzIuMCBvcy9tYWNvcyMyNC4yLjAg
- bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjcgbWQvcHlpbXBsI0NQeXRob24gY2ZnL3Jl
- dHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuMzUuODggUmVzb3VyY2U=
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEQs
+ YiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS4zOC40MiBSZXNvdXJjZQ==
amz-sdk-invocation-id:
- !!binary |
- YzZhNDc4ZjEtNmY4Ny00NDRlLWIyMjItNGEwOGIzMWZiYzNk
+ ZjdmOWI0MDAtMWI4ZS00NmJjLWIxMTctNGU4MWQ0ZDQ4M2Uy
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: GET
- uri: https://noaa-goes16.s3.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadC%2F2024%2F345%2F02%2FOR_ABI-L1b-RadC-&delimiter=_&encoding-type=url
+ uri: https://noaa-goes16.s3.us-east-1.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadC%2F2024%2F345%2F02%2FOR_ABI-L1b-RadC-&delimiter=_&encoding-type=url
response:
body:
string: '
@@ -102,7 +143,7 @@ interactions:
Content-Type:
- application/xml
Date:
- - Fri, 10 Jan 2025 02:09:09 GMT
+ - Sun, 06 Jul 2025 19:22:36 GMT
Server:
- AmazonS3
Transfer-Encoding:
@@ -110,9 +151,9 @@ interactions:
x-amz-bucket-region:
- us-east-1
x-amz-id-2:
- - tAqoJiBkXJdHKrtpWWZb4Yt8DphggjAKI8OJC2MiGKmNJWBLk5VHaHwZEtUMt+BSZWiwddKw97WJmjiBthZe0Q==
+ - F/4ZOyIGH+pHVCGwOQrqIAmwf6aSWMorFfKelg2uvk0ySyU3ELRCFSyxdFfm6rriBHXuadIYvq0=
x-amz-request-id:
- - EJ03B4YM36WGMTTA
+ - HY3PBEFX6JFMS9FK
status:
code: 200
message: OK
@@ -121,17 +162,17 @@ interactions:
headers:
User-Agent:
- !!binary |
- Qm90bzMvMS4zNS44OCBtZC9Cb3RvY29yZSMxLjM1Ljg4IHVhLzIuMCBvcy9tYWNvcyMyNC4yLjAg
- bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjcgbWQvcHlpbXBsI0NQeXRob24gY2ZnL3Jl
- dHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuMzUuODggUmVzb3VyY2U=
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEMs
+ RCxiIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjM4LjQyIFJlc291cmNl
amz-sdk-invocation-id:
- !!binary |
- NGNhZmJhNmMtNTM2YS00NWM0LThjNGYtNDRjZjliMWQ3NTcz
+ YmRkZTlkYmYtOTc2NS00NWIxLWEwNzYtMTExYzVhMDBkYjZm
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: GET
- uri: https://noaa-goes16.s3.amazonaws.com/?prefix=ABI-L1b-RadC%2F2024%2F345%2F02%2FOR_ABI-L1b-RadC-M6C01&encoding-type=url
+ uri: https://noaa-goes16.s3.us-east-1.amazonaws.com/?prefix=ABI-L1b-RadC%2F2024%2F345%2F02%2FOR_ABI-L1b-RadC-M6C01&encoding-type=url
response:
body:
string: '
@@ -141,7 +182,7 @@ interactions:
Content-Type:
- application/xml
Date:
- - Fri, 10 Jan 2025 02:09:09 GMT
+ - Sun, 06 Jul 2025 19:22:36 GMT
Server:
- AmazonS3
Transfer-Encoding:
@@ -149,9 +190,9 @@ interactions:
x-amz-bucket-region:
- us-east-1
x-amz-id-2:
- - sfOGqkBfxVw7mfxv9HJ7bu241qditGmUbHrHkg7ZBNx+2L+8sPyYmd17d3xLQ+DN2hkUdLQ7WHxt38dd8ytfKg==
+ - pdHzOjM4OtCc+je7BkRjfRYOpj6WKN2QXS7gI//NhegVsHrH7Lf4IMxIr1OeSuLN+ORoDwLbbuE=
x-amz-request-id:
- - EJ01RYXSA2KSGN1R
+ - HY3T3WM5FH6FVVHW
status:
code: 200
message: OK
diff --git a/tests/remote/fixtures/test_goes_single.yaml b/tests/remote/fixtures/test_goes_single.yaml
index 512eab5bedb..e48f15ac537 100644
--- a/tests/remote/fixtures/test_goes_single.yaml
+++ b/tests/remote/fixtures/test_goes_single.yaml
@@ -4,17 +4,58 @@ interactions:
headers:
User-Agent:
- !!binary |
- Qm90bzMvMS4zNS44OCBtZC9Cb3RvY29yZSMxLjM1Ljg4IHVhLzIuMCBvcy9tYWNvcyMyNC4yLjAg
- bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjcgbWQvcHlpbXBsI0NQeXRob24gY2ZnL3Jl
- dHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuMzUuODggUmVzb3VyY2U=
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEQs
+ YiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS4zOC40MiBSZXNvdXJjZQ==
amz-sdk-invocation-id:
- !!binary |
- MzRiMTdmZjgtNGNkZi00ODIyLTg4NTEtZDBlMTFiMzM2ZWE0
+ MjBjMTFhMDYtY2Q1OS00YzIyLTg5OWItZTQyMjk5NmQ2ZWY3
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: GET
- uri: https://noaa-goes18.s3.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadM%2F2025%2F009%2F23%2FOR_ABI-L1b-RadM1-&delimiter=_&encoding-type=url
+ uri: https://noaa-goes18.s3.us-east-2.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadM%2F2025%2F009%2F23%2FOR_ABI-L1b-RadM1-&delimiter=_&encoding-type=url
+ response:
+ body:
+ string: '
+
+ PermanentRedirectThe bucket you are attempting
+ to access must be addressed using the specified endpoint. Please send all
+ future requests to this endpoint.s3.amazonaws.comnoaa-goes183TZQCM2ERHHGKJT6P1sTLEphZRLlu/JNFBnfeM1U3DWdYTGi8D8K5QIQP+QUe3Cg1iAGz+/9eL/+XjB0kpo5o6xhgGfqzqViCp0c9Q=='
+ headers:
+ Content-Type:
+ - application/xml
+ Date:
+ - Sun, 06 Jul 2025 19:22:15 GMT
+ Server:
+ - AmazonS3
+ Transfer-Encoding:
+ - chunked
+ x-amz-bucket-region:
+ - us-east-1
+ x-amz-id-2:
+ - P1sTLEphZRLlu/JNFBnfeM1U3DWdYTGi8D8K5QIQP+QUe3Cg1iAGz+/9eL/+XjB0kpo5o6xhgGfqzqViCp0c9Q==
+ x-amz-request-id:
+ - 3TZQCM2ERHHGKJT6
+ status:
+ code: 301
+ message: Moved Permanently
+- request:
+ body: null
+ headers:
+ User-Agent:
+ - !!binary |
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEQs
+ YiBjZmcvcmV0cnktbW9kZSNsZWdhY3kgQm90b2NvcmUvMS4zOC40MiBSZXNvdXJjZQ==
+ amz-sdk-invocation-id:
+ - !!binary |
+ MjBjMTFhMDYtY2Q1OS00YzIyLTg5OWItZTQyMjk5NmQ2ZWY3
+ amz-sdk-request:
+ - !!binary |
+ YXR0ZW1wdD0yOyBtYXg9NQ==
+ method: GET
+ uri: https://noaa-goes18.s3.us-east-1.amazonaws.com/?list-type=2&prefix=ABI-L1b-RadM%2F2025%2F009%2F23%2FOR_ABI-L1b-RadM1-&delimiter=_&encoding-type=url
response:
body:
string: '
@@ -24,7 +65,7 @@ interactions:
Content-Type:
- application/xml
Date:
- - Fri, 10 Jan 2025 02:02:27 GMT
+ - Sun, 06 Jul 2025 19:22:17 GMT
Server:
- AmazonS3
Transfer-Encoding:
@@ -32,9 +73,9 @@ interactions:
x-amz-bucket-region:
- us-east-1
x-amz-id-2:
- - hH1J9lfZ95RZ8I3MkhF43t4ohUlkCaJdDdk5mLG0r4hYd5hZbuokkeJfKGAvt/Ai8LE7qBVVxTY=
+ - OQvPIu++dcz+2pX7Rv7UIFTG70lKeYYGPvbXEaJcunn7SxfkiA2karQXVDF2xMg+ZnWukBH6Fz4=
x-amz-request-id:
- - 0PDVEMNP5XVRXZP0
+ - 9JM0N0RXMXZN2KY3
status:
code: 200
message: OK
@@ -43,17 +84,17 @@ interactions:
headers:
User-Agent:
- !!binary |
- Qm90bzMvMS4zNS44OCBtZC9Cb3RvY29yZSMxLjM1Ljg4IHVhLzIuMCBvcy9tYWNvcyMyNC4yLjAg
- bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjcgbWQvcHlpbXBsI0NQeXRob24gY2ZnL3Jl
- dHJ5LW1vZGUjbGVnYWN5IEJvdG9jb3JlLzEuMzUuODggUmVzb3VyY2U=
+ Qm90bzMvMS4zOC40MiBtZC9Cb3RvY29yZSMxLjM4LjQyIHVhLzIuMSBvcy9tYWNvcyMyNC4zLjAg
+ bWQvYXJjaCNhcm02NCBsYW5nL3B5dGhvbiMzLjEyLjggbWQvcHlpbXBsI0NQeXRob24gbS9aLEMs
+ RCxiIGNmZy9yZXRyeS1tb2RlI2xlZ2FjeSBCb3RvY29yZS8xLjM4LjQyIFJlc291cmNl
amz-sdk-invocation-id:
- !!binary |
- Y2NlM2E0YWEtZGZmZC00ZGIxLWJmYzItMTU0ZjIwYTE2ODk5
+ Mzg2NzE2YzctY2M5MS00YzhkLTljNjYtNjQ2YmZmYmJiZGU0
amz-sdk-request:
- !!binary |
YXR0ZW1wdD0x
method: GET
- uri: https://noaa-goes18.s3.amazonaws.com/?prefix=ABI-L1b-RadM%2F2025%2F009%2F23%2FOR_ABI-L1b-RadM1-M6C02&encoding-type=url
+ uri: https://noaa-goes18.s3.us-east-1.amazonaws.com/?prefix=ABI-L1b-RadM%2F2025%2F009%2F23%2FOR_ABI-L1b-RadM1-M6C02&encoding-type=url
response:
body:
string: '
@@ -63,7 +104,7 @@ interactions:
Content-Type:
- application/xml
Date:
- - Fri, 10 Jan 2025 02:02:28 GMT
+ - Sun, 06 Jul 2025 19:22:17 GMT
Server:
- AmazonS3
Transfer-Encoding:
@@ -71,9 +112,9 @@ interactions:
x-amz-bucket-region:
- us-east-1
x-amz-id-2:
- - 8qow0h7U+tKbbnkSN0DZPqbhimqk41cSHWPfEY2RobHQmr1XDPkiFcdiRTD6SKNT8X3pLQhnGzo=
+ - gq1oqpUsOIpqKCWzuvM+IPWHDn/GPomFHwI8drMUalfDtkHIaGbdbgsLd8rNL9JBYHIeaB02TXY=
x-amz-request-id:
- - BS923NKZP8GB1KZP
+ - 9JM2SQ28WZ36NM2E
status:
code: 200
message: OK
diff --git a/tests/remote/test_goes_client.py b/tests/remote/test_goes_client.py
new file mode 100644
index 00000000000..4feb7492343
--- /dev/null
+++ b/tests/remote/test_goes_client.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# Copyright (c) 2015-2025 MetPy Developers.
+# Distributed under the terms of the BSD 3-Clause License.
+# SPDX-License-Identifier: BSD-3-Clause
+"""Test the `metpy.remote.GOESArchive` module."""
+from datetime import datetime, timezone
+
+from metpy.remote import GOESArchive
+from metpy.testing import needs_aws
+
+
+@needs_aws
+def test_goes_hour_boundary():
+ """Test the GOES client's ability to find products across hour boundaries."""
+ # Create a GOES client
+ goes = GOESArchive(16)
+ # Test case 1: Exact hour boundary
+ dt = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ prod = goes.get_product('ABI-L1b-RadC', dt, band=1)
+ assert prod.url is not None
+ # Test case 2: Just after hour boundary
+ dt = datetime(2025, 1, 1, 0, 0, 30, tzinfo=timezone.utc)
+ prod = goes.get_product('ABI-L1b-RadC', dt, band=1)
+ assert prod.url is not None
+ # Test case 3: Just before hour boundary
+ dt = datetime(2025, 1, 1, 0, 59, 30, tzinfo=timezone.utc)
+ prod = goes.get_product('ABI-L1b-RadC', dt, band=1)
+ assert prod.url is not None
+ # Test case 4: Day boundary
+ dt = datetime(2025, 1, 1, 0, 0, 0, tzinfo=timezone.utc)
+ prod = goes.get_product('ABI-L1b-RadC', dt, band=1)
+ assert prod.url is not None