hydroanomaly 0.7.3__py3-none-any.whl → 1.2.0__py3-none-any.whl

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.
hydroanomaly/__init__.py CHANGED
@@ -5,17 +5,20 @@ A simple Python package with just 3 modules:
5
5
  1. USGS turbidity data retrieval (returns data and site coordinates)
6
6
  2. Sentinel satellite bands retrieval
7
7
  3. Time series visualization
8
+ 4. Machine learning anomaly detection (One-Class SVM and Isolation Forest)
8
9
 
9
10
  That's it - nothing else!
10
11
  """
11
12
 
12
- __version__ = "0.7.3"
13
+ __version__ = "1.2.0"
13
14
  __author__ = "Ehsan Kahrizi (Ehsan.kahrizi@usu.edu)"
14
15
 
15
16
  # Import the 3 simple modules
16
17
  from .usgs_turbidity import get_turbidity, get_usgs_turbidity
17
- from .sentinel_bands import get_sentinel_bands, get_satellite_data, get_sentinel, get_sentinel_bands_gee
18
+ from .sentinel_bands import get_sentinel_bands, get_satellite_data, get_sentinel, get_sentinel_bands_gee, show_sentinel_ndwi_map
18
19
  from .visualize import plot_timeseries, plot_turbidity, plot_sentinel, plot_comparison, plot, visualize
20
+ from .ml import run_oneclass_svm, run_isolation_forest
21
+
19
22
 
20
23
  # Export everything
21
24
  __all__ = [
@@ -28,7 +31,7 @@ __all__ = [
28
31
  'get_sentinel_bands',
29
32
  'get_satellite_data',
30
33
  'get_sentinel',
31
-
34
+ 'show_sentinel_ndwi_map',
32
35
 
33
36
  # Visualization functions
34
37
  'plot_timeseries',
@@ -36,7 +39,11 @@ __all__ = [
36
39
  'plot_sentinel',
37
40
  'plot_comparison',
38
41
  'plot',
39
- 'visualize'
42
+ 'visualize',
43
+
44
+ # Machine learning functions
45
+ 'run_oneclass_svm',
46
+ 'run_isolation_forest'
40
47
  ]
41
48
 
42
49
  print(f"HydroAnomaly v{__version__} - Simple Water Data Package")
hydroanomaly/ml.py ADDED
@@ -0,0 +1,170 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from sklearn.preprocessing import StandardScaler
4
+ from sklearn.svm import OneClassSVM
5
+ from sklearn.ensemble import IsolationForest
6
+ from sklearn.metrics import f1_score, recall_score, precision_score
7
+ import matplotlib.pyplot as plt
8
+
9
+ # ============= Helper Functions =========================================================================
10
+ def match_nearest(row, usgs):
11
+ target_time = row['date']
12
+ same_day = usgs[usgs['date'] == target_time.date()]
13
+ if same_day.empty:
14
+ return np.nan
15
+ delta = (same_day['datetime'] - target_time).abs()
16
+ return same_day.loc[delta.idxmin(), 'Turbidity']
17
+
18
+
19
+
20
+ # ============= Preprocessing and Feature Engineering ========================================================
21
+ def preprocess_data(sentinel, usgs):
22
+ # Add matched turbidity
23
+ sentinel['Turbidity'] = sentinel.apply(lambda row: match_nearest(row, usgs), axis=1)
24
+ df = sentinel.dropna(subset=['Turbidity'])
25
+
26
+ # Water pixels filtering
27
+ if 'SCL' in df.columns and (df['SCL'] == 6).sum() > 0:
28
+ df = df[df['SCL'] == 6].drop_duplicates(subset=['B2', 'B3', 'B4'])
29
+
30
+ # Feature engineering
31
+ bands = ['B2','B3','B4','B5','B6','B7','B8','B8A','B9','B11','B12']
32
+ df['NDVI'] = (df['B8'] - df['B4']) / (df['B8'] + df['B4'])
33
+ df['NDWI'] = (df['B3'] - df['B8']) / (df['B3'] + df['B8'])
34
+ df['NDSI'] = (df['B3'] - df['B11']) / (df['B3'] + df['B11'])
35
+
36
+ df = df.sort_values('date').reset_index(drop=True)
37
+ df['Turbidity_diff1'] = df['Turbidity'].diff()
38
+ df['Turbidity_diff2'] = df['Turbidity_diff1'].diff()
39
+ thresh = 2 * df['Turbidity_diff2'].std()
40
+ df['spike'] = (df['Turbidity_diff2'].abs() > thresh).astype(int)
41
+ df = df.dropna()
42
+
43
+ # Class label
44
+ df['Classe'] = (df['Turbidity'] > 20).astype(int)
45
+ return df, bands
46
+
47
+
48
+ # ============= Anomaly Detection Methods ================================================================
49
+ def run_oneclass_svm(sentinel, usgs, plot=True):
50
+ """
51
+ Apply One-Class SVM anomaly detection on Sentinel/USGS data.
52
+ Returns: DataFrame with predictions, and best model parameters.
53
+ """
54
+ df, bands = preprocess_data(sentinel, usgs)
55
+ features = bands + ['NDVI','NDWI','NDSI','Turbidity_diff1','Turbidity_diff2','spike']
56
+ X = df[features].fillna(df[features].mean()).values
57
+ y = df['Classe'].values
58
+
59
+ scaler = StandardScaler()
60
+ X_scaled = scaler.fit_transform(X)
61
+
62
+ X_class0 = X_scaled[y == 0]
63
+ X_class1 = X_scaled[y == 1]
64
+
65
+ train_size = max(1, int(0.8 * len(X_class0)))
66
+ X_train = X_class0[:train_size]
67
+ X_test = np.vstack([X_class0[train_size:], X_class1])
68
+ y_test = np.array([0]*(len(X_class0)-train_size) + [1]*len(X_class1))
69
+
70
+ best_f1 = -1
71
+ best_model, best_y_pred, best_params = None, None, None
72
+ for gamma in ['auto', 'scale']:
73
+ for nu in [0.01, 0.05, 0.1, 0.2]:
74
+ model = OneClassSVM(kernel='rbf', gamma=gamma, nu=nu)
75
+ model.fit(X_train)
76
+ y_pred = np.where(model.predict(X_test) == 1, 0, 1)
77
+ if len(np.unique(y_pred)) > 1:
78
+ f1 = f1_score(y_test, y_pred)
79
+ if f1 > best_f1:
80
+ best_f1 = f1
81
+ best_model = model
82
+ best_y_pred = y_pred
83
+ best_params = {'gamma': gamma, 'nu': nu}
84
+
85
+ if best_f1 > -1:
86
+ df_out = df.iloc[-len(y_test):].copy()
87
+ df_out['predicted'] = best_y_pred
88
+ if plot:
89
+ plt.figure(figsize=(15,6))
90
+ plt.plot(df_out['date'], df_out['Turbidity'], label='Turbidity', color='blue')
91
+ plt.scatter(df_out[df_out['Classe']==1]['date'], df_out[df_out['Classe']==1]['Turbidity'],
92
+ color='red', marker='x', label='True Anomaly', s=100)
93
+ plt.scatter(df_out[df_out['predicted']==1]['date'], df_out[df_out['predicted']==1]['Turbidity'],
94
+ edgecolors='orange', facecolors='none', marker='o', label='Predicted Anomaly', s=80)
95
+ plt.title("True vs Predicted Anomalies (OneClassSVM)")
96
+ plt.xlabel("Date")
97
+ plt.ylabel("Turbidity")
98
+ plt.legend()
99
+ plt.grid(True)
100
+ plt.tight_layout()
101
+ plt.show()
102
+ return df_out, best_params, best_f1
103
+ else:
104
+ print("Could not find a good model. Try different hyperparameters.")
105
+ return None, None, None
106
+
107
+
108
+ # ============= Isolation Forest Method ================================================================
109
+ def run_isolation_forest(sentinel, usgs, plot=True):
110
+ """
111
+ Apply Isolation Forest anomaly detection on Sentinel/USGS data.
112
+ Returns: DataFrame with predictions, and best model parameters.
113
+ """
114
+ df, bands = preprocess_data(sentinel, usgs)
115
+ features = bands + ['NDVI','NDWI','NDSI','Turbidity_diff1','Turbidity_diff2','spike']
116
+ X = df[features].fillna(df[features].mean()).values
117
+ y = df['Classe'].values
118
+
119
+ scaler = StandardScaler()
120
+ X_scaled = scaler.fit_transform(X)
121
+
122
+ X_class0 = X_scaled[y == 0]
123
+ X_class1 = X_scaled[y == 1]
124
+
125
+ train_size = max(1, int(0.8 * len(X_class0)))
126
+ X_train = X_class0[:train_size]
127
+ X_test = np.vstack([X_class0[train_size:], X_class1])
128
+ y_test = np.array([0]*(len(X_class0)-train_size) + [1]*len(X_class1))
129
+
130
+ best_f1 = -1
131
+ best_model, best_y_pred, best_params = None, None, None
132
+ for contamination in [0.01, 0.05, 0.1, 0.15, 0.2, 0.3]:
133
+ model = IsolationForest(
134
+ n_estimators=100,
135
+ contamination=contamination,
136
+ max_samples='auto',
137
+ bootstrap=True,
138
+ random_state=42
139
+ )
140
+ model.fit(X_train)
141
+ y_pred = np.where(model.predict(X_test) == 1, 0, 1)
142
+ if len(np.unique(y_pred)) > 1:
143
+ f1 = f1_score(y_test, y_pred)
144
+ if f1 > best_f1:
145
+ best_f1 = f1
146
+ best_model = model
147
+ best_y_pred = y_pred
148
+ best_params = {'contamination': contamination}
149
+
150
+ if best_f1 > -1:
151
+ df_out = df.iloc[-len(y_test):].copy()
152
+ df_out['predicted'] = best_y_pred
153
+ if plot:
154
+ plt.figure(figsize=(15,6))
155
+ plt.plot(df_out['date'], df_out['Turbidity'], label='Turbidity', color='blue')
156
+ plt.scatter(df_out[df_out['Classe']==1]['date'], df_out[df_out['Classe']==1]['Turbidity'],
157
+ color='red', marker='x', label='True Anomaly', s=100)
158
+ plt.scatter(df_out[df_out['predicted']==1]['date'], df_out[df_out['predicted']==1]['Turbidity'],
159
+ edgecolors='orange', facecolors='none', marker='o', label='Predicted Anomaly', s=80)
160
+ plt.title("True vs Predicted Anomalies (Isolation Forest)")
161
+ plt.xlabel("Date")
162
+ plt.ylabel("Turbidity")
163
+ plt.legend()
164
+ plt.grid(True)
165
+ plt.tight_layout()
166
+ plt.show()
167
+ return df_out, best_params, best_f1
168
+ else:
169
+ print("Could not find a good model. Try different hyperparameters.")
170
+ return None, None, None
@@ -4,13 +4,15 @@ Sentinel-2 Satellite Data Retrieval using Google Earth Engine (GEE)
4
4
  This module provides a function to retrieve Sentinel-2 satellite band data
5
5
  for a specified location and time period, with masking and cloud filtering.
6
6
  """
