solarviewer 1.0.2__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.
- solar_radio_image_viewer/__init__.py +12 -0
- solar_radio_image_viewer/assets/add_tab_default.png +0 -0
- solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/browse.png +0 -0
- solar_radio_image_viewer/assets/browse_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default.png +0 -0
- solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
- solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
- solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
- solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
- solar_radio_image_viewer/assets/profile.png +0 -0
- solar_radio_image_viewer/assets/profile_light.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
- solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
- solar_radio_image_viewer/assets/reset.png +0 -0
- solar_radio_image_viewer/assets/reset_light.png +0 -0
- solar_radio_image_viewer/assets/ruler.png +0 -0
- solar_radio_image_viewer/assets/ruler_light.png +0 -0
- solar_radio_image_viewer/assets/search.png +0 -0
- solar_radio_image_viewer/assets/search_light.png +0 -0
- solar_radio_image_viewer/assets/settings.png +0 -0
- solar_radio_image_viewer/assets/settings_light.png +0 -0
- solar_radio_image_viewer/assets/splash.fits +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
- solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_in.png +0 -0
- solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
- solar_radio_image_viewer/assets/zoom_out.png +0 -0
- solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
- solar_radio_image_viewer/create_video.py +1345 -0
- solar_radio_image_viewer/dialogs.py +2665 -0
- solar_radio_image_viewer/from_simpl/__init__.py +184 -0
- solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
- solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
- solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
- solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
- solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
- solar_radio_image_viewer/from_simpl/utils.py +984 -0
- solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
- solar_radio_image_viewer/helioprojective.py +1916 -0
- solar_radio_image_viewer/helioprojective_viewer.py +817 -0
- solar_radio_image_viewer/helioviewer_browser.py +1514 -0
- solar_radio_image_viewer/main.py +148 -0
- solar_radio_image_viewer/move_phasecenter.py +1269 -0
- solar_radio_image_viewer/napari_viewer.py +368 -0
- solar_radio_image_viewer/noaa_events/__init__.py +32 -0
- solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
- solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
- solar_radio_image_viewer/norms.py +293 -0
- solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
- solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
- solar_radio_image_viewer/searchable_combobox.py +220 -0
- solar_radio_image_viewer/solar_context/__init__.py +41 -0
- solar_radio_image_viewer/solar_context/active_regions.py +371 -0
- solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
- solar_radio_image_viewer/solar_context/context_images.py +297 -0
- solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
- solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
- solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
- solar_radio_image_viewer/styles.py +643 -0
- solar_radio_image_viewer/utils/__init__.py +32 -0
- solar_radio_image_viewer/utils/rate_limiter.py +255 -0
- solar_radio_image_viewer/utils.py +952 -0
- solar_radio_image_viewer/video_dialog.py +2629 -0
- solar_radio_image_viewer/video_utils.py +656 -0
- solar_radio_image_viewer/viewer.py +11174 -0
- solarviewer-1.0.2.dist-info/METADATA +343 -0
- solarviewer-1.0.2.dist-info/RECORD +82 -0
- solarviewer-1.0.2.dist-info/WHEEL +5 -0
- solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
- solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
- solarviewer-1.0.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Rate Limiting and Caching utilities for HTTP requests.
|
|
4
|
+
|
|
5
|
+
Prevents IP blocking by enforcing delays between requests and caching responses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from datetime import datetime, timedelta
|
|
14
|
+
from typing import Optional, Dict, Any
|
|
15
|
+
import requests
|
|
16
|
+
from threading import Lock
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class RateLimitedSession:
|
|
20
|
+
"""
|
|
21
|
+
HTTP session with automatic rate limiting per domain.
|
|
22
|
+
|
|
23
|
+
Enforces minimum delay between consecutive requests to the same domain
|
|
24
|
+
to prevent triggering anti-bot protection.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(self, min_delay: float = 0.5):
|
|
28
|
+
"""
|
|
29
|
+
Initialize rate-limited session.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
min_delay: Minimum seconds between requests to same domain (default 0.5)
|
|
33
|
+
"""
|
|
34
|
+
self.session = requests.Session()
|
|
35
|
+
self.min_delay = min_delay
|
|
36
|
+
self.last_request_time: Dict[str, float] = {}
|
|
37
|
+
self.lock = Lock()
|
|
38
|
+
|
|
39
|
+
# Set a browser-like User-Agent to avoid simple bot detection
|
|
40
|
+
self.session.headers.update({
|
|
41
|
+
'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
def _get_domain(self, url: str) -> str:
|
|
45
|
+
"""Extract domain from URL."""
|
|
46
|
+
from urllib.parse import urlparse
|
|
47
|
+
return urlparse(url).netloc
|
|
48
|
+
|
|
49
|
+
def _enforce_rate_limit(self, domain: str):
|
|
50
|
+
"""Enforce minimum delay since last request to this domain."""
|
|
51
|
+
with self.lock:
|
|
52
|
+
if domain in self.last_request_time:
|
|
53
|
+
elapsed = time.time() - self.last_request_time[domain]
|
|
54
|
+
if elapsed < self.min_delay:
|
|
55
|
+
sleep_time = self.min_delay - elapsed
|
|
56
|
+
time.sleep(sleep_time)
|
|
57
|
+
|
|
58
|
+
self.last_request_time[domain] = time.time()
|
|
59
|
+
|
|
60
|
+
def get(self, url: str, **kwargs) -> requests.Response:
|
|
61
|
+
"""
|
|
62
|
+
Perform GET request with automatic rate limiting.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
url: URL to fetch
|
|
66
|
+
**kwargs: Additional arguments passed to requests.get()
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
Response object
|
|
70
|
+
|
|
71
|
+
Raises:
|
|
72
|
+
requests.exceptions.RequestException: On HTTP errors
|
|
73
|
+
"""
|
|
74
|
+
domain = self._get_domain(url)
|
|
75
|
+
self._enforce_rate_limit(domain)
|
|
76
|
+
|
|
77
|
+
# Set default timeout if not provided
|
|
78
|
+
if 'timeout' not in kwargs:
|
|
79
|
+
kwargs['timeout'] = 30
|
|
80
|
+
|
|
81
|
+
response = self.session.get(url, **kwargs)
|
|
82
|
+
|
|
83
|
+
# Handle rate limiting responses
|
|
84
|
+
if response.status_code == 429:
|
|
85
|
+
raise requests.exceptions.HTTPError(
|
|
86
|
+
"Rate limit exceeded. Please wait before making more requests.",
|
|
87
|
+
response=response
|
|
88
|
+
)
|
|
89
|
+
elif response.status_code == 403:
|
|
90
|
+
# Could be IP block or other forbidden access
|
|
91
|
+
raise requests.exceptions.HTTPError(
|
|
92
|
+
"Access forbidden. Your IP may be temporarily blocked. Please try again later.",
|
|
93
|
+
response=response
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
response.raise_for_status()
|
|
97
|
+
return response
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class CachedSession(RateLimitedSession):
|
|
101
|
+
"""
|
|
102
|
+
Rate-limited session with file-based response caching.
|
|
103
|
+
|
|
104
|
+
Caches successful responses to disk with configurable TTL.
|
|
105
|
+
Prevents redundant requests and speeds up repeated fetches.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
def __init__(self, min_delay: float = 0.5, cache_ttl_hours: int = 24, cache_dir: Optional[Path] = None):
|
|
109
|
+
"""
|
|
110
|
+
Initialize cached session.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
min_delay: Minimum seconds between requests to same domain
|
|
114
|
+
cache_ttl_hours: How long to keep cached responses (default 24 hours)
|
|
115
|
+
cache_dir: Directory for cache storage (default ~/.cache/solarviewer/)
|
|
116
|
+
"""
|
|
117
|
+
super().__init__(min_delay)
|
|
118
|
+
self.cache_ttl = timedelta(hours=cache_ttl_hours)
|
|
119
|
+
|
|
120
|
+
if cache_dir is None:
|
|
121
|
+
cache_dir = Path.home() / '.cache' / 'solarviewer'
|
|
122
|
+
|
|
123
|
+
self.cache_dir = Path(cache_dir)
|
|
124
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
125
|
+
|
|
126
|
+
def _get_cache_key(self, url: str) -> str:
|
|
127
|
+
"""Generate cache key from URL."""
|
|
128
|
+
return hashlib.md5(url.encode()).hexdigest()
|
|
129
|
+
|
|
130
|
+
def _get_cache_path(self, cache_key: str) -> Path:
|
|
131
|
+
"""Get path to cache file for given key."""
|
|
132
|
+
return self.cache_dir / f"{cache_key}.json"
|
|
133
|
+
|
|
134
|
+
def _is_cache_valid(self, cache_path: Path) -> bool:
|
|
135
|
+
"""Check if cached response is still valid (not expired)."""
|
|
136
|
+
if not cache_path.exists():
|
|
137
|
+
return False
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
with open(cache_path, 'r') as f:
|
|
141
|
+
cached = json.load(f)
|
|
142
|
+
|
|
143
|
+
cached_time = datetime.fromisoformat(cached['timestamp'])
|
|
144
|
+
age = datetime.now() - cached_time
|
|
145
|
+
|
|
146
|
+
return age < self.cache_ttl
|
|
147
|
+
except (json.JSONDecodeError, KeyError, ValueError):
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
def _load_from_cache(self, cache_path: Path) -> Optional[Dict[str, Any]]:
|
|
151
|
+
"""Load cached response if valid."""
|
|
152
|
+
if not self._is_cache_valid(cache_path):
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
try:
|
|
156
|
+
with open(cache_path, 'r') as f:
|
|
157
|
+
return json.load(f)
|
|
158
|
+
except (json.JSONDecodeError, IOError):
|
|
159
|
+
return None
|
|
160
|
+
|
|
161
|
+
def _save_to_cache(self, cache_path: Path, url: str, response: requests.Response):
|
|
162
|
+
"""Save response to cache."""
|
|
163
|
+
try:
|
|
164
|
+
cache_data = {
|
|
165
|
+
'timestamp': datetime.now().isoformat(),
|
|
166
|
+
'url': url,
|
|
167
|
+
'status_code': response.status_code,
|
|
168
|
+
'content': response.text,
|
|
169
|
+
'headers': dict(response.headers),
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
with open(cache_path, 'w') as f:
|
|
173
|
+
json.dump(cache_data, f)
|
|
174
|
+
except (IOError, TypeError):
|
|
175
|
+
# Silently fail if caching doesn't work
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
def get(self, url: str, use_cache: bool = True, **kwargs) -> requests.Response:
|
|
179
|
+
"""
|
|
180
|
+
Perform GET request with caching.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
url: URL to fetch
|
|
184
|
+
use_cache: Whether to use cached response if available
|
|
185
|
+
**kwargs: Additional arguments passed to requests.get()
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Response object (either from cache or fresh request)
|
|
189
|
+
"""
|
|
190
|
+
cache_key = self._get_cache_key(url)
|
|
191
|
+
cache_path = self._get_cache_path(cache_key)
|
|
192
|
+
|
|
193
|
+
# Try to load from cache
|
|
194
|
+
if use_cache:
|
|
195
|
+
cached = self._load_from_cache(cache_path)
|
|
196
|
+
if cached:
|
|
197
|
+
# Create a mock response object from cached data
|
|
198
|
+
response = requests.Response()
|
|
199
|
+
response.status_code = cached['status_code']
|
|
200
|
+
response._content = cached['content'].encode()
|
|
201
|
+
response.headers.update(cached['headers'])
|
|
202
|
+
response.url = url
|
|
203
|
+
response.from_cache = True # Custom attribute to indicate cache hit
|
|
204
|
+
return response
|
|
205
|
+
|
|
206
|
+
# Not in cache or cache disabled, fetch fresh
|
|
207
|
+
response = super().get(url, **kwargs)
|
|
208
|
+
|
|
209
|
+
# Cache successful responses
|
|
210
|
+
if response.status_code == 200:
|
|
211
|
+
self._save_to_cache(cache_path, url, response)
|
|
212
|
+
|
|
213
|
+
response.from_cache = False
|
|
214
|
+
return response
|
|
215
|
+
|
|
216
|
+
def clear_cache(self):
|
|
217
|
+
"""Remove all cached responses."""
|
|
218
|
+
for cache_file in self.cache_dir.glob("*.json"):
|
|
219
|
+
try:
|
|
220
|
+
cache_file.unlink()
|
|
221
|
+
except OSError:
|
|
222
|
+
pass
|
|
223
|
+
|
|
224
|
+
def cleanup_expired_cache(self):
|
|
225
|
+
"""Remove expired cache entries."""
|
|
226
|
+
for cache_file in self.cache_dir.glob("*.json"):
|
|
227
|
+
if not self._is_cache_valid(cache_file):
|
|
228
|
+
try:
|
|
229
|
+
cache_file.unlink()
|
|
230
|
+
except OSError:
|
|
231
|
+
pass
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
# Global cached session instance for reuse across modules
|
|
235
|
+
_global_session: Optional[CachedSession] = None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def get_global_session(min_delay: float = 0.5, cache_ttl_hours: int = 24) -> CachedSession:
|
|
239
|
+
"""
|
|
240
|
+
Get or create global cached session instance.
|
|
241
|
+
|
|
242
|
+
This allows multiple modules to share the same session and benefit from
|
|
243
|
+
shared rate limiting and caching.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
min_delay: Minimum seconds between requests
|
|
247
|
+
cache_ttl_hours: Cache validity period
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
Global CachedSession instance
|
|
251
|
+
"""
|
|
252
|
+
global _global_session
|
|
253
|
+
if _global_session is None:
|
|
254
|
+
_global_session = CachedSession(min_delay=min_delay, cache_ttl_hours=cache_ttl_hours)
|
|
255
|
+
return _global_session
|