Spaces:
Sleeping
Sleeping
Delete unified_data_manager.py
Browse files- unified_data_manager.py +0 -413
unified_data_manager.py
DELETED
@@ -1,413 +0,0 @@
|
|
1 |
-
"""
|
2 |
-
Unified Data Manager for GlycoAI - FIXED VERSION
|
3 |
-
Restores the original working API calls that were working before
|
4 |
-
"""
|
5 |
-
|
6 |
-
import logging
|
7 |
-
from typing import Dict, Any, Optional, Tuple
|
8 |
-
import pandas as pd
|
9 |
-
from datetime import datetime, timedelta
|
10 |
-
from dataclasses import asdict
|
11 |
-
|
12 |
-
from apifunctions import (
|
13 |
-
DexcomAPI,
|
14 |
-
GlucoseAnalyzer,
|
15 |
-
DEMO_USERS,
|
16 |
-
DemoUser
|
17 |
-
)
|
18 |
-
|
19 |
-
logger = logging.getLogger(__name__)
|
20 |
-
|
21 |
-
class UnifiedDataManager:
|
22 |
-
"""
|
23 |
-
FIXED: Unified data manager that calls the API exactly as it was working before
|
24 |
-
"""
|
25 |
-
|
26 |
-
def __init__(self):
|
27 |
-
self.dexcom_api = DexcomAPI()
|
28 |
-
self.analyzer = GlucoseAnalyzer()
|
29 |
-
|
30 |
-
logger.info(f"UnifiedDataManager initialized - RESTORED to working version")
|
31 |
-
|
32 |
-
# Single source of truth for all data
|
33 |
-
self.current_user: Optional[DemoUser] = None
|
34 |
-
self.raw_glucose_data: Optional[list] = None
|
35 |
-
self.processed_glucose_data: Optional[pd.DataFrame] = None
|
36 |
-
self.calculated_stats: Optional[Dict] = None
|
37 |
-
self.identified_patterns: Optional[Dict] = None
|
38 |
-
|
39 |
-
# Metadata
|
40 |
-
self.data_loaded_at: Optional[datetime] = None
|
41 |
-
self.data_source: str = "none" # "dexcom_api", "mock", or "none"
|
42 |
-
|
43 |
-
def load_user_data(self, user_key: str, force_reload: bool = False) -> Dict[str, Any]:
|
44 |
-
"""
|
45 |
-
FIXED: Load glucose data using the ORIGINAL WORKING method
|
46 |
-
"""
|
47 |
-
|
48 |
-
# Check if we already have data for this user and it's recent
|
49 |
-
if (not force_reload and
|
50 |
-
self.current_user and
|
51 |
-
self.current_user == DEMO_USERS.get(user_key) and
|
52 |
-
self.data_loaded_at and
|
53 |
-
(datetime.now() - self.data_loaded_at).seconds < 300): # 5 minutes cache
|
54 |
-
|
55 |
-
logger.info(f"Using cached data for {user_key}")
|
56 |
-
return self._build_success_response()
|
57 |
-
|
58 |
-
try:
|
59 |
-
if user_key not in DEMO_USERS:
|
60 |
-
return {
|
61 |
-
"success": False,
|
62 |
-
"message": f"❌ Invalid user key '{user_key}'. Available: {', '.join(DEMO_USERS.keys())}"
|
63 |
-
}
|
64 |
-
|
65 |
-
logger.info(f"Loading data for user: {user_key}")
|
66 |
-
|
67 |
-
# Set current user
|
68 |
-
self.current_user = DEMO_USERS[user_key]
|
69 |
-
|
70 |
-
# Call API EXACTLY as it was working before
|
71 |
-
try:
|
72 |
-
logger.info(f"Attempting Dexcom API authentication for {user_key}")
|
73 |
-
|
74 |
-
# ORIGINAL WORKING METHOD: Use the simulate_demo_login exactly as before
|
75 |
-
access_token = self.dexcom_api.simulate_demo_login(user_key)
|
76 |
-
logger.info(f"Dexcom authentication result: {bool(access_token)}")
|
77 |
-
|
78 |
-
if access_token:
|
79 |
-
# ORIGINAL WORKING METHOD: Get data with 14-day range
|
80 |
-
end_date = datetime.now()
|
81 |
-
start_date = end_date - timedelta(days=14)
|
82 |
-
|
83 |
-
# Call get_egv_data EXACTLY as it was working before
|
84 |
-
self.raw_glucose_data = self.dexcom_api.get_egv_data(
|
85 |
-
start_date.isoformat(),
|
86 |
-
end_date.isoformat()
|
87 |
-
)
|
88 |
-
|
89 |
-
if self.raw_glucose_data and len(self.raw_glucose_data) > 0:
|
90 |
-
self.data_source = "dexcom_api"
|
91 |
-
logger.info(f"✅ Successfully loaded {len(self.raw_glucose_data)} readings from Dexcom API")
|
92 |
-
else:
|
93 |
-
logger.warning("Dexcom API returned empty data - falling back to mock data")
|
94 |
-
raise Exception("Empty data from Dexcom API")
|
95 |
-
else:
|
96 |
-
logger.warning("Failed to get access token - falling back to mock data")
|
97 |
-
raise Exception("Authentication failed")
|
98 |
-
|
99 |
-
except Exception as api_error:
|
100 |
-
logger.warning(f"Dexcom API failed ({str(api_error)}) - using mock data fallback")
|
101 |
-
self.raw_glucose_data = self._generate_realistic_mock_data(user_key)
|
102 |
-
self.data_source = "mock"
|
103 |
-
|
104 |
-
# Process the raw data (same processing for everyone)
|
105 |
-
self.processed_glucose_data = self.analyzer.process_egv_data(self.raw_glucose_data)
|
106 |
-
|
107 |
-
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
108 |
-
return {
|
109 |
-
"success": False,
|
110 |
-
"message": "❌ Failed to process glucose data"
|
111 |
-
}
|
112 |
-
|
113 |
-
# Calculate statistics (single source of truth)
|
114 |
-
self.calculated_stats = self._calculate_unified_stats()
|
115 |
-
|
116 |
-
# Identify patterns
|
117 |
-
self.identified_patterns = self.analyzer.identify_patterns(self.processed_glucose_data)
|
118 |
-
|
119 |
-
# Mark when data was loaded
|
120 |
-
self.data_loaded_at = datetime.now()
|
121 |
-
|
122 |
-
logger.info(f"Successfully loaded and processed data for {self.current_user.name}")
|
123 |
-
logger.info(f"Data source: {self.data_source}, Readings: {len(self.processed_glucose_data)}")
|
124 |
-
logger.info(f"TIR: {self.calculated_stats.get('time_in_range_70_180', 0):.1f}%")
|
125 |
-
|
126 |
-
return self._build_success_response()
|
127 |
-
|
128 |
-
except Exception as e:
|
129 |
-
logger.error(f"Failed to load user data: {e}")
|
130 |
-
return {
|
131 |
-
"success": False,
|
132 |
-
"message": f"❌ Failed to load user data: {str(e)}"
|
133 |
-
}
|
134 |
-
|
135 |
-
def get_stats_for_ui(self) -> Dict[str, Any]:
|
136 |
-
"""Get statistics formatted for the UI display"""
|
137 |
-
if not self.calculated_stats:
|
138 |
-
return {}
|
139 |
-
|
140 |
-
return {
|
141 |
-
**self.calculated_stats,
|
142 |
-
"data_source": self.data_source,
|
143 |
-
"loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
144 |
-
"user_name": self.current_user.name if self.current_user else None
|
145 |
-
}
|
146 |
-
|
147 |
-
def get_context_for_agent(self) -> Dict[str, Any]:
|
148 |
-
"""Get context formatted for the AI agent"""
|
149 |
-
if not self.current_user or not self.calculated_stats:
|
150 |
-
return {"error": "No user data loaded"}
|
151 |
-
|
152 |
-
# Build agent context with the SAME data as UI
|
153 |
-
context = {
|
154 |
-
"user": {
|
155 |
-
"name": self.current_user.name,
|
156 |
-
"age": self.current_user.age,
|
157 |
-
"diabetes_type": self.current_user.diabetes_type,
|
158 |
-
"device_type": self.current_user.device_type,
|
159 |
-
"years_with_diabetes": self.current_user.years_with_diabetes,
|
160 |
-
"typical_pattern": getattr(self.current_user, 'typical_glucose_pattern', 'normal')
|
161 |
-
},
|
162 |
-
"statistics": self._safe_convert_for_json(self.calculated_stats),
|
163 |
-
"patterns": self._safe_convert_for_json(self.identified_patterns),
|
164 |
-
"data_points": len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0,
|
165 |
-
"recent_readings": self._get_recent_readings_for_agent(),
|
166 |
-
"data_metadata": {
|
167 |
-
"source": self.data_source,
|
168 |
-
"loaded_at": self.data_loaded_at.isoformat() if self.data_loaded_at else None,
|
169 |
-
"data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None
|
170 |
-
}
|
171 |
-
}
|
172 |
-
|
173 |
-
return context
|
174 |
-
|
175 |
-
def get_chart_data(self) -> Optional[pd.DataFrame]:
|
176 |
-
"""Get processed data for chart display"""
|
177 |
-
return self.processed_glucose_data
|
178 |
-
|
179 |
-
def _calculate_unified_stats(self) -> Dict[str, Any]:
|
180 |
-
"""Calculate statistics using a single, consistent method"""
|
181 |
-
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
182 |
-
return {"error": "No data available"}
|
183 |
-
|
184 |
-
try:
|
185 |
-
# Get glucose values
|
186 |
-
glucose_values = self.processed_glucose_data['value'].dropna()
|
187 |
-
|
188 |
-
if len(glucose_values) == 0:
|
189 |
-
return {"error": "No valid glucose values"}
|
190 |
-
|
191 |
-
# Convert to numpy array for consistent calculations
|
192 |
-
import numpy as np
|
193 |
-
values = np.array(glucose_values.tolist(), dtype=float)
|
194 |
-
|
195 |
-
# Calculate basic statistics
|
196 |
-
avg_glucose = float(np.mean(values))
|
197 |
-
min_glucose = float(np.min(values))
|
198 |
-
max_glucose = float(np.max(values))
|
199 |
-
std_glucose = float(np.std(values))
|
200 |
-
total_readings = int(len(values))
|
201 |
-
|
202 |
-
# Calculate time in ranges - CONSISTENT METHOD
|
203 |
-
in_range_mask = (values >= 70) & (values <= 180)
|
204 |
-
below_range_mask = values < 70
|
205 |
-
above_range_mask = values > 180
|
206 |
-
|
207 |
-
in_range_count = int(np.sum(in_range_mask))
|
208 |
-
below_range_count = int(np.sum(below_range_mask))
|
209 |
-
above_range_count = int(np.sum(above_range_mask))
|
210 |
-
|
211 |
-
# Calculate percentages
|
212 |
-
time_in_range = (in_range_count / total_readings) * 100 if total_readings > 0 else 0
|
213 |
-
time_below_70 = (below_range_count / total_readings) * 100 if total_readings > 0 else 0
|
214 |
-
time_above_180 = (above_range_count / total_readings) * 100 if total_readings > 0 else 0
|
215 |
-
|
216 |
-
# Calculate additional metrics
|
217 |
-
gmi = 3.31 + (0.02392 * avg_glucose) # Glucose Management Indicator
|
218 |
-
cv = (std_glucose / avg_glucose) * 100 if avg_glucose > 0 else 0 # Coefficient of Variation
|
219 |
-
|
220 |
-
stats = {
|
221 |
-
"average_glucose": avg_glucose,
|
222 |
-
"min_glucose": min_glucose,
|
223 |
-
"max_glucose": max_glucose,
|
224 |
-
"std_glucose": std_glucose,
|
225 |
-
"time_in_range_70_180": time_in_range,
|
226 |
-
"time_below_70": time_below_70,
|
227 |
-
"time_above_180": time_above_180,
|
228 |
-
"total_readings": total_readings,
|
229 |
-
"gmi": gmi,
|
230 |
-
"cv": cv,
|
231 |
-
"in_range_count": in_range_count,
|
232 |
-
"below_range_count": below_range_count,
|
233 |
-
"above_range_count": above_range_count
|
234 |
-
}
|
235 |
-
|
236 |
-
# Log for debugging
|
237 |
-
logger.info(f"Calculated stats - TIR: {time_in_range:.1f}%, Total: {total_readings}, In range: {in_range_count}")
|
238 |
-
|
239 |
-
return stats
|
240 |
-
|
241 |
-
except Exception as e:
|
242 |
-
logger.error(f"Error calculating unified stats: {e}")
|
243 |
-
return {"error": f"Statistics calculation failed: {str(e)}"}
|
244 |
-
|
245 |
-
def _generate_realistic_mock_data(self, user_key: str) -> list:
|
246 |
-
"""Generate consistent mock data for demo users"""
|
247 |
-
from mistral_chat import GlucoseDataGenerator
|
248 |
-
|
249 |
-
# Map users to patterns
|
250 |
-
pattern_map = {
|
251 |
-
"sarah_g7": "normal",
|
252 |
-
"marcus_one": "dawn_phenomenon",
|
253 |
-
"jennifer_g6": "normal",
|
254 |
-
"robert_receiver": "dawn_phenomenon"
|
255 |
-
}
|
256 |
-
|
257 |
-
user_pattern = pattern_map.get(user_key, "normal")
|
258 |
-
|
259 |
-
# Generate 14 days of data
|
260 |
-
mock_data = GlucoseDataGenerator.create_realistic_pattern(days=14, user_type=user_pattern)
|
261 |
-
|
262 |
-
logger.info(f"Generated {len(mock_data)} mock data points for {user_key} with pattern {user_pattern}")
|
263 |
-
|
264 |
-
return mock_data
|
265 |
-
|
266 |
-
def _get_recent_readings_for_agent(self, count: int = 5) -> list:
|
267 |
-
"""Get recent readings formatted for agent context"""
|
268 |
-
if self.processed_glucose_data is None or self.processed_glucose_data.empty:
|
269 |
-
return []
|
270 |
-
|
271 |
-
try:
|
272 |
-
recent_df = self.processed_glucose_data.tail(count)
|
273 |
-
readings = []
|
274 |
-
|
275 |
-
for _, row in recent_df.iterrows():
|
276 |
-
display_time = row.get('displayTime') or row.get('systemTime')
|
277 |
-
glucose_value = row.get('value')
|
278 |
-
trend_value = row.get('trend', 'flat')
|
279 |
-
|
280 |
-
if pd.notna(display_time):
|
281 |
-
if isinstance(display_time, str):
|
282 |
-
time_str = display_time
|
283 |
-
else:
|
284 |
-
time_str = pd.to_datetime(display_time).isoformat()
|
285 |
-
else:
|
286 |
-
time_str = datetime.now().isoformat()
|
287 |
-
|
288 |
-
if pd.notna(glucose_value):
|
289 |
-
glucose_clean = self._safe_convert_for_json(glucose_value)
|
290 |
-
else:
|
291 |
-
glucose_clean = None
|
292 |
-
|
293 |
-
trend_clean = str(trend_value) if pd.notna(trend_value) else 'flat'
|
294 |
-
|
295 |
-
readings.append({
|
296 |
-
"time": time_str,
|
297 |
-
"glucose": glucose_clean,
|
298 |
-
"trend": trend_clean
|
299 |
-
})
|
300 |
-
|
301 |
-
return readings
|
302 |
-
|
303 |
-
except Exception as e:
|
304 |
-
logger.error(f"Error getting recent readings: {e}")
|
305 |
-
return []
|
306 |
-
|
307 |
-
def _safe_convert_for_json(self, obj):
|
308 |
-
"""Safely convert objects for JSON serialization"""
|
309 |
-
import numpy as np
|
310 |
-
|
311 |
-
if obj is None:
|
312 |
-
return None
|
313 |
-
elif isinstance(obj, (np.integer, np.int64, np.int32)):
|
314 |
-
return int(obj)
|
315 |
-
elif isinstance(obj, (np.floating, np.float64, np.float32)):
|
316 |
-
if np.isnan(obj):
|
317 |
-
return None
|
318 |
-
return float(obj)
|
319 |
-
elif isinstance(obj, dict):
|
320 |
-
return {key: self._safe_convert_for_json(value) for key, value in obj.items()}
|
321 |
-
elif isinstance(obj, list):
|
322 |
-
return [self._safe_convert_for_json(item) for item in obj]
|
323 |
-
elif isinstance(obj, pd.Timestamp):
|
324 |
-
return obj.isoformat()
|
325 |
-
else:
|
326 |
-
return obj
|
327 |
-
|
328 |
-
def _build_success_response(self) -> Dict[str, Any]:
|
329 |
-
"""Build a consistent success response"""
|
330 |
-
data_points = len(self.processed_glucose_data) if self.processed_glucose_data is not None else 0
|
331 |
-
avg_glucose = self.calculated_stats.get('average_glucose', 0)
|
332 |
-
time_in_range = self.calculated_stats.get('time_in_range_70_180', 0)
|
333 |
-
|
334 |
-
return {
|
335 |
-
"success": True,
|
336 |
-
"message": f"✅ Successfully loaded data for {self.current_user.name}",
|
337 |
-
"user": asdict(self.current_user),
|
338 |
-
"data_points": data_points,
|
339 |
-
"stats": self.calculated_stats,
|
340 |
-
"data_source": self.data_source,
|
341 |
-
"summary": f"📊 {data_points} readings | Avg: {avg_glucose:.1f} mg/dL | TIR: {time_in_range:.1f}% | Source: {self.data_source}"
|
342 |
-
}
|
343 |
-
|
344 |
-
def validate_data_consistency(self) -> Dict[str, Any]:
|
345 |
-
"""Validate that all components are using consistent data"""
|
346 |
-
if not self.calculated_stats:
|
347 |
-
return {"valid": False, "message": "No data loaded"}
|
348 |
-
|
349 |
-
validation = {
|
350 |
-
"valid": True,
|
351 |
-
"data_source": self.data_source,
|
352 |
-
"data_age_minutes": int((datetime.now() - self.data_loaded_at).total_seconds() / 60) if self.data_loaded_at else None,
|
353 |
-
"total_readings": self.calculated_stats.get('total_readings', 0),
|
354 |
-
"time_in_range": self.calculated_stats.get('time_in_range_70_180', 0),
|
355 |
-
"average_glucose": self.calculated_stats.get('average_glucose', 0),
|
356 |
-
"user": self.current_user.name if self.current_user else None
|
357 |
-
}
|
358 |
-
|
359 |
-
logger.info(f"Data consistency check: {validation}")
|
360 |
-
|
361 |
-
return validation
|
362 |
-
|
363 |
-
# ADDITIONAL: Debug function to test the API connection as it was working before
|
364 |
-
def test_original_api_method():
|
365 |
-
"""Test the API exactly as it was working before unified data manager"""
|
366 |
-
from apifunctions import DexcomAPI, DEMO_USERS
|
367 |
-
|
368 |
-
print("🔍 Testing API exactly as it was working before...")
|
369 |
-
|
370 |
-
api = DexcomAPI()
|
371 |
-
|
372 |
-
# Test with sarah_g7 as it was working before
|
373 |
-
user_key = "sarah_g7"
|
374 |
-
user = DEMO_USERS[user_key]
|
375 |
-
|
376 |
-
print(f"Testing with {user.name} ({user.username})")
|
377 |
-
|
378 |
-
try:
|
379 |
-
# Call simulate_demo_login exactly as before
|
380 |
-
access_token = api.simulate_demo_login(user_key)
|
381 |
-
print(f"✅ Authentication: {bool(access_token)}")
|
382 |
-
|
383 |
-
if access_token:
|
384 |
-
# Call get_egv_data exactly as before
|
385 |
-
end_date = datetime.now()
|
386 |
-
start_date = end_date - timedelta(days=14)
|
387 |
-
|
388 |
-
egv_data = api.get_egv_data(
|
389 |
-
start_date.isoformat(),
|
390 |
-
end_date.isoformat()
|
391 |
-
)
|
392 |
-
|
393 |
-
print(f"✅ EGV Data: {len(egv_data)} readings")
|
394 |
-
|
395 |
-
if egv_data:
|
396 |
-
print(f"✅ SUCCESS! API is working as before")
|
397 |
-
sample = egv_data[0] if egv_data else {}
|
398 |
-
print(f"Sample reading: {sample}")
|
399 |
-
return True
|
400 |
-
else:
|
401 |
-
print("⚠️ API authenticated but returned no data")
|
402 |
-
return False
|
403 |
-
else:
|
404 |
-
print("❌ Authentication failed")
|
405 |
-
return False
|
406 |
-
|
407 |
-
except Exception as e:
|
408 |
-
print(f"❌ Error: {e}")
|
409 |
-
return False
|
410 |
-
|
411 |
-
if __name__ == "__main__":
|
412 |
-
# Test the original API method
|
413 |
-
test_original_api_method()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|