ecoenv 0.1.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.
ecoenv-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 EcoToolBox
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.
ecoenv-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: ecoenv
3
+ Version: 0.1.0
4
+ Summary: A Python library for ecological data collection and environmental analysis
5
+ Author: Ane Simões
6
+ Author-email: anes.2017@alunos.utfpr.edu.br
7
+ License: MIT
8
+ Requires-Python: >=3.7
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: ee
12
+ Requires-Dist: pandas
13
+ Requires-Dist: requests
14
+ Requires-Dist: python-dotenv
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: license
20
+ Dynamic: license-file
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+
26
+ # USAGE.md
27
+
28
+ ## Overview
29
+
30
+ **ecoenv** is a library designed to facilitate data management and processing for scientific and research applications.
31
+
32
+ ## Getting Started
33
+
34
+ ### Installation
35
+
36
+ ```bash
37
+ pip install ecoenv
38
+ ```
39
+
40
+ ### Basic Usage
41
+
42
+ ```python
43
+ import ecoenv
44
+
45
+
46
+ ecoenv.autenticateEE('project_name')
47
+ ecoenv.get_environment_data(gdf, ndvi=True, ndwi=True, temperature=True, precipitation=True)
48
+ ```
49
+ ## Functions
50
+ - `autenticateEE`: Authenticates the user with the Earth Engine API using the specified project name.
51
+ - `get_environment_data`: Retrieves environmental data based on the provided geospatial data frame (gdf) and specified parameters (ndvi, ndwi, temperature, precipitation).
52
+
ecoenv-0.1.0/README.md ADDED
@@ -0,0 +1,48 @@
1
+
2
+ # EcoEnv
3
+
4
+ ## Overview
5
+
6
+ EcoEnv is a project focused on environmental monitoring and ecological analysis. This repository contains tools and utilities for tracking environmental metrics and managing ecosystem data.
7
+
8
+ ## Features
9
+
10
+ - Environmental data collection
11
+ - Ecological metrics analysis
12
+ - Data visualization
13
+ - Real-time monitoring
14
+
15
+ ## Getting Started
16
+
17
+ ### Prerequisites
18
+
19
+ - Python 3.8+
20
+ - pip or conda
21
+
22
+ ### Installation
23
+
24
+ ```bash
25
+ git clone https://github.com/EcoToolBox/EcoEnv.git
26
+ cd EcoEnv
27
+ pip install -r requirements.txt
28
+ ```
29
+
30
+ ### Usage
31
+
32
+ ```python
33
+ import ecoenk
34
+
35
+ # Your code here
36
+ ```
37
+
38
+ ## Contributing
39
+
40
+ Contributions are welcome! Please fork the repository and submit a pull request.
41
+
42
+ ## License
43
+
44
+ This project is licensed under the MIT License - see the LICENSE file for details.
45
+
46
+ ## Support
47
+
48
+ For issues and questions, please open an issue on GitHub.
@@ -0,0 +1,21 @@
1
+ from .env_request import get_environment_data, autenticateEE
2
+ # from .gbif import save_gbif_credentials, delete_gbif_credentials, get_species_autocomplete
3
+ # from .speciesLink import save_specieslink_apikey, delete_specieslink_apikey
4
+
5
+ __all__ = [
6
+ "get_environment_data",
7
+ "autenticateEE"
8
+ ]
9
+
10
+ def version():
11
+ return "0.0.1"
12
+ def describe ():
13
+ description = (
14
+ "Eco Env Library\n"
15
+ "Version: {}\n"
16
+ "Implement functions to get environment data from GEE and (soon) GlobalBioticInteractions as:\n"
17
+ " - get_environment_data\n"
18
+ " - autenticateEE\n"
19
+ ). format (version())
20
+ print (description)
21
+ return description
@@ -0,0 +1,51 @@
1
+
2
+ import hashlib
3
+ import ee
4
+ import os
5
+ from pathlib import Path
6
+ import pandas as pd
7
+ from concurrent.futures import ThreadPoolExecutor, as_completed
8
+
9
+ from requests_cache import logger
10
+
11
+ from .sentinel import get_sentinel_properties
12
+ from .worldclim import get_average_temperature_and_precipation
13
+
14
+ path_root = Path(__file__).resolve().parent
15
+
16
+ def autenticateEE(project):
17
+ ee.Authenticate()
18
+ ee.Initialize(project=project)
19
+
20
+
21
+ def get_environment_data(gdf, ndvi=True, ndwi=True, temperature=True, precipitation=True):
22
+ if not (ndvi or ndwi or temperature or precipitation):
23
+ raise ValueError("At least one of the following parameters must be True: ndvi, ndwi, temperature, precipitation.")
24
+
25
+ index_list = []
26
+ if ndvi:
27
+ index_list.append('NDVI')
28
+ if ndwi:
29
+ index_list.append('NDWI')
30
+ if temperature:
31
+ index_list.append('Temperature')
32
+ if precipitation:
33
+ index_list.append('Precipitation')
34
+ try:
35
+ env_data = None
36
+ logger.info("Fetching fresh data from sources...")
37
+
38
+ if ndvi or ndwi:
39
+ env_data = get_sentinel_properties(gdf, 250, ndvi, ndwi)
40
+
41
+ if temperature or precipitation:
42
+ if(env_data is None):
43
+ env_data = get_average_temperature_and_precipation(gdf, batch_size=5000, temperature=temperature, precipitation=precipitation)
44
+ else:
45
+ env_data = get_average_temperature_and_precipation(env_data, batch_size=5000, temperature=temperature, precipitation=precipitation)
46
+
47
+ return env_data
48
+
49
+ except Exception as e:
50
+ logger.error(f"Critical error retrieving environment data: {e}", exc_info=True)
51
+ return pd.DataFrame()
File without changes
@@ -0,0 +1,197 @@
1
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ import hashlib
3
+ import ee
4
+ import geopandas as gpd
5
+ import pandas as pd
6
+ from datetime import date, timedelta
7
+ import json
8
+ from pathlib import Path
9
+ from diskcache import Cache
10
+
11
+ NDVI = True
12
+ NDWI = True
13
+
14
+ path_root = Path(__file__).resolve().parent
15
+
16
+ cache = Cache(path_root / ".sentinel_cache")
17
+
18
+
19
+ def chunk_gdf(gdf, size=500):
20
+ n = len(gdf)
21
+ for i in range(0, n, size):
22
+ yield gdf.iloc[i:i+size]
23
+
24
+
25
+ def add_index(img):
26
+ index = []
27
+
28
+ if NDVI:
29
+ index.append(img.normalizedDifference(['B8', 'B4']).rename('NDVI'))
30
+
31
+ if NDWI:
32
+ index.append(img.normalizedDifference(['B3', 'B8']).rename('NDWI'))
33
+
34
+ return img.addBands(index)
35
+
36
+
37
+ def generate_cache_key(gdf, ndvi, ndwi):
38
+ lat_min = gdf.geometry.bounds['miny'].min()
39
+ lat_max = gdf.geometry.bounds['maxy'].max()
40
+ lon_min = gdf.geometry.bounds['minx'].min()
41
+ lon_max = gdf.geometry.bounds['maxx'].max()
42
+
43
+ date_min = gdf['eventDate'].min()
44
+ date_max = gdf['eventDate'].max()
45
+
46
+ normalized = {
47
+ "lat_min": float(lat_min),
48
+ "lat_max": float(lat_max),
49
+ "lon_min": float(lon_min),
50
+ "lon_max": float(lon_max),
51
+ "date_min": str(date_min),
52
+ "date_max": str(date_max),
53
+ "ndvi": ndvi,
54
+ "ndwi": ndwi,
55
+ "size": len(gdf)
56
+ }
57
+
58
+ return hashlib.md5(
59
+ json.dumps(normalized, sort_keys=True).encode()
60
+ ).hexdigest()
61
+
62
+
63
+ def get_index_from_sentinel_batch(batch, ndvi, ndwi):
64
+
65
+ print(f"Processing batch: {len(batch)}")
66
+
67
+ if pd.Timestamp(batch['eventDate'].max()) < pd.Timestamp('2019-01-01'):
68
+ startDate = date.strftime(date.today() - timedelta(30), '%Y-%m-%d')
69
+ endDate = date.strftime(date.today(), '%Y-%m-%d')
70
+ else:
71
+ startDate = batch['eventDate'].min()
72
+ endDate = batch['eventDate'].max()
73
+
74
+ index = []
75
+ if ndvi:
76
+ index.append('NDVI')
77
+ if ndwi:
78
+ index.append('NDWI')
79
+
80
+ img = (
81
+ ee.ImageCollection('COPERNICUS/S2_SR_HARMONIZED')
82
+ .filterDate(startDate, endDate)
83
+ .map(add_index)
84
+ .select(index)
85
+ .median()
86
+ )
87
+
88
+ batch['eventDate'] = batch['eventDate'].astype(str)
89
+ geojson = json.loads(batch.to_json())
90
+ fc = ee.FeatureCollection(geojson)
91
+
92
+ sampled = img.reduceRegions(
93
+ collection=fc,
94
+ reducer=ee.Reducer.mean(),
95
+ scale=30
96
+ )
97
+
98
+ result = sampled.getInfo()
99
+
100
+ gdf_out = gpd.GeoDataFrame.from_features(
101
+ result["features"],
102
+ crs="EPSG:4326"
103
+ )
104
+
105
+ if not ndvi or not ndwi:
106
+ gdf_out = gdf_out.rename(columns={'mean': index[0]})
107
+
108
+ return gdf_out
109
+
110
+ def get_sentinel_properties(
111
+ gdf,
112
+ batch_size=150,
113
+ ndvi=True,
114
+ ndwi=True
115
+ ):
116
+
117
+ if not ndvi and not ndwi:
118
+ raise ValueError("At least one of NDVI or NDWI must be True.")
119
+
120
+ gdf = gdf.sort_values("eventDate", ascending=True)
121
+
122
+ cached_rows = []
123
+ missing_rows = []
124
+
125
+ # 🔍 Separa registros já existentes dos que faltam
126
+ for _, row in gdf.iterrows():
127
+ key = generate_point_key(row, ndvi, ndwi)
128
+
129
+ if key in cache:
130
+ cached_rows.append(cache[key])
131
+ else:
132
+ missing_rows.append(row)
133
+
134
+ print(f"cached: {len(cached_rows)}")
135
+ print(f"missing: {len(missing_rows)}")
136
+
137
+ new_results = []
138
+
139
+ # 🔥 Busca só os faltantes
140
+ if missing_rows:
141
+ missing_gdf = gpd.GeoDataFrame(
142
+ missing_rows,
143
+ geometry=gdf.geometry.name,
144
+ crs=gdf.crs
145
+ )
146
+
147
+ batches = list(chunk_gdf(missing_gdf, batch_size))
148
+
149
+ with ThreadPoolExecutor(max_workers=4) as executor:
150
+ futures = [
151
+ executor.submit(
152
+ get_index_from_sentinel_batch,
153
+ batch,
154
+ ndvi,
155
+ ndwi
156
+ )
157
+ for batch in batches
158
+ ]
159
+
160
+ for future in futures:
161
+ result = future.result()
162
+ new_results.append(result)
163
+
164
+ if new_results:
165
+ new_df = pd.concat(new_results, ignore_index=True)
166
+
167
+ for _, row in new_df.iterrows():
168
+ key = generate_point_key(row, ndvi, ndwi)
169
+ cache.set(key, row)
170
+
171
+ else:
172
+ new_df = pd.DataFrame()
173
+
174
+ else:
175
+ new_df = pd.DataFrame()
176
+
177
+ if cached_rows:
178
+ cached_df = pd.DataFrame(cached_rows)
179
+ else:
180
+ cached_df = pd.DataFrame()
181
+
182
+ final_df = pd.concat([cached_df, new_df], ignore_index=True)
183
+
184
+ return final_df
185
+
186
+ def generate_point_key(row, ndvi, ndwi):
187
+ normalized = {
188
+ "lat": float(row["latitude"]),
189
+ "lon": float(row["longitude"]),
190
+ "date": str(row["eventDate"]),
191
+ "ndvi": ndvi,
192
+ "ndwi": ndwi
193
+ }
194
+
195
+ return hashlib.md5(
196
+ json.dumps(normalized, sort_keys=True).encode()
197
+ ).hexdigest()
@@ -0,0 +1,154 @@
1
+ from concurrent.futures import ThreadPoolExecutor, as_completed
2
+ import hashlib
3
+ import ee
4
+ import geopandas as gpd
5
+ import pandas as pd
6
+ import json
7
+ from pathlib import Path
8
+ from diskcache import Cache
9
+
10
+ path_root = Path(__file__).resolve().parent
11
+
12
+ # 🔥 Cache persistente
13
+ cache = Cache(path_root / ".worldclim_cache")
14
+
15
+
16
+ def chunk_gdf(gdf, size=500):
17
+ n = len(gdf)
18
+ for i in range(0, n, size):
19
+ yield gdf.iloc[i:i+size]
20
+
21
+ def generate_cache_key(gdf, index, scale):
22
+ if isinstance(gdf, pd.DataFrame):
23
+ gdf = gpd.GeoDataFrame(gdf, geometry=gpd.points_from_xy(gdf.longitude, gdf.latitude), crs='EPSG:4326')
24
+
25
+ if not isinstance(gdf, gpd.GeoDataFrame):
26
+ raise TypeError(
27
+ f"Expected GeoDataFrame, got {type(gdf)}"
28
+ )
29
+
30
+ if gdf.geometry is None:
31
+ raise ValueError("GeoDataFrame has no geometry column.")
32
+
33
+ if gdf.empty:
34
+ raise ValueError("GeoDataFrame is empty.")
35
+
36
+ bounds = gdf.total_bounds
37
+
38
+ lat_min, lon_min, lat_max, lon_max = (
39
+ bounds[1], # miny
40
+ bounds[0], # minx
41
+ bounds[3], # maxy
42
+ bounds[2], # maxx
43
+ )
44
+
45
+ date_min = gdf['eventDate'].min()
46
+ date_max = gdf['eventDate'].max()
47
+
48
+ normalized = {
49
+ "lat_min": float(lat_min),
50
+ "lat_max": float(lat_max),
51
+ "lon_min": float(lon_min),
52
+ "lon_max": float(lon_max),
53
+ "date_min": str(date_min),
54
+ "date_max": str(date_max),
55
+ "index": index,
56
+ "scale": scale,
57
+ "size": len(gdf)
58
+ }
59
+
60
+ return hashlib.md5(
61
+ json.dumps(normalized, sort_keys=True).encode()
62
+ ).hexdigest()
63
+
64
+ def add_worldclim_properties(gdf, img, scale):
65
+
66
+ print(f"Processing {len(gdf)} records...")
67
+
68
+ gdf['eventDate'] = gdf['eventDate'].astype(str)
69
+ geojson = json.loads(gdf.to_json())
70
+ fc = ee.FeatureCollection(geojson)
71
+
72
+ sampled = img.reduceRegions(
73
+ collection=fc,
74
+ reducer=ee.Reducer.mean(),
75
+ scale=scale
76
+ )
77
+
78
+ result = sampled.getInfo()
79
+
80
+ gdf_out = gpd.GeoDataFrame.from_features(
81
+ result["features"],
82
+ crs="EPSG:4326"
83
+ )
84
+
85
+ return gdf_out
86
+
87
+
88
+ def get_average_temperature_and_precipation(
89
+ gdf,
90
+ scale=1000,
91
+ batch_size=5000,
92
+ temperature=True,
93
+ precipitation=True
94
+ ):
95
+
96
+ index = []
97
+
98
+ if temperature:
99
+ index.append("tavg")
100
+
101
+ if precipitation:
102
+ index.append("prec")
103
+
104
+ if not index:
105
+ raise ValueError("At least temperature or precipitation must be True.")
106
+
107
+ cache_key = generate_cache_key(gdf, index, scale)
108
+
109
+ if cache_key in cache:
110
+ print("DiskCache full hit")
111
+ return cache[cache_key]
112
+
113
+ print("Downloading full dataset from GEE...")
114
+
115
+ img = (
116
+ ee.ImageCollection("WORLDCLIM/V1/MONTHLY")
117
+ .select(index)
118
+ .mean()
119
+ .multiply(0.1)
120
+ )
121
+
122
+ if len(gdf) > batch_size:
123
+
124
+ all_batches = []
125
+
126
+ with ThreadPoolExecutor(max_workers=4) as executor:
127
+
128
+ futures = [
129
+ executor.submit(
130
+ add_worldclim_properties,
131
+ batch,
132
+ img,
133
+ scale
134
+ )
135
+ for batch in chunk_gdf(gdf, size=batch_size)
136
+ ]
137
+
138
+ for future in as_completed(futures):
139
+ all_batches.append(future.result())
140
+
141
+ final_gdf = gpd.GeoDataFrame(
142
+ pd.concat(all_batches, ignore_index=True),
143
+ crs="EPSG:4326"
144
+ )
145
+
146
+ else:
147
+ final_gdf = add_worldclim_properties(gdf, img, scale)
148
+
149
+ if not temperature or not precipitation:
150
+ final_gdf = final_gdf.rename(columns={"mean": index[0]})
151
+
152
+ cache.set(cache_key, final_gdf)
153
+
154
+ return final_gdf
@@ -0,0 +1,52 @@
1
+ Metadata-Version: 2.4
2
+ Name: ecoenv
3
+ Version: 0.1.0
4
+ Summary: A Python library for ecological data collection and environmental analysis
5
+ Author: Ane Simões
6
+ Author-email: anes.2017@alunos.utfpr.edu.br
7
+ License: MIT
8
+ Requires-Python: >=3.7
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: ee
12
+ Requires-Dist: pandas
13
+ Requires-Dist: requests
14
+ Requires-Dist: python-dotenv
15
+ Dynamic: author
16
+ Dynamic: author-email
17
+ Dynamic: description
18
+ Dynamic: description-content-type
19
+ Dynamic: license
20
+ Dynamic: license-file
21
+ Dynamic: requires-dist
22
+ Dynamic: requires-python
23
+ Dynamic: summary
24
+
25
+
26
+ # USAGE.md
27
+
28
+ ## Overview
29
+
30
+ **ecoenv** is a library designed to facilitate data management and processing for scientific and research applications.
31
+
32
+ ## Getting Started
33
+
34
+ ### Installation
35
+
36
+ ```bash
37
+ pip install ecoenv
38
+ ```
39
+
40
+ ### Basic Usage
41
+
42
+ ```python
43
+ import ecoenv
44
+
45
+
46
+ ecoenv.autenticateEE('project_name')
47
+ ecoenv.get_environment_data(gdf, ndvi=True, ndwi=True, temperature=True, precipitation=True)
48
+ ```
49
+ ## Functions
50
+ - `autenticateEE`: Authenticates the user with the Earth Engine API using the specified project name.
51
+ - `get_environment_data`: Retrieves environmental data based on the provided geospatial data frame (gdf) and specified parameters (ndvi, ndwi, temperature, precipitation).
52
+
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ setup.py
4
+ ecoenv/__init__.py
5
+ ecoenv/env_request.py
6
+ ecoenv/globi.py
7
+ ecoenv/sentinel.py
8
+ ecoenv/worldclim.py
9
+ ecoenv.egg-info/PKG-INFO
10
+ ecoenv.egg-info/SOURCES.txt
11
+ ecoenv.egg-info/dependency_links.txt
12
+ ecoenv.egg-info/requires.txt
13
+ ecoenv.egg-info/top_level.txt
14
+ tests/test_ecoenv.py
@@ -0,0 +1,4 @@
1
+ ee
2
+ pandas
3
+ requests
4
+ python-dotenv
@@ -0,0 +1 @@
1
+ ecoenv
ecoenv-0.1.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
ecoenv-0.1.0/setup.py ADDED
@@ -0,0 +1,20 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ setup(
4
+ name='ecoenv',
5
+ version='0.1.0',
6
+ description='A Python library for ecological data collection and environmental analysis',
7
+ long_description=open('USAGE.md').read(),
8
+ long_description_content_type='text/markdown',
9
+ author='Ane Simões',
10
+ author_email='anes.2017@alunos.utfpr.edu.br',
11
+ packages=find_packages(),
12
+ install_requires=[
13
+ "ee",
14
+ "pandas",
15
+ "requests",
16
+ "python-dotenv",
17
+ ],
18
+ license='MIT',
19
+ python_requires='>=3.7',
20
+ )
@@ -0,0 +1,387 @@
1
+ import pytest
2
+ import pandas as pd
3
+ import geopandas as gpd
4
+ from shapely.geometry import Point
5
+ from unittest.mock import patch, MagicMock
6
+ from pathlib import Path
7
+
8
+ from ecoenv.env_request import autenticateEE, get_environment_data
9
+ from ecoenv.sentinel import (
10
+ chunk_gdf,
11
+ generate_cache_key as sentinel_cache_key,
12
+ generate_point_key,
13
+ get_index_from_sentinel_batch,
14
+ get_sentinel_properties,
15
+ add_index,
16
+ )
17
+ from ecoenv.worldclim import (
18
+ chunk_gdf as worldclim_chunk_gdf,
19
+ generate_cache_key as worldclim_cache_key,
20
+ add_worldclim_properties,
21
+ get_average_temperature_and_precipation,
22
+ )
23
+
24
+
25
+ @pytest.fixture
26
+ def sample_gdf():
27
+ """Create a sample GeoDataFrame for testing."""
28
+ data = {
29
+ 'latitude': [10.0, 20.0, 30.0],
30
+ 'longitude': [50.0, 60.0, 70.0],
31
+ 'eventDate': ['2020-01-01', '2020-06-15', '2021-01-01'],
32
+ }
33
+ gdf = gpd.GeoDataFrame(
34
+ data,
35
+ geometry=[Point(50, 10), Point(60, 20), Point(70, 30)],
36
+ crs='EPSG:4326'
37
+ )
38
+ return gdf
39
+
40
+
41
+ @pytest.fixture
42
+ def sample_gdf_with_dates():
43
+ """Create a GeoDataFrame with proper date formatting."""
44
+ data = {
45
+ 'latitude': [10.0, 20.0],
46
+ 'longitude': [50.0, 60.0],
47
+ 'eventDate': pd.to_datetime(['2020-01-01', '2020-06-15']),
48
+ }
49
+ gdf = gpd.GeoDataFrame(
50
+ data,
51
+ geometry=[Point(50, 10), Point(60, 20)],
52
+ crs='EPSG:4326'
53
+ )
54
+ return gdf
55
+
56
+
57
+ class TestEnvRequest:
58
+ """Tests for env_request.py"""
59
+
60
+ @patch('ecoenv.env_request.ee')
61
+ def test_autenticateEE(self, mock_ee):
62
+ """Test authentication with Earth Engine."""
63
+ autenticateEE("test-project")
64
+ mock_ee.Authenticate.assert_called_once()
65
+ mock_ee.Initialize.assert_called_once_with(project="test-project")
66
+
67
+ @patch('ecoenv.env_request.get_sentinel_properties')
68
+ @patch('ecoenv.env_request.get_average_temperature_and_precipation')
69
+ def test_get_environment_data_all_true(self, mock_temp, mock_sentinel, sample_gdf):
70
+ """Test get_environment_data with all parameters True."""
71
+ mock_sentinel.return_value = sample_gdf
72
+ mock_temp.return_value = sample_gdf
73
+
74
+ result = get_environment_data(
75
+ sample_gdf,
76
+ ndvi=True,
77
+ ndwi=True,
78
+ temperature=True,
79
+ precipitation=True
80
+ )
81
+
82
+ assert mock_sentinel.called
83
+ assert mock_temp.called
84
+
85
+ @patch('ecoenv.env_request.get_sentinel_properties')
86
+ def test_get_environment_data_ndvi_only(self, mock_sentinel, sample_gdf):
87
+ """Test get_environment_data with only NDVI."""
88
+ mock_sentinel.return_value = sample_gdf
89
+
90
+ result = get_environment_data(
91
+ sample_gdf,
92
+ ndvi=True,
93
+ ndwi=False,
94
+ temperature=False,
95
+ precipitation=False
96
+ )
97
+
98
+ mock_sentinel.assert_called_once()
99
+
100
+ def test_get_environment_data_all_false(self, sample_gdf):
101
+ """Test that ValueError is raised when all parameters are False."""
102
+ with pytest.raises(ValueError):
103
+ get_environment_data(
104
+ sample_gdf,
105
+ ndvi=False,
106
+ ndwi=False,
107
+ temperature=False,
108
+ precipitation=False
109
+ )
110
+
111
+ @patch('ecoenv.env_request.get_sentinel_properties')
112
+ @patch('ecoenv.env_request.get_average_temperature_and_precipation')
113
+ def test_get_environment_data_exception(self, mock_temp, mock_sentinel, sample_gdf):
114
+ """Test exception handling in get_environment_data."""
115
+ mock_sentinel.side_effect = Exception("API Error")
116
+
117
+ result = get_environment_data(sample_gdf)
118
+
119
+ assert isinstance(result, pd.DataFrame)
120
+ assert result.empty
121
+
122
+
123
+ class TestSentinel:
124
+ """Tests for sentinel.py"""
125
+
126
+ def test_chunk_gdf(self, sample_gdf):
127
+ """Test chunking of GeoDataFrame."""
128
+ chunks = list(chunk_gdf(sample_gdf, size=1))
129
+ assert len(chunks) == 3
130
+ assert len(chunks[0]) == 1
131
+
132
+ def test_chunk_gdf_size_larger_than_gdf(self, sample_gdf):
133
+ """Test chunking when chunk size is larger than GeoDataFrame."""
134
+ chunks = list(chunk_gdf(sample_gdf, size=10))
135
+ assert len(chunks) == 1
136
+ assert len(chunks[0]) == 3
137
+
138
+ def test_generate_point_key(self, sample_gdf_with_dates):
139
+ """Test generation of cache key for a point."""
140
+ row = sample_gdf_with_dates.iloc[0]
141
+ key = generate_point_key(row, ndvi=True, ndwi=True)
142
+
143
+ assert isinstance(key, str)
144
+ assert len(key) == 32 # MD5 hash length
145
+
146
+ def test_generate_point_key_consistency(self, sample_gdf_with_dates):
147
+ """Test that same input generates same key."""
148
+ row = sample_gdf_with_dates.iloc[0]
149
+ key1 = generate_point_key(row, ndvi=True, ndwi=False)
150
+ key2 = generate_point_key(row, ndvi=True, ndwi=False)
151
+
152
+ assert key1 == key2
153
+
154
+ def test_generate_point_key_different_params(self, sample_gdf_with_dates):
155
+ """Test that different parameters generate different keys."""
156
+ row = sample_gdf_with_dates.iloc[0]
157
+ key1 = generate_point_key(row, ndvi=True, ndwi=False)
158
+ key2 = generate_point_key(row, ndvi=False, ndwi=True)
159
+
160
+ assert key1 != key2
161
+
162
+ @patch('ecoenv.sentinel.ee')
163
+ def test_add_index(self, mock_ee):
164
+ """Test add_index function."""
165
+ mock_img = MagicMock()
166
+ mock_ndvi = MagicMock()
167
+ mock_ndwi = MagicMock()
168
+
169
+ mock_img.normalizedDifference.side_effect = [mock_ndvi, mock_ndwi]
170
+ mock_ndvi.rename.return_value = mock_ndvi
171
+ mock_ndwi.rename.return_value = mock_ndwi
172
+ mock_img.addBands.return_value = mock_img
173
+
174
+ result = add_index(mock_img)
175
+
176
+ assert mock_img.normalizedDifference.called
177
+ assert mock_img.addBands.called
178
+
179
+ def test_sentinel_cache_key_generation(self, sample_gdf_with_dates):
180
+ """Test cache key generation for sentinel data."""
181
+ key = sentinel_cache_key(sample_gdf_with_dates, ndvi=True, ndwi=True)
182
+
183
+ assert isinstance(key, str)
184
+ assert len(key) == 32
185
+
186
+ def test_sentinel_cache_key_different_dates(self, sample_gdf_with_dates):
187
+ """Test that different dates generate different cache keys."""
188
+ gdf2 = sample_gdf_with_dates.copy()
189
+ gdf2['eventDate'] = pd.to_datetime(['2019-01-01', '2019-06-15'])
190
+
191
+ key1 = sentinel_cache_key(sample_gdf_with_dates, ndvi=True, ndwi=True)
192
+ key2 = sentinel_cache_key(gdf2, ndvi=True, ndwi=True)
193
+
194
+ assert key1 != key2
195
+
196
+ def test_get_sentinel_properties_ndvi_ndwi_both_false(self, sample_gdf_with_dates):
197
+ """Test ValueError when both NDVI and NDWI are False."""
198
+ with pytest.raises(ValueError):
199
+ get_sentinel_properties(sample_gdf_with_dates, ndvi=False, ndwi=False)
200
+
201
+ @patch('ecoenv.sentinel.get_index_from_sentinel_batch')
202
+ def test_get_sentinel_properties_single_batch(self, mock_batch, sample_gdf_with_dates):
203
+ """Test get_sentinel_properties with single batch."""
204
+ mock_batch.return_value = sample_gdf_with_dates
205
+
206
+ result = get_sentinel_properties(
207
+ sample_gdf_with_dates,
208
+ batch_size=150,
209
+ ndvi=True,
210
+ ndwi=True
211
+ )
212
+
213
+ assert isinstance(result, pd.DataFrame)
214
+
215
+
216
+ class TestWorldclim:
217
+ """Tests for worldclim.py"""
218
+
219
+ def test_worldclim_chunk_gdf(self, sample_gdf):
220
+ """Test chunking of worldclim GeoDataFrame."""
221
+ chunks = list(worldclim_chunk_gdf(sample_gdf, size=2))
222
+ assert len(chunks) == 2
223
+
224
+ def test_worldclim_generate_cache_key_with_geodataframe(self, sample_gdf_with_dates):
225
+ """Test cache key generation for worldclim with GeoDataFrame."""
226
+ key = worldclim_cache_key(sample_gdf_with_dates, ["tavg"], 1000)
227
+
228
+ assert isinstance(key, str)
229
+ assert len(key) == 32
230
+
231
+ def test_worldclim_generate_cache_key_with_dataframe(self):
232
+ """Test cache key generation with regular DataFrame."""
233
+ data = {
234
+ 'latitude': [10.0, 20.0],
235
+ 'longitude': [50.0, 60.0],
236
+ 'eventDate': pd.to_datetime(['2020-01-01', '2020-06-15']),
237
+ }
238
+ df = pd.DataFrame(data)
239
+
240
+ key = worldclim_cache_key(df, ["tavg"], 1000)
241
+
242
+ assert isinstance(key, str)
243
+ assert len(key) == 32
244
+
245
+ def test_worldclim_generate_cache_key_invalid_type(self):
246
+ """Test that invalid type raises TypeError."""
247
+ with pytest.raises(TypeError):
248
+ worldclim_cache_key([1, 2, 3], ["tavg"], 1000)
249
+
250
+ def test_worldclim_generate_cache_key_empty_gdf(self, sample_gdf_with_dates):
251
+ """Test that empty GeoDataFrame raises ValueError."""
252
+ empty_gdf = sample_gdf_with_dates.iloc[0:0]
253
+
254
+ with pytest.raises(ValueError):
255
+ worldclim_cache_key(empty_gdf, ["tavg"], 1000)
256
+
257
+ @patch('ecoenv.worldclim.ee')
258
+ def test_add_worldclim_properties(self, mock_ee, sample_gdf_with_dates):
259
+ """Test adding worldclim properties to GeoDataFrame."""
260
+ mock_img = MagicMock()
261
+ mock_fc = MagicMock()
262
+ mock_ee.FeatureCollection.return_value = mock_fc
263
+
264
+ mock_sampled = MagicMock()
265
+ mock_img.reduceRegions.return_value = mock_sampled
266
+ mock_sampled.getInfo.return_value = {
267
+ "features": [
268
+ {
269
+ "type": "Feature",
270
+ "geometry": {"type": "Point", "coordinates": [50, 10]},
271
+ "properties": {"tavg": 25.0}
272
+ }
273
+ ]
274
+ }
275
+
276
+ result = add_worldclim_properties(sample_gdf_with_dates, mock_img, 1000)
277
+
278
+ assert isinstance(result, gpd.GeoDataFrame)
279
+
280
+ def test_get_average_temperature_and_precipation_no_index(self, sample_gdf_with_dates):
281
+ """Test ValueError when both temperature and precipitation are False."""
282
+ with pytest.raises(ValueError):
283
+ get_average_temperature_and_precipation(
284
+ sample_gdf_with_dates,
285
+ temperature=False,
286
+ precipitation=False
287
+ )
288
+
289
+ @patch('ecoenv.worldclim.add_worldclim_properties')
290
+ @patch('ecoenv.worldclim.ee')
291
+ def test_get_average_temperature_small_batch(
292
+ self,
293
+ mock_ee,
294
+ mock_add,
295
+ sample_gdf_with_dates
296
+ ):
297
+ """Test temperature and precipitation retrieval for small batch."""
298
+ mock_add.return_value = sample_gdf_with_dates
299
+ mock_collection = MagicMock()
300
+ mock_ee.ImageCollection.return_value = mock_collection
301
+ mock_collection.select.return_value = mock_collection
302
+ mock_collection.mean.return_value = mock_collection
303
+ mock_collection.multiply.return_value = MagicMock()
304
+
305
+ result = get_average_temperature_and_precipation(
306
+ sample_gdf_with_dates,
307
+ batch_size=5000,
308
+ temperature=True,
309
+ precipitation=True
310
+ )
311
+
312
+ assert isinstance(result, gpd.GeoDataFrame)
313
+
314
+ @patch('ecoenv.worldclim.add_worldclim_properties')
315
+ @patch('ecoenv.worldclim.ee')
316
+ def test_get_average_temperature_large_batch(
317
+ self,
318
+ mock_ee,
319
+ mock_add,
320
+ sample_gdf_with_dates
321
+ ):
322
+ """Test temperature retrieval with multiple batches."""
323
+ large_gdf = pd.concat(
324
+ [sample_gdf_with_dates] * 3000,
325
+ ignore_index=True
326
+ )
327
+ large_gdf = gpd.GeoDataFrame(
328
+ large_gdf,
329
+ geometry=sample_gdf_with_dates.geometry,
330
+ crs='EPSG:4326'
331
+ )
332
+
333
+ mock_add.return_value = large_gdf.iloc[:1000]
334
+ mock_collection = MagicMock()
335
+ mock_ee.ImageCollection.return_value = mock_collection
336
+ mock_collection.select.return_value = mock_collection
337
+ mock_collection.mean.return_value = mock_collection
338
+ mock_collection.multiply.return_value = MagicMock()
339
+
340
+ result = get_average_temperature_and_precipation(
341
+ large_gdf,
342
+ batch_size=1000,
343
+ temperature=True,
344
+ precipitation=False
345
+ )
346
+
347
+ assert isinstance(result, gpd.GeoDataFrame)
348
+
349
+ @patch('ecoenv.worldclim.ee')
350
+ def test_get_average_temperature_only(self, mock_ee, sample_gdf_with_dates):
351
+ """Test retrieving only temperature data."""
352
+ mock_collection = MagicMock()
353
+ mock_ee.ImageCollection.return_value = mock_collection
354
+ mock_collection.select.return_value = mock_collection
355
+ mock_collection.mean.return_value = mock_collection
356
+ mock_collection.multiply.return_value = MagicMock()
357
+
358
+ with patch('ecoenv.worldclim.add_worldclim_properties') as mock_add:
359
+ mock_add.return_value = sample_gdf_with_dates
360
+
361
+ result = get_average_temperature_and_precipation(
362
+ sample_gdf_with_dates,
363
+ temperature=True,
364
+ precipitation=False
365
+ )
366
+
367
+ assert isinstance(result, gpd.GeoDataFrame)
368
+
369
+ @patch('ecoenv.worldclim.ee')
370
+ def test_get_average_precipitation_only(self, mock_ee, sample_gdf_with_dates):
371
+ """Test retrieving only precipitation data."""
372
+ mock_collection = MagicMock()
373
+ mock_ee.ImageCollection.return_value = mock_collection
374
+ mock_collection.select.return_value = mock_collection
375
+ mock_collection.mean.return_value = mock_collection
376
+ mock_collection.multiply.return_value = MagicMock()
377
+
378
+ with patch('ecoenv.worldclim.add_worldclim_properties') as mock_add:
379
+ mock_add.return_value = sample_gdf_with_dates
380
+
381
+ result = get_average_temperature_and_precipation(
382
+ sample_gdf_with_dates,
383
+ temperature=False,
384
+ precipitation=True
385
+ )
386
+
387
+ assert isinstance(result, gpd.GeoDataFrame)