nasapower-s3 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nasapower_s3-1.0.0/LICENSE +21 -0
- nasapower_s3-1.0.0/PKG-INFO +135 -0
- nasapower_s3-1.0.0/README.md +82 -0
- nasapower_s3-1.0.0/pyproject.toml +60 -0
- nasapower_s3-1.0.0/setup.cfg +4 -0
- nasapower_s3-1.0.0/src/nasapower_s3/__init__.py +19 -0
- nasapower_s3-1.0.0/src/nasapower_s3/client.py +149 -0
- nasapower_s3-1.0.0/src/nasapower_s3/constants.py +43 -0
- nasapower_s3-1.0.0/src/nasapower_s3/exceptions.py +23 -0
- nasapower_s3-1.0.0/src/nasapower_s3/validators.py +44 -0
- nasapower_s3-1.0.0/src/nasapower_s3.egg-info/PKG-INFO +135 -0
- nasapower_s3-1.0.0/src/nasapower_s3.egg-info/SOURCES.txt +14 -0
- nasapower_s3-1.0.0/src/nasapower_s3.egg-info/dependency_links.txt +1 -0
- nasapower_s3-1.0.0/src/nasapower_s3.egg-info/requires.txt +13 -0
- nasapower_s3-1.0.0/src/nasapower_s3.egg-info/top_level.txt +1 -0
- nasapower_s3-1.0.0/tests/test_nasapower.py +152 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Varenya Sri Mudumba
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nasapower-s3
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A universal, secure, and fast interface for NASA POWER S3 data.
|
|
5
|
+
Author-email: Varenya Sri Mudumba <srivarenya.mudumba@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Varenya Sri Mudumba
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/srivarenya01/nasapower-s3
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/srivarenya01/nasapower-s3/issues
|
|
30
|
+
Keywords: nasa,power,s3,zarr,meteorology,solar,climate
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: pandas
|
|
41
|
+
Requires-Dist: xarray
|
|
42
|
+
Requires-Dist: zarr
|
|
43
|
+
Requires-Dist: s3fs
|
|
44
|
+
Requires-Dist: fsspec
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pytest; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
48
|
+
Requires-Dist: black; extra == "dev"
|
|
49
|
+
Requires-Dist: isort; extra == "dev"
|
|
50
|
+
Requires-Dist: codespell; extra == "dev"
|
|
51
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# nasapower-s3
|
|
55
|
+
|
|
56
|
+
[](https://opensource.org/licenses/MIT)
|
|
57
|
+
[](https://github.com/psf/black)
|
|
58
|
+
|
|
59
|
+
A universal, secure, and fast interface for accessing NASA POWER data directly from AWS S3 Zarr stores.
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **High Speed**: Access data directly from S3 using Zarr (cloud-optimized file format).
|
|
64
|
+
- **Universal**: Supports Daily, Hourly, and Monthly temporal scales.
|
|
65
|
+
- **Easy**: Returns standard pandas DataFrames.
|
|
66
|
+
- **Secure**: No AWS credentials required (anonymous access).
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install nasapower-s3
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from nasapower_s3 import NasaPowerS3
|
|
78
|
+
|
|
79
|
+
# Initialize client
|
|
80
|
+
client = NasaPowerS3()
|
|
81
|
+
|
|
82
|
+
# Fetch data (Meteorology)
|
|
83
|
+
df = client.get_data(
|
|
84
|
+
lat=30.6,
|
|
85
|
+
lon=-96.3,
|
|
86
|
+
start_date="2023-01-01",
|
|
87
|
+
end_date="2023-01-10",
|
|
88
|
+
variables=["T2M", "PRECTOTCORR"],
|
|
89
|
+
frequency="daily",
|
|
90
|
+
collection="meteorology" # Default
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Fetch Solar data
|
|
94
|
+
df_solar = client.get_data(
|
|
95
|
+
lat=30.6,
|
|
96
|
+
lon=-96.3,
|
|
97
|
+
start_date="2023-01-01",
|
|
98
|
+
end_date="2023-01-10",
|
|
99
|
+
variables=["ALLSKY_SFC_SW_DWN"],
|
|
100
|
+
frequency="daily",
|
|
101
|
+
collection="solar"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print(df.head())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Available Data
|
|
108
|
+
|
|
109
|
+
The library supports:
|
|
110
|
+
- **Collections**: `meteorology` (MERRA-2) and `solar` (SRB)
|
|
111
|
+
- **Frequencies**: `daily`, `hourly`, `monthly`, `climatology`
|
|
112
|
+
|
|
113
|
+
For a full list of variables, refer to the [NASA POWER API Docs](https://power.larc.nasa.gov/docs/services/api/).
|
|
114
|
+
|
|
115
|
+
## Data License & Attribution
|
|
116
|
+
|
|
117
|
+
The code in this repository is licensed under **MIT**.
|
|
118
|
+
The data accessed from NASA POWER is licensed under **Creative Commons Attribution 4.0 International (CC BY 4.0)**.
|
|
119
|
+
|
|
120
|
+
**Attribution**:
|
|
121
|
+
> "These data were obtained from the NASA Langley Research Center POWER Project funded through the NASA Earth Science Directorate Applied Science Program."
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
126
|
+
|
|
127
|
+
1. Fork the repo
|
|
128
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
129
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
130
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
131
|
+
5. Open a Pull Request
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# nasapower-s3
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://github.com/psf/black)
|
|
5
|
+
|
|
6
|
+
A universal, secure, and fast interface for accessing NASA POWER data directly from AWS S3 Zarr stores.
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
- **High Speed**: Access data directly from S3 using Zarr (cloud-optimized file format).
|
|
11
|
+
- **Universal**: Supports Daily, Hourly, and Monthly temporal scales.
|
|
12
|
+
- **Easy**: Returns standard pandas DataFrames.
|
|
13
|
+
- **Secure**: No AWS credentials required (anonymous access).
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install nasapower-s3
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
```python
|
|
24
|
+
from nasapower_s3 import NasaPowerS3
|
|
25
|
+
|
|
26
|
+
# Initialize client
|
|
27
|
+
client = NasaPowerS3()
|
|
28
|
+
|
|
29
|
+
# Fetch data (Meteorology)
|
|
30
|
+
df = client.get_data(
|
|
31
|
+
lat=30.6,
|
|
32
|
+
lon=-96.3,
|
|
33
|
+
start_date="2023-01-01",
|
|
34
|
+
end_date="2023-01-10",
|
|
35
|
+
variables=["T2M", "PRECTOTCORR"],
|
|
36
|
+
frequency="daily",
|
|
37
|
+
collection="meteorology" # Default
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Fetch Solar data
|
|
41
|
+
df_solar = client.get_data(
|
|
42
|
+
lat=30.6,
|
|
43
|
+
lon=-96.3,
|
|
44
|
+
start_date="2023-01-01",
|
|
45
|
+
end_date="2023-01-10",
|
|
46
|
+
variables=["ALLSKY_SFC_SW_DWN"],
|
|
47
|
+
frequency="daily",
|
|
48
|
+
collection="solar"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
print(df.head())
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Available Data
|
|
55
|
+
|
|
56
|
+
The library supports:
|
|
57
|
+
- **Collections**: `meteorology` (MERRA-2) and `solar` (SRB)
|
|
58
|
+
- **Frequencies**: `daily`, `hourly`, `monthly`, `climatology`
|
|
59
|
+
|
|
60
|
+
For a full list of variables, refer to the [NASA POWER API Docs](https://power.larc.nasa.gov/docs/services/api/).
|
|
61
|
+
|
|
62
|
+
## Data License & Attribution
|
|
63
|
+
|
|
64
|
+
The code in this repository is licensed under **MIT**.
|
|
65
|
+
The data accessed from NASA POWER is licensed under **Creative Commons Attribution 4.0 International (CC BY 4.0)**.
|
|
66
|
+
|
|
67
|
+
**Attribution**:
|
|
68
|
+
> "These data were obtained from the NASA Langley Research Center POWER Project funded through the NASA Earth Science Directorate Applied Science Program."
|
|
69
|
+
|
|
70
|
+
## Contributing
|
|
71
|
+
|
|
72
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
73
|
+
|
|
74
|
+
1. Fork the repo
|
|
75
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
76
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
77
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
78
|
+
5. Open a Pull Request
|
|
79
|
+
|
|
80
|
+
## License
|
|
81
|
+
|
|
82
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nasapower-s3"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A universal, secure, and fast interface for NASA POWER S3 data."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [
|
|
11
|
+
{ name = "Varenya Sri Mudumba", email = "srivarenya.mudumba@gmail.com"},
|
|
12
|
+
]
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Development Status :: 3 - Alpha",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Topic :: Scientific/Engineering :: Atmospheric Science",
|
|
21
|
+
]
|
|
22
|
+
keywords = ["nasa", "power", "s3", "zarr", "meteorology", "solar", "climate"]
|
|
23
|
+
requires-python = ">=3.8"
|
|
24
|
+
dependencies = [
|
|
25
|
+
"pandas",
|
|
26
|
+
"xarray",
|
|
27
|
+
"zarr",
|
|
28
|
+
"s3fs",
|
|
29
|
+
"fsspec",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
"Homepage" = "https://github.com/srivarenya01/nasapower-s3"
|
|
34
|
+
"Bug Tracker" = "https://github.com/srivarenya01/nasapower-s3/issues"
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
dev = [
|
|
38
|
+
"pytest",
|
|
39
|
+
"pytest-cov",
|
|
40
|
+
"black",
|
|
41
|
+
"isort",
|
|
42
|
+
"codespell",
|
|
43
|
+
"pre-commit",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.setuptools.packages.find]
|
|
47
|
+
where = ["src"]
|
|
48
|
+
|
|
49
|
+
[tool.isort]
|
|
50
|
+
profile = "black"
|
|
51
|
+
|
|
52
|
+
[tool.codespell]
|
|
53
|
+
skip = "*.lock,*.zarr,.git,__pycache__"
|
|
54
|
+
ignore-words-list = "fsspec"
|
|
55
|
+
|
|
56
|
+
[tool.pytest.ini_options]
|
|
57
|
+
markers = [
|
|
58
|
+
"integration: marks tests that require network access to S3 (deselect with '-m not integration')",
|
|
59
|
+
]
|
|
60
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .client import NasaPowerS3
|
|
2
|
+
from .exceptions import (
|
|
3
|
+
DataAccessError,
|
|
4
|
+
InvalidCoordinateError,
|
|
5
|
+
InvalidFrequencyError,
|
|
6
|
+
NasaPowerError,
|
|
7
|
+
VariableNotFoundError,
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"NasaPowerS3",
|
|
12
|
+
"NasaPowerError",
|
|
13
|
+
"InvalidCoordinateError",
|
|
14
|
+
"InvalidFrequencyError",
|
|
15
|
+
"VariableNotFoundError",
|
|
16
|
+
"DataAccessError",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Dict, List, Optional, Union
|
|
3
|
+
|
|
4
|
+
import fsspec
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import xarray as xr
|
|
7
|
+
|
|
8
|
+
from .constants import BUCKETS as DEFAULT_BUCKETS
|
|
9
|
+
from .exceptions import DataAccessError, VariableNotFoundError
|
|
10
|
+
from .validators import validate_coordinates, validate_frequency
|
|
11
|
+
|
|
12
|
+
# Configure logging
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class NasaPowerS3:
|
|
17
|
+
"""
|
|
18
|
+
A secure, high-speed interface for NASA POWER data via AWS S3.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
"""Initialize the NASA POWER S3 client."""
|
|
23
|
+
# Create a local copy of buckets to avoid global state mutation
|
|
24
|
+
self._buckets = DEFAULT_BUCKETS.copy()
|
|
25
|
+
|
|
26
|
+
def update_buckets(
|
|
27
|
+
self, collection: str, frequency: Optional[str] = None, url: Optional[str] = None
|
|
28
|
+
) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Update or add a BUCKET mapping for this client instance.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
collection: Collection name (e.g., "meteorology", "solar")
|
|
34
|
+
frequency: Optional specific frequency name (e.g., "daily").
|
|
35
|
+
If None, the url is treated as a template.
|
|
36
|
+
url: Valid S3 Zarr store URL or template with {freq}
|
|
37
|
+
"""
|
|
38
|
+
if frequency is None:
|
|
39
|
+
# Treat url as a template for all frequencies in this collection
|
|
40
|
+
self._buckets[collection] = url
|
|
41
|
+
else:
|
|
42
|
+
# Add/Update a specific frequency override
|
|
43
|
+
if collection not in self._buckets or isinstance(self._buckets[collection], str):
|
|
44
|
+
self._buckets[collection] = {}
|
|
45
|
+
self._buckets[collection][frequency] = url
|
|
46
|
+
|
|
47
|
+
logger.info(f"Updated bucket mapping for collection '{collection}'")
|
|
48
|
+
|
|
49
|
+
def get_data(
|
|
50
|
+
self,
|
|
51
|
+
lat: float,
|
|
52
|
+
lon: float,
|
|
53
|
+
start_date: str,
|
|
54
|
+
end_date: str,
|
|
55
|
+
variables: List[str],
|
|
56
|
+
frequency: str = "daily",
|
|
57
|
+
collection: str = "meteorology",
|
|
58
|
+
) -> pd.DataFrame:
|
|
59
|
+
"""
|
|
60
|
+
Fetch data from NASA POWER S3 Zarr store.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
lat: Latitude (-90 to 90)
|
|
64
|
+
lon: Longitude (-180 to 180)
|
|
65
|
+
start_date: Start date (YYYY-MM-DD or YYYY-MM-DDTHH:MM)
|
|
66
|
+
end_date: End date (YYYY-MM-DD or YYYY-MM-DDTHH:MM)
|
|
67
|
+
variables: List of variable names to fetch (e.g., ["T2M", "PRECTOTCORR"])
|
|
68
|
+
frequency: Data frequency ("daily", "hourly", "monthly")
|
|
69
|
+
collection: Data collection ("meteorology" or "solar"). Defaults to "meteorology".
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
pd.DataFrame: DataFrame containing the requested data.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
InvalidCoordinateError: If coordinates are invalid.
|
|
76
|
+
InvalidFrequencyError: If frequency is not supported.
|
|
77
|
+
VariableNotFoundError: If a variable is not found.
|
|
78
|
+
DataAccessError: If there is an error accessing S3.
|
|
79
|
+
"""
|
|
80
|
+
# Validate inputs
|
|
81
|
+
validate_coordinates(lat, lon)
|
|
82
|
+
validate_frequency(frequency)
|
|
83
|
+
|
|
84
|
+
ds = None
|
|
85
|
+
try:
|
|
86
|
+
# Get the URL/template for this collection
|
|
87
|
+
bucket_info = self._buckets.get(collection)
|
|
88
|
+
if not bucket_info:
|
|
89
|
+
raise ValueError(
|
|
90
|
+
f"Invalid collection: '{collection}'. Supported: {list(self._buckets.keys())}"
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Resolve the URL
|
|
94
|
+
if isinstance(bucket_info, str):
|
|
95
|
+
# It's a template
|
|
96
|
+
url = bucket_info.format(freq=frequency)
|
|
97
|
+
elif isinstance(bucket_info, dict):
|
|
98
|
+
# It's a dictionary of frequency-specific URLs
|
|
99
|
+
url = bucket_info.get(frequency)
|
|
100
|
+
if not url:
|
|
101
|
+
raise DataAccessError(
|
|
102
|
+
f"Frequency '{frequency}' is not mapped for collection '{collection}'."
|
|
103
|
+
)
|
|
104
|
+
else:
|
|
105
|
+
raise DataAccessError(f"Malformed bucket configuration for '{collection}'")
|
|
106
|
+
|
|
107
|
+
logger.info(f"Connecting to NASA POWER S3: {url} (anonymous access)")
|
|
108
|
+
store = fsspec.get_mapper(url, anon=True)
|
|
109
|
+
ds = xr.open_zarr(store, consolidated=True)
|
|
110
|
+
|
|
111
|
+
# Check if variables exist in dataset
|
|
112
|
+
missing_vars = [v for v in variables if v not in ds.data_vars]
|
|
113
|
+
if missing_vars:
|
|
114
|
+
raise VariableNotFoundError(
|
|
115
|
+
f"Variables not found in {frequency} dataset: {missing_vars}"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
logger.debug(f"Subsetting data for {lat}, {lon} from {start_date} to {end_date}")
|
|
119
|
+
# Subset data - select nearest lat/lon first, then slice time
|
|
120
|
+
subset = ds[variables].sel(
|
|
121
|
+
lat=lat,
|
|
122
|
+
lon=lon,
|
|
123
|
+
method="nearest",
|
|
124
|
+
).sel(
|
|
125
|
+
time=slice(start_date, end_date)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Convert to Pandas
|
|
129
|
+
df = subset.to_dataframe().reset_index()
|
|
130
|
+
return df
|
|
131
|
+
|
|
132
|
+
except (ValueError, VariableNotFoundError) as e:
|
|
133
|
+
# Re-raise validation or specific business logic errors
|
|
134
|
+
raise
|
|
135
|
+
except Exception as e:
|
|
136
|
+
# Distinguish between connection errors and processing errors
|
|
137
|
+
err_str = str(e).lower()
|
|
138
|
+
if "keyerror" in err_str or "404" in err_str or "nosuchkey" in err_str:
|
|
139
|
+
msg = (f"Frequency '{frequency}' may not be available for collection '{collection}'. "
|
|
140
|
+
f"Attempted to access: {url if 'url' in locals() else 'Unknown'}")
|
|
141
|
+
logger.error(msg)
|
|
142
|
+
raise DataAccessError(msg) from e
|
|
143
|
+
|
|
144
|
+
msg = f"Error accessing/processing NASA POWER S3 data: {e}"
|
|
145
|
+
logger.error(msg)
|
|
146
|
+
raise DataAccessError(msg) from e
|
|
147
|
+
finally:
|
|
148
|
+
if ds is not None:
|
|
149
|
+
ds.close()
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Constants and mappings for NASA POWER S3 Zarr stores.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
# Zarr store URLs
|
|
6
|
+
# Zarr store URLs
|
|
7
|
+
# Structure matches S3: s3://nasa-power/{collection}/temporal/power_{collection}_{freq}_temporal_utc.zarr
|
|
8
|
+
BUCKETS = {
|
|
9
|
+
"meteorology": "s3://nasa-power/merra2/temporal/power_merra2_{freq}_temporal_utc.zarr",
|
|
10
|
+
"solar": "s3://nasa-power/srb/temporal/power_srb_{freq}_temporal_utc.zarr",
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
# Coordinate boundaries
|
|
14
|
+
MIN_LAT = -90.0
|
|
15
|
+
MAX_LAT = 90.0
|
|
16
|
+
MIN_LON = -180.0
|
|
17
|
+
MAX_LON = 180.0
|
|
18
|
+
|
|
19
|
+
# Supported frequencies
|
|
20
|
+
FREQUENCIES = ["daily", "hourly", "monthly", "climatology", "annual"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# Common variables default collection map
|
|
24
|
+
# If a variable is in this list, we can guess the collection.
|
|
25
|
+
# Solar variables often start with ALLSKY, CLR, TOA, SZA
|
|
26
|
+
# Meteorology variables are often T2M, PRECTOT, RH, WS
|
|
27
|
+
VARIABLE_COLLECTION_MAP = {
|
|
28
|
+
# Default Meteorology (MERRA-2)
|
|
29
|
+
"T2M": "meteorology",
|
|
30
|
+
"T2M_MAX": "meteorology",
|
|
31
|
+
"T2M_MIN": "meteorology",
|
|
32
|
+
"PRECTOTCORR": "meteorology",
|
|
33
|
+
"RH2M": "meteorology",
|
|
34
|
+
"WS10M": "meteorology",
|
|
35
|
+
"PS": "meteorology",
|
|
36
|
+
|
|
37
|
+
# Default Solar (SRB)
|
|
38
|
+
"ALLSKY_SFC_SW_DWN": "solar",
|
|
39
|
+
"CLR_SFC_SW_DWN": "solar",
|
|
40
|
+
"ALLSKY_KT": "solar",
|
|
41
|
+
"ALLSKY_SFC_LW_DWN": "solar",
|
|
42
|
+
"TOA_SW_DWN": "solar",
|
|
43
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class NasaPowerError(Exception):
|
|
2
|
+
"""Base exception for NASA POWER library."""
|
|
3
|
+
pass
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class InvalidCoordinateError(NasaPowerError):
|
|
7
|
+
"""Raised when coordinates are out of valid range."""
|
|
8
|
+
pass
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class InvalidFrequencyError(NasaPowerError):
|
|
12
|
+
"""Raised when an invalid frequency is requested."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class VariableNotFoundError(NasaPowerError):
|
|
17
|
+
"""Raised when a requested variable is not found in the dataset."""
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class DataAccessError(NasaPowerError):
|
|
22
|
+
"""Raised when there is an issue accessing the data source."""
|
|
23
|
+
pass
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Input validation utilities.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Union
|
|
5
|
+
|
|
6
|
+
from .constants import FREQUENCIES, MAX_LAT, MAX_LON, MIN_LAT, MIN_LON
|
|
7
|
+
from .exceptions import InvalidCoordinateError, InvalidFrequencyError
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_coordinates(lat: float, lon: float) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Validate latitude and longitude.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
lat: Latitude (-90 to 90)
|
|
16
|
+
lon: Longitude (-180 to 180)
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
InvalidCoordinateError: If coordinates are out of bounds.
|
|
20
|
+
"""
|
|
21
|
+
if not (MIN_LAT <= lat <= MAX_LAT):
|
|
22
|
+
raise InvalidCoordinateError(
|
|
23
|
+
f"Invalid Latitude: {lat}. Must be between {MIN_LAT} and {MAX_LAT}."
|
|
24
|
+
)
|
|
25
|
+
if not (MIN_LON <= lon <= MAX_LON):
|
|
26
|
+
raise InvalidCoordinateError(
|
|
27
|
+
f"Invalid Longitude: {lon}. Must be between {MIN_LON} and {MAX_LON}."
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_frequency(frequency: str) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Validate data frequency.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
frequency: Data frequency string (e.g., 'daily', 'hourly')
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
InvalidFrequencyError: If frequency is not supported.
|
|
40
|
+
"""
|
|
41
|
+
if frequency not in FREQUENCIES:
|
|
42
|
+
raise InvalidFrequencyError(
|
|
43
|
+
f"Invalid frequency: '{frequency}'. Supported values: {FREQUENCIES}"
|
|
44
|
+
)
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nasapower-s3
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A universal, secure, and fast interface for NASA POWER S3 data.
|
|
5
|
+
Author-email: Varenya Sri Mudumba <srivarenya.mudumba@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Varenya Sri Mudumba
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/srivarenya01/nasapower-s3
|
|
29
|
+
Project-URL: Bug Tracker, https://github.com/srivarenya01/nasapower-s3/issues
|
|
30
|
+
Keywords: nasa,power,s3,zarr,meteorology,solar,climate
|
|
31
|
+
Classifier: Programming Language :: Python :: 3
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Development Status :: 3 - Alpha
|
|
35
|
+
Classifier: Intended Audience :: Science/Research
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
|
|
37
|
+
Requires-Python: >=3.8
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: pandas
|
|
41
|
+
Requires-Dist: xarray
|
|
42
|
+
Requires-Dist: zarr
|
|
43
|
+
Requires-Dist: s3fs
|
|
44
|
+
Requires-Dist: fsspec
|
|
45
|
+
Provides-Extra: dev
|
|
46
|
+
Requires-Dist: pytest; extra == "dev"
|
|
47
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
48
|
+
Requires-Dist: black; extra == "dev"
|
|
49
|
+
Requires-Dist: isort; extra == "dev"
|
|
50
|
+
Requires-Dist: codespell; extra == "dev"
|
|
51
|
+
Requires-Dist: pre-commit; extra == "dev"
|
|
52
|
+
Dynamic: license-file
|
|
53
|
+
|
|
54
|
+
# nasapower-s3
|
|
55
|
+
|
|
56
|
+
[](https://opensource.org/licenses/MIT)
|
|
57
|
+
[](https://github.com/psf/black)
|
|
58
|
+
|
|
59
|
+
A universal, secure, and fast interface for accessing NASA POWER data directly from AWS S3 Zarr stores.
|
|
60
|
+
|
|
61
|
+
## Features
|
|
62
|
+
|
|
63
|
+
- **High Speed**: Access data directly from S3 using Zarr (cloud-optimized file format).
|
|
64
|
+
- **Universal**: Supports Daily, Hourly, and Monthly temporal scales.
|
|
65
|
+
- **Easy**: Returns standard pandas DataFrames.
|
|
66
|
+
- **Secure**: No AWS credentials required (anonymous access).
|
|
67
|
+
|
|
68
|
+
## Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pip install nasapower-s3
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Usage
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from nasapower_s3 import NasaPowerS3
|
|
78
|
+
|
|
79
|
+
# Initialize client
|
|
80
|
+
client = NasaPowerS3()
|
|
81
|
+
|
|
82
|
+
# Fetch data (Meteorology)
|
|
83
|
+
df = client.get_data(
|
|
84
|
+
lat=30.6,
|
|
85
|
+
lon=-96.3,
|
|
86
|
+
start_date="2023-01-01",
|
|
87
|
+
end_date="2023-01-10",
|
|
88
|
+
variables=["T2M", "PRECTOTCORR"],
|
|
89
|
+
frequency="daily",
|
|
90
|
+
collection="meteorology" # Default
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
# Fetch Solar data
|
|
94
|
+
df_solar = client.get_data(
|
|
95
|
+
lat=30.6,
|
|
96
|
+
lon=-96.3,
|
|
97
|
+
start_date="2023-01-01",
|
|
98
|
+
end_date="2023-01-10",
|
|
99
|
+
variables=["ALLSKY_SFC_SW_DWN"],
|
|
100
|
+
frequency="daily",
|
|
101
|
+
collection="solar"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
print(df.head())
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Available Data
|
|
108
|
+
|
|
109
|
+
The library supports:
|
|
110
|
+
- **Collections**: `meteorology` (MERRA-2) and `solar` (SRB)
|
|
111
|
+
- **Frequencies**: `daily`, `hourly`, `monthly`, `climatology`
|
|
112
|
+
|
|
113
|
+
For a full list of variables, refer to the [NASA POWER API Docs](https://power.larc.nasa.gov/docs/services/api/).
|
|
114
|
+
|
|
115
|
+
## Data License & Attribution
|
|
116
|
+
|
|
117
|
+
The code in this repository is licensed under **MIT**.
|
|
118
|
+
The data accessed from NASA POWER is licensed under **Creative Commons Attribution 4.0 International (CC BY 4.0)**.
|
|
119
|
+
|
|
120
|
+
**Attribution**:
|
|
121
|
+
> "These data were obtained from the NASA Langley Research Center POWER Project funded through the NASA Earth Science Directorate Applied Science Program."
|
|
122
|
+
|
|
123
|
+
## Contributing
|
|
124
|
+
|
|
125
|
+
We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
126
|
+
|
|
127
|
+
1. Fork the repo
|
|
128
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
129
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
130
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
131
|
+
5. Open a Pull Request
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/nasapower_s3/__init__.py
|
|
5
|
+
src/nasapower_s3/client.py
|
|
6
|
+
src/nasapower_s3/constants.py
|
|
7
|
+
src/nasapower_s3/exceptions.py
|
|
8
|
+
src/nasapower_s3/validators.py
|
|
9
|
+
src/nasapower_s3.egg-info/PKG-INFO
|
|
10
|
+
src/nasapower_s3.egg-info/SOURCES.txt
|
|
11
|
+
src/nasapower_s3.egg-info/dependency_links.txt
|
|
12
|
+
src/nasapower_s3.egg-info/requires.txt
|
|
13
|
+
src/nasapower_s3.egg-info/top_level.txt
|
|
14
|
+
tests/test_nasapower.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nasapower_s3
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Tests for the nasapower-s3 library.
|
|
3
|
+
"""
|
|
4
|
+
import pytest
|
|
5
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from nasapower_s3 import NasaPowerS3
|
|
10
|
+
from nasapower_s3.validators import validate_coordinates, validate_frequency
|
|
11
|
+
from nasapower_s3.exceptions import (
|
|
12
|
+
InvalidCoordinateError,
|
|
13
|
+
InvalidFrequencyError,
|
|
14
|
+
VariableNotFoundError,
|
|
15
|
+
DataAccessError,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TestValidators:
|
|
20
|
+
"""Test input validation functions."""
|
|
21
|
+
|
|
22
|
+
def test_valid_coordinates(self):
|
|
23
|
+
"""Valid coordinates should not raise."""
|
|
24
|
+
validate_coordinates(0, 0)
|
|
25
|
+
validate_coordinates(90, 180)
|
|
26
|
+
validate_coordinates(-90, -180)
|
|
27
|
+
validate_coordinates(30.6, -96.3)
|
|
28
|
+
|
|
29
|
+
def test_invalid_latitude(self):
|
|
30
|
+
"""Latitude out of range should raise InvalidCoordinateError."""
|
|
31
|
+
with pytest.raises(InvalidCoordinateError):
|
|
32
|
+
validate_coordinates(91, 0)
|
|
33
|
+
with pytest.raises(InvalidCoordinateError):
|
|
34
|
+
validate_coordinates(-91, 0)
|
|
35
|
+
|
|
36
|
+
def test_invalid_longitude(self):
|
|
37
|
+
"""Longitude out of range should raise InvalidCoordinateError."""
|
|
38
|
+
with pytest.raises(InvalidCoordinateError):
|
|
39
|
+
validate_coordinates(0, 181)
|
|
40
|
+
with pytest.raises(InvalidCoordinateError):
|
|
41
|
+
validate_coordinates(0, -181)
|
|
42
|
+
|
|
43
|
+
def test_valid_frequency(self):
|
|
44
|
+
"""Valid frequencies should not raise."""
|
|
45
|
+
validate_frequency("daily")
|
|
46
|
+
validate_frequency("hourly")
|
|
47
|
+
validate_frequency("monthly")
|
|
48
|
+
|
|
49
|
+
def test_invalid_frequency(self):
|
|
50
|
+
"""Invalid frequency should raise InvalidFrequencyError."""
|
|
51
|
+
with pytest.raises(InvalidFrequencyError):
|
|
52
|
+
validate_frequency("weekly")
|
|
53
|
+
with pytest.raises(InvalidFrequencyError):
|
|
54
|
+
validate_frequency("invalid")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class TestNasaPowerS3Client:
|
|
58
|
+
"""Test the main NasaPowerS3 client."""
|
|
59
|
+
|
|
60
|
+
def test_client_initialization(self):
|
|
61
|
+
"""Client should initialize without errors."""
|
|
62
|
+
client = NasaPowerS3()
|
|
63
|
+
assert client is not None
|
|
64
|
+
|
|
65
|
+
def test_invalid_collection(self):
|
|
66
|
+
"""Invalid collection should raise DataAccessError."""
|
|
67
|
+
client = NasaPowerS3()
|
|
68
|
+
with pytest.raises((ValueError, DataAccessError)):
|
|
69
|
+
client.get_data(
|
|
70
|
+
lat=30.6,
|
|
71
|
+
lon=-96.3,
|
|
72
|
+
start_date="2023-01-01",
|
|
73
|
+
end_date="2023-01-10",
|
|
74
|
+
variables=["T2M"],
|
|
75
|
+
collection="invalid_collection",
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
@pytest.mark.integration
|
|
79
|
+
def test_real_data_fetch(self):
|
|
80
|
+
"""Integration test: Fetch real data from S3."""
|
|
81
|
+
client = NasaPowerS3()
|
|
82
|
+
df = client.get_data(
|
|
83
|
+
lat=30.6,
|
|
84
|
+
lon=-96.3,
|
|
85
|
+
start_date="2023-01-01",
|
|
86
|
+
end_date="2023-01-05",
|
|
87
|
+
variables=["T2M"],
|
|
88
|
+
frequency="daily",
|
|
89
|
+
collection="meteorology",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Verify structure
|
|
93
|
+
assert isinstance(df, pd.DataFrame)
|
|
94
|
+
assert len(df) == 5 # 5 days
|
|
95
|
+
assert "T2M" in df.columns
|
|
96
|
+
assert "time" in df.columns
|
|
97
|
+
assert "lat" in df.columns
|
|
98
|
+
assert "lon" in df.columns
|
|
99
|
+
|
|
100
|
+
# Verify data types
|
|
101
|
+
assert df["T2M"].dtype in [np.float32, np.float64]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_update_buckets_template(self):
|
|
105
|
+
"""Test updating a collection with a new template."""
|
|
106
|
+
client = NasaPowerS3()
|
|
107
|
+
new_template = "s3://new-bucket/power_{freq}.zarr"
|
|
108
|
+
client.update_buckets("new_collection", url=new_template)
|
|
109
|
+
|
|
110
|
+
# We can't easily fetch from a fake S3 URL without more mocking,
|
|
111
|
+
# but we can verify the internal state.
|
|
112
|
+
assert client._buckets["new_collection"] == new_template
|
|
113
|
+
|
|
114
|
+
def test_update_buckets_override(self):
|
|
115
|
+
"""Test updating a specific frequency override."""
|
|
116
|
+
client = NasaPowerS3()
|
|
117
|
+
client.update_buckets("meteorology", frequency="daily", url="s3://custom-daily.zarr")
|
|
118
|
+
|
|
119
|
+
assert isinstance(client._buckets["meteorology"], dict)
|
|
120
|
+
assert client._buckets["meteorology"]["daily"] == "s3://custom-daily.zarr"
|
|
121
|
+
|
|
122
|
+
def test_instance_isolation(self):
|
|
123
|
+
"""Test that updating one client doesn't affect another."""
|
|
124
|
+
client1 = NasaPowerS3()
|
|
125
|
+
client2 = NasaPowerS3()
|
|
126
|
+
|
|
127
|
+
client1.update_buckets("meteorology", frequency="daily", url="s3://custom-daily.zarr")
|
|
128
|
+
|
|
129
|
+
# client1 should be updated
|
|
130
|
+
assert isinstance(client1._buckets["meteorology"], dict)
|
|
131
|
+
# client2 should still have the default template string
|
|
132
|
+
assert isinstance(client2._buckets["meteorology"], str)
|
|
133
|
+
assert "{freq}" in client2._buckets["meteorology"]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class TestExceptions:
|
|
137
|
+
"""Test custom exception classes."""
|
|
138
|
+
|
|
139
|
+
def test_invalid_coordinate_error(self):
|
|
140
|
+
"""InvalidCoordinateError should be raisable with message."""
|
|
141
|
+
with pytest.raises(InvalidCoordinateError, match="test message"):
|
|
142
|
+
raise InvalidCoordinateError("test message")
|
|
143
|
+
|
|
144
|
+
def test_variable_not_found_error(self):
|
|
145
|
+
"""VariableNotFoundError should be raisable."""
|
|
146
|
+
with pytest.raises(VariableNotFoundError):
|
|
147
|
+
raise VariableNotFoundError("Variable XYZ not found")
|
|
148
|
+
|
|
149
|
+
def test_data_access_error(self):
|
|
150
|
+
"""DataAccessError should be raisable."""
|
|
151
|
+
with pytest.raises(DataAccessError):
|
|
152
|
+
raise DataAccessError("S3 connection failed")
|