7
- import ee
7
+ import ee
8
+ import geemap
8
9
  import pandas as pd
9
10
  import numpy as np
10
11
  from datetime import datetime, timedelta
11
12
  import requests
12
13
  import warnings
13
14
 
15
+ # Retrive data from Google Earth Engine ========================================================
14
16
  def get_sentinel_bands_gee(
15
17
  latitude: float,
16
18
  longitude: float,
@@ -98,6 +100,54 @@ def get_sentinel_bands_gee(
98
100
  df = df.set_index('date')
99
101
  return df
100
102
 
103
+
104
+ # Showing map with NDWI (Normalized Difference Water Index) ========================================================
105
+ def show_sentinel_ndwi_map(
106
+ latitude: float,
107
+ longitude: float,
108
+ start_date: str,
109
+ end_date: str,
110
+ buffer_meters: int = 20,
111
+ cloudy_pixel_percentage: int = 20,
112
+ zoom: int = 15
113
+ ):
114
+ """
115
+ Display an interactive map showing the NDWI, point, and buffer.
116
+
117
+ Args:
118
+ latitude (float): Latitude of the center point.
119
+ longitude (float): Longitude of the center point.
120
+ start_date (str): Start date as "YYYY-MM-DD".
121
+ end_date (str): End date as "YYYY-MM-DD".
122
+ buffer_meters (int): Buffer radius in meters.
123
+ cloudy_pixel_percentage (int): Max allowed cloud percentage.
124
+ zoom (int): Zoom level for map.
125
+ """
126
+ point = ee.Geometry.Point([longitude, latitude])
127
+ buffer = point.buffer(buffer_meters)
128
+
129
+ image = (ee.ImageCollection("COPERNICUS/S2_SR_HARMONIZED")
130
+ .filterBounds(buffer)
131
+ .filterDate(start_date, end_date)
132
+ .filter(ee.Filter.lt("CLOUDY_PIXEL_PERCENTAGE", cloudy_pixel_percentage))
133
+ .median())
134
+
135
+ ndwi = image.normalizedDifference(["B3", "B8"]).rename("NDWI")
136
+
137
+ ndwi_vis = {
138
+ 'min': 0,
139
+ 'max': 1,
140
+ 'palette': ['white', 'cyan', 'blue']}
141
+
142
+ Map = geemap.Map()
143
+ Map.centerObject(buffer, zoom=zoom)
144
+ Map.addLayer(ndwi, ndwi_vis, "NDWI (Water)")
145
+ Map.addLayer(point, {'color': 'yellow'}, 'Point')
146
+ Map.addLayer(buffer, {'color': 'red'}, 'Buffer')
147
+ Map.add_colorbar(ndwi_vis, label="NDWI", layer_name="NDWI (Water)")
148
+ return Map
149
+
150
+
101
151
  # Aliases for user convenience
102
152
  get_sentinel_bands = get_sentinel_bands_gee
103
153
  get_satellite_data = get_sentinel_bands_gee
hydroanomaly/visualize.py CHANGED
@@ -11,7 +11,7 @@ import pandas as pd
11
11
  import numpy as np
12
12
  from datetime import datetime
13
13
 
14
-
14
+ # ==============================================================================================
15
15
  def plot_timeseries(data: pd.DataFrame, title: str = "Time Series Data", save_file: str = None) -> None:
16
16
  """
17
17
  Create a simple time series plot.
@@ -26,10 +26,10 @@ def plot_timeseries(data: pd.DataFrame, title: str = "Time Series Data", save_fi
26
26
  """
27
27
 
28
28
  if data.empty:
29
- print("No data to plot")
29
+ print("No data to plot")
30
30
  return
31
31
 
32
- print(f"📊 Creating plot: {title}")
32
+ print(f"Creating plot: {title}")
33
33
 
34
34
  # Create figure
35
35
  plt.figure(figsize=(12, 6))
@@ -59,12 +59,12 @@ def plot_timeseries(data: pd.DataFrame, title: str = "Time Series Data", save_fi
59
59
  # Save if requested
60
60
  if save_file:
61
61
  plt.savefig(save_file, dpi=300, bbox_inches='tight')
62
- print(f"💾 Plot saved as {save_file}")
62
+ print(f"Plot saved as {save_file}")
63
63
 
64
64
  plt.show()
65
- print("Plot created successfully!")
66
-
65
+ print("Plot created successfully!")
67
66
 
67
+ # ==============================================================================================
68
68
  def plot_turbidity(turbidity_data: pd.DataFrame, save_file: str = None) -> None:
69
69
  """
70
70
  Create a turbidity-specific plot with appropriate formatting.
@@ -75,10 +75,10 @@ def plot_turbidity(turbidity_data: pd.DataFrame, save_file: str = None) -> None:
75
75
  """
76
76
 
77
77
  if turbidity_data.empty:
78
- print("No turbidity data to plot")
78
+ print("No turbidity data to plot")
79
79
  return
80
80
 
81
- print("🌫️ Creating turbidity plot")
81
+ print("Creating turbidity plot")
82
82
 
83
83
  plt.figure(figsize=(12, 6))
84
84
 
@@ -92,7 +92,7 @@ def plot_turbidity(turbidity_data: pd.DataFrame, save_file: str = None) -> None:
92
92
  plt.axhline(y=25, color='red', linestyle='--', alpha=0.7, label='High (25 NTU)')
93
93
 
94
94
  # Format plot
95
- plt.title('💧 Turbidity Time Series', fontsize=14, fontweight='bold', pad=20)
95
+ plt.title('Turbidity Time Series', fontsize=14, fontweight='bold', pad=20)
96
96
  plt.xlabel('Date', fontsize=12)
97
97
  plt.ylabel('Turbidity (NTU)', fontsize=12)
98
98
  plt.grid(True, alpha=0.3)
@@ -107,12 +107,12 @@ def plot_turbidity(turbidity_data: pd.DataFrame, save_file: str = None) -> None:
107
107
  # Save if requested
108
108
  if save_file:
109
109
  plt.savefig(save_file, dpi=300, bbox_inches='tight')
110
- print(f"💾 Turbidity plot saved as {save_file}")
110
+ print(f"Turbidity plot saved as {save_file}")
111
111
 
112
112
  plt.show()
113
- print("Turbidity plot created!")
114
-
113
+ print("Turbidity plot created!")
115
114
 
115
+ # ==============================================================================================
116
116
  def plot_sentinel(sentinel_data: pd.DataFrame, save_file: str = None) -> None:
117
117
  """
118
118
  Create a Sentinel satellite data plot.
@@ -123,10 +123,10 @@ def plot_sentinel(sentinel_data: pd.DataFrame, save_file: str = None) -> None:
123
123
  """
124
124
 
125
125
  if sentinel_data.empty:
126
- print("No Sentinel data to plot")
126
+ print("No Sentinel data to plot")
127
127
  return
128
128
 
129
- print("🛰️ Creating Sentinel bands plot")
129
+ print("Creating Sentinel bands plot")
130
130
 
131
131
  plt.figure(figsize=(12, 8))
132
132
 
@@ -146,7 +146,7 @@ def plot_sentinel(sentinel_data: pd.DataFrame, save_file: str = None) -> None:
146
146
  label=column, color=color, linewidth=2, marker='o', markersize=4)
147
147
 
148
148
  # Format plot
149
- plt.title('🛰️ Sentinel Satellite Data', fontsize=14, fontweight='bold', pad=20)
149
+ plt.title('Sentinel Satellite Data', fontsize=14, fontweight='bold', pad=20)
150
150
  plt.xlabel('Date', fontsize=12)
151
151
  plt.ylabel('Digital Number / Index Value', fontsize=12)
152
152
  plt.grid(True, alpha=0.3)
@@ -161,12 +161,12 @@ def plot_sentinel(sentinel_data: pd.DataFrame, save_file: str = None) -> None:
161
161
  # Save if requested
162
162
  if save_file:
163
163
  plt.savefig(save_file, dpi=300, bbox_inches='tight')
164
- print(f"💾 Sentinel plot saved as {save_file}")
164
+ print(f"Sentinel plot saved as {save_file}")
165
165
 
166
166
  plt.show()
167
- print("Sentinel plot created!")
168
-
167
+ print("Sentinel plot created!")
169
168
 
169
+ # ==============================================================================================
170
170
  def plot_comparison(data1: pd.DataFrame, data2: pd.DataFrame,
171
171
  label1: str = "Dataset 1", label2: str = "Dataset 2",
172
172
  title: str = "Data Comparison", save_file: str = None) -> None:
@@ -183,10 +183,10 @@ def plot_comparison(data1: pd.DataFrame, data2: pd.DataFrame,
183
183
  """
184
184
 
185
185
  if data1.empty and data2.empty:
186
- print("No data to plot")
186
+ print("No data to plot")
187
187
  return
188
188
 
189
- print(f"📊 Creating comparison plot: {title}")
189
+ print(f"Creating comparison plot: {title}")
190
190
 
191
191
  fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), sharex=True)
192
192
 
@@ -215,10 +215,10 @@ def plot_comparison(data1: pd.DataFrame, data2: pd.DataFrame,
215
215
  # Save if requested
216
216
  if save_file:
217
217
  plt.savefig(save_file, dpi=300, bbox_inches='tight')
218
- print(f"💾 Comparison plot saved as {save_file}")
218
+ print(f"Comparison plot saved as {save_file}")
219
219
 
220
220
  plt.show()
221
- print("Comparison plot created!")
221
+ print("Comparison plot created!")
222
222
 
223
223
 
224
224
  # Simple aliases
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hydroanomaly
3
- Version: 0.7.3
3
+ Version: 1.2.0
4
4
  Summary: A Python package for hydro anomaly detection with simple USGS data retrieval
5
5
  Author-email: Ehsan Kahrizi <ehsan.kahrizi@usu.edu>
6
6
  License: MIT License
@@ -28,7 +28,7 @@ License: MIT License
28
28
  Project-URL: Homepage, https://github.com/yourusername/hydroanomaly
29
29
  Project-URL: Bug Reports, https://github.com/yourusername/hydroanomaly/issues
30
30
  Project-URL: Source, https://github.com/yourusername/hydroanomaly
31
- Keywords: python,package,hydro,anomaly,detection
31
+ Keywords: python,package,hydrology,anomaly detection,remote sensing
32
32
  Classifier: Programming Language :: Python :: 3
33
33
  Classifier: Operating System :: OS Independent
34
34
  Requires-Python: >=3.6
@@ -0,0 +1,10 @@
1
+ hydroanomaly/__init__.py,sha256=IGPsOv-xfQGQpr_HxhrcZXjvVVkAfMBxh97e6S8Rfac,1664
2
+ hydroanomaly/ml.py,sha256=vqfnmGijjxGgtqJ2rzOmnMMrrVAVlYOPe1AnuX4EuG4,7018
3
+ hydroanomaly/sentinel_bands.py,sha256=Y6RAunVJDYLs13WemSSQNEu07GqmhR64fC2mLPxwh2k,5371
4
+ hydroanomaly/usgs_turbidity.py,sha256=k0cXRXpTe1YgjfR0Htw77SLD8hM--43jiEiJwx1vRg0,5664
5
+ hydroanomaly/visualize.py,sha256=d_Ou1sTr648TdAW-94NXwNbLPL4rvYVYb5pw4Xux3aE,7228
6
+ hydroanomaly-1.2.0.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
7
+ hydroanomaly-1.2.0.dist-info/METADATA,sha256=bh51WnUxEbk3azZY2IN9a9Io1Pav7knU90qMiiiTGDU,12981
8
+ hydroanomaly-1.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ hydroanomaly-1.2.0.dist-info/top_level.txt,sha256=t-5Lc-eTLlkxIhR_N1Cpp6_YZafKS3xLLk9D2CtbE7o,13
10
+ hydroanomaly-1.2.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- hydroanomaly/__init__.py,sha256=blfdU8RYTU_JTMgD_OUEx6YNEZW_Xi6Nn6vqgOdgbt8,1388
2
- hydroanomaly/sentinel_bands.py,sha256=LL_ChqpN6d_aJ0nq0YPWkL_vp9ns5bkSRp0UcKYe3o8,3637
3
- hydroanomaly/usgs_turbidity.py,sha256=k0cXRXpTe1YgjfR0Htw77SLD8hM--43jiEiJwx1vRg0,5664
4
- hydroanomaly/visualize.py,sha256=gkLgI3agx291jK5o08nYEbEpGpr6cD-6aAKn2Ha2Lqk,6937
5
- hydroanomaly-0.7.3.dist-info/licenses/LICENSE,sha256=OphKV48tcMv6ep-7j-8T6nycykPT0g8ZlMJ9zbGvdPs,1066
6
- hydroanomaly-0.7.3.dist-info/METADATA,sha256=yRZiu7Pea2bWa42QUcfkxTTtgBRRD2AOAPLsThwqXSw,12962
7
- hydroanomaly-0.7.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
8
- hydroanomaly-0.7.3.dist-info/top_level.txt,sha256=t-5Lc-eTLlkxIhR_N1Cpp6_YZafKS3xLLk9D2CtbE7o,13
9
- hydroanomaly-0.7.3.dist-info/RECORD,,