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.
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
57
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
57
+ [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](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,13 @@
1
+ pandas
2
+ xarray
3
+ zarr
4
+ s3fs
5
+ fsspec
6
+
7
+ [dev]
8
+ pytest
9
+ pytest-cov
10
+ black
11
+ isort
12
+ codespell
13
+ pre-commit
@@ -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")