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.
Files changed (82) hide show
  1. solar_radio_image_viewer/__init__.py +12 -0
  2. solar_radio_image_viewer/assets/add_tab_default.png +0 -0
  3. solar_radio_image_viewer/assets/add_tab_default_light.png +0 -0
  4. solar_radio_image_viewer/assets/add_tab_hover.png +0 -0
  5. solar_radio_image_viewer/assets/add_tab_hover_light.png +0 -0
  6. solar_radio_image_viewer/assets/browse.png +0 -0
  7. solar_radio_image_viewer/assets/browse_light.png +0 -0
  8. solar_radio_image_viewer/assets/close_tab_default.png +0 -0
  9. solar_radio_image_viewer/assets/close_tab_default_light.png +0 -0
  10. solar_radio_image_viewer/assets/close_tab_hover.png +0 -0
  11. solar_radio_image_viewer/assets/close_tab_hover_light.png +0 -0
  12. solar_radio_image_viewer/assets/ellipse_selection.png +0 -0
  13. solar_radio_image_viewer/assets/ellipse_selection_light.png +0 -0
  14. solar_radio_image_viewer/assets/icons8-ellipse-90.png +0 -0
  15. solar_radio_image_viewer/assets/icons8-ellipse-90_light.png +0 -0
  16. solar_radio_image_viewer/assets/icons8-info-90.png +0 -0
  17. solar_radio_image_viewer/assets/icons8-info-90_light.png +0 -0
  18. solar_radio_image_viewer/assets/profile.png +0 -0
  19. solar_radio_image_viewer/assets/profile_light.png +0 -0
  20. solar_radio_image_viewer/assets/rectangle_selection.png +0 -0
  21. solar_radio_image_viewer/assets/rectangle_selection_light.png +0 -0
  22. solar_radio_image_viewer/assets/reset.png +0 -0
  23. solar_radio_image_viewer/assets/reset_light.png +0 -0
  24. solar_radio_image_viewer/assets/ruler.png +0 -0
  25. solar_radio_image_viewer/assets/ruler_light.png +0 -0
  26. solar_radio_image_viewer/assets/search.png +0 -0
  27. solar_radio_image_viewer/assets/search_light.png +0 -0
  28. solar_radio_image_viewer/assets/settings.png +0 -0
  29. solar_radio_image_viewer/assets/settings_light.png +0 -0
  30. solar_radio_image_viewer/assets/splash.fits +0 -0
  31. solar_radio_image_viewer/assets/zoom_60arcmin.png +0 -0
  32. solar_radio_image_viewer/assets/zoom_60arcmin_light.png +0 -0
  33. solar_radio_image_viewer/assets/zoom_in.png +0 -0
  34. solar_radio_image_viewer/assets/zoom_in_light.png +0 -0
  35. solar_radio_image_viewer/assets/zoom_out.png +0 -0
  36. solar_radio_image_viewer/assets/zoom_out_light.png +0 -0
  37. solar_radio_image_viewer/create_video.py +1345 -0
  38. solar_radio_image_viewer/dialogs.py +2665 -0
  39. solar_radio_image_viewer/from_simpl/__init__.py +184 -0
  40. solar_radio_image_viewer/from_simpl/caltable_visualizer.py +1001 -0
  41. solar_radio_image_viewer/from_simpl/dynamic_spectra_dialog.py +332 -0
  42. solar_radio_image_viewer/from_simpl/make_dynamic_spectra.py +351 -0
  43. solar_radio_image_viewer/from_simpl/pipeline_logger_gui.py +1232 -0
  44. solar_radio_image_viewer/from_simpl/simpl_theme.py +352 -0
  45. solar_radio_image_viewer/from_simpl/utils.py +984 -0
  46. solar_radio_image_viewer/from_simpl/view_dynamic_spectra_GUI.py +1975 -0
  47. solar_radio_image_viewer/helioprojective.py +1916 -0
  48. solar_radio_image_viewer/helioprojective_viewer.py +817 -0
  49. solar_radio_image_viewer/helioviewer_browser.py +1514 -0
  50. solar_radio_image_viewer/main.py +148 -0
  51. solar_radio_image_viewer/move_phasecenter.py +1269 -0
  52. solar_radio_image_viewer/napari_viewer.py +368 -0
  53. solar_radio_image_viewer/noaa_events/__init__.py +32 -0
  54. solar_radio_image_viewer/noaa_events/noaa_events.py +430 -0
  55. solar_radio_image_viewer/noaa_events/noaa_events_gui.py +1922 -0
  56. solar_radio_image_viewer/norms.py +293 -0
  57. solar_radio_image_viewer/radio_data_downloader/__init__.py +25 -0
  58. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader.py +756 -0
  59. solar_radio_image_viewer/radio_data_downloader/radio_data_downloader_gui.py +528 -0
  60. solar_radio_image_viewer/searchable_combobox.py +220 -0
  61. solar_radio_image_viewer/solar_context/__init__.py +41 -0
  62. solar_radio_image_viewer/solar_context/active_regions.py +371 -0
  63. solar_radio_image_viewer/solar_context/cme_alerts.py +234 -0
  64. solar_radio_image_viewer/solar_context/context_images.py +297 -0
  65. solar_radio_image_viewer/solar_context/realtime_data.py +528 -0
  66. solar_radio_image_viewer/solar_data_downloader/__init__.py +35 -0
  67. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader.py +1667 -0
  68. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_cli.py +901 -0
  69. solar_radio_image_viewer/solar_data_downloader/solar_data_downloader_gui.py +1210 -0
  70. solar_radio_image_viewer/styles.py +643 -0
  71. solar_radio_image_viewer/utils/__init__.py +32 -0
  72. solar_radio_image_viewer/utils/rate_limiter.py +255 -0
  73. solar_radio_image_viewer/utils.py +952 -0
  74. solar_radio_image_viewer/video_dialog.py +2629 -0
  75. solar_radio_image_viewer/video_utils.py +656 -0
  76. solar_radio_image_viewer/viewer.py +11174 -0
  77. solarviewer-1.0.2.dist-info/METADATA +343 -0
  78. solarviewer-1.0.2.dist-info/RECORD +82 -0
  79. solarviewer-1.0.2.dist-info/WHEEL +5 -0
  80. solarviewer-1.0.2.dist-info/entry_points.txt +8 -0
  81. solarviewer-1.0.2.dist-info/licenses/LICENSE +21 -0
  82. 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