· last year · Jun 12, 2024, 05:20 AM
1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3# Filename: nasa_epic_metadata.py
4# Version: 1.0.0
5# Author: Jeoi Reqi
6
7"""
8Description:
9 - This script fetches image metadata from NASA's EPIC (Earth Polychromatic Imaging Camera) API.
10 - EPIC is a camera onboard the NOAA's DSCOVR (Deep Space Climate Observatory) spacecraft,
11 positioned at the Lagrange point 1 (L1), approximately one million miles from Earth.
12 - The camera captures images of the entire sunlit side of Earth, providing a unique perspective on our planet.
13
14 The EPIC images are available in four different types:
15 - Natural: True-color images that show Earth as it appears to the human eye.
16 - Enhanced: Images that are adjusted to enhance specific features of the atmosphere and surface.
17 - Aerosol: Images that highlight the distribution and concentration of aerosols in the atmosphere.
18 - Cloud: Images that provide detailed views of cloud formations and their dynamics.
19
20 Users can specify a date range to fetch metadata for, and the metadata will be saved in the specified folder.
21 The metadata includes information such as:
22 - Identifier: A unique identifier for each image.
23 - Image: The name of the image file.
24 - Date: The date and time when the image was captured.
25 - Centroid Coordinates: The latitude and longitude coordinates of the image's center.
26 - DSCOVR J2000 Position: The position of the DSCOVR spacecraft in J2000 coordinates (X, Y, Z).
27 - Lunar J2000 Position: The position of the Moon in J2000 coordinates (X, Y, Z).
28 - Sun J2000 Position: The position of the Sun in J2000 coordinates (X, Y, Z).
29
30Requirements:
31 - Python 3.x
32 - Required modules:
33 - requests
34 - datetime
35 - time
36 - dateutil
37 - colorama
38
39Functions:
40 - make_api_request:
41 Makes an API request to the specified URL.
42 - check_rate_limit:
43 Checks and manages the API call rate limit.
44 - write_metadata_to_file:
45 Writes the fetched metadata to a file.
46
47Classes:
48 - NASAEPICManager:
49 A class to manage NASA EPIC API requests and rate limits.
50
51Usage:
52 1. Ensure there's a file named 'nasa_api.txt' in the current working directory containing your NASA API Key(s).
53 The API key file contents must be separated by a new line if you need to use multiple keys.
54 2. Run the script.
55 3. Follow the on-screen prompts to select an option and specify the date range.
56 4. The metadata will be fetched and saved in a text file.
57
58Additional Notes:
59 - You need to obtain a NASA API key to use this script.
60 - You can get it by registering on the [NASA API portal](https://api.nasa.gov/index.html?apply-for-an-api-key).
61 - After obtaining your API key, create a text file named 'nasa_api.txt' in the same directory as this script.
62 - Save your API key(s) in 'nasa_api.txt', with each key on a new line if you have multiple keys. Example:
63 - EXAMPLE:
64 - YOUR_FIRST_API_KEY
65 - YOUR_SECOND_API_KEY
66 - To download the image files, you can go to URL:
67 - Save the script as 'nasa_epic_images.py' in your current working directory.
68 - Follow the detailed prompting from the program
69"""
70
71# Get Essential Imports
72import time
73import requests
74from datetime import datetime, timedelta
75from dateutil import parser as date_parser
76from colorama import init, Fore, Style
77
78# Initialize colorama
79init(autoreset=True)
80
81class NASAEPICManager:
82 """
83 A class to manage NASA EPIC API requests and rate limits.
84
85 Attributes:
86 api_keys (list): List of API keys obtained from a file.
87 current_key_index (int): Index of the current API key being used.
88 hourly_limit (int): Maximum number of API calls allowed per hour.
89 calls_count (int): Number of API calls made in the current hour.
90 last_reset_time (float): Timestamp of the last reset time for the hourly limit.
91 """
92
93 def __init__(self, api_keys_file):
94 """
95 Initializes the NASAEPICManager with API keys and rate limit parameters.
96
97 Args:
98 api_keys_file (str): Path to the file containing API keys.
99 """
100 with open(api_keys_file, 'r', encoding='utf-8') as file:
101 self.api_keys = file.read().splitlines()
102 self.current_key_index = 0
103 self.hourly_limit = 999
104 self.calls_count = 0
105 self.last_reset_time = time.time()
106
107 def make_api_request(self, url, retries=3):
108 """
109 Makes an API request to the specified URL.
110
111 Args:
112 url (str): The URL to make the API request to.
113 retries (int): Number of retries in case of failure. Default is 3.
114
115 Returns:
116 dict: JSON response from the API if successful, otherwise None.
117 """
118 self.check_rate_limit()
119 current_api_key = self.api_keys[self.current_key_index]
120 params = {'api_key': current_api_key}
121
122 for _ in range(retries):
123 try:
124 response = requests.get(url, params=params)
125 response.raise_for_status()
126 self.calls_count += 1
127 print(f"\nAPI Key: {current_api_key}, Remaining Requests: {response.headers.get('X-RateLimit-Remaining', 0)}\n")
128 return response.json()
129 except requests.exceptions.RequestException:
130 print(f"\nError: No image metadata found for {url}! Retrying...\n")
131 time.sleep(3) # Wait for 3 seconds before retrying
132
133 print(f"Failed to fetch metadata after retries for {url}.")
134 return None
135
136 def check_rate_limit(self):
137 """
138 Checks and manages the API call rate limit.
139 Resets counters if more than one hour has passed or switches to the next API key if the hourly limit is reached.
140 """
141 current_time = time.time()
142 time_since_reset = current_time - self.last_reset_time
143
144 if time_since_reset >= 3600:
145 # Reset counters if more than one hour has passed
146 self.calls_count = 0
147 self.last_reset_time = current_time
148
149 if self.calls_count >= self.hourly_limit:
150 # Switch to the next API key if the hourly limit is reached
151 self.current_key_index = (self.current_key_index + 1) % len(self.api_keys)
152 self.calls_count = 0
153
154def write_metadata_to_file(metadata_list, filename_str):
155 """
156 Writes metadata to a file.
157
158 Args:
159 metadata_list (list): A list of dictionaries containing metadata information.
160 filename_str (str): The name of the file to write the metadata to.
161
162 Notes:
163 - Each entry in the metadata list should be a dictionary containing metadata information.
164 - The function writes metadata in a formatted way to the specified file.
165 - Metadata includes identifier, image name, date, centroid coordinates, and positions of DSCOVr, Lunar, and Sun.
166
167 """
168 with open(filename_str, 'w', encoding='UTF-8') as file:
169 for entry in metadata_list:
170 file.write(f"Identifier: #{entry['identifier']}, \tImage: {entry['image']}, \tDate: {entry['date']}\n\n")
171 file.write(f"Centroid Coordinates: \tLatitude {entry['coords']['centroid_coordinates']['lat']}, \tLongitude {entry['coords']['centroid_coordinates']['lon']}\n\n")
172 file.write("\t\t\t\t[X]\t\t\t[Y]\t\t\t[Z]\n\n")
173 file.write(f"DSCOVr J2000 Position: \tx {entry['coords']['dscovr_j2000_position']['x']}, \ty {entry['coords']['dscovr_j2000_position']['y']}, \tz {entry['coords']['dscovr_j2000_position']['z']}\n")
174 file.write(f"Lunar J2000 Position: \tx {entry['coords']['lunar_j2000_position']['x']}, \ty {entry['coords']['lunar_j2000_position']['y']}, \tz {entry['coords']['lunar_j2000_position']['z']}\n")
175 file.write(f"Sun J2000 Position: \tx {entry['coords']['sun_j2000_position']['x']}, \ty {entry['coords']['sun_j2000_position']['y']}, \tz {entry['coords']['sun_j2000_position']['z']}\n\n")
176 file.write("-" * 50 + "\n\n")
177
178# Define the header as a list of strings for easier manipulation
179header_lines = [
180 " ███ ██ █████ ███████ █████ ",
181 " ████ ██ ██ ██ ██ ██ ██ ",
182 " ██ ██ ██ ███████ ███████ ███████ ",
183 " ██ ██ ██ ██ ██ ██ ██ ██ ",
184 " ██ ████ ██ ██ ███████ ██ ██ ",
185 "███████ █████ ██████ ███████ █████ ██████ ██████ ███████ ",
186 " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
187 " ██ █████ █████ ███████ ███████ █████ █████ █████ █████ ███████ ███████ ",
188 " ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ",
189 " ██ █████ ██████ ███████ █████ ██████ ██████ ███████ ",
190 " ███████ ██████ ██ ██████ ",
191 " ██ ██ ██ ██ ██ ",
192 " █████ ██████ ██ ██ ",
193 " ██ ██ ██ ██ ",
194 " ███████ ██ ██ ██████ (METADATA) "
195]
196
197# Apply colors to the specified lines
198colored_header = []
199for i, line in enumerate(header_lines):
200 if 0 <= i < 5: # NASA
201 colored_header.append(Fore.RED + line + Style.RESET_ALL)
202 elif 5 <= i < 10: # 78-65-83-65 (ASCII CODE)
203 colored_header.append(Fore.WHITE + line + Style.RESET_ALL)
204 elif 10 <= i < 15: # EPIC
205 colored_header.append(Fore.BLUE + line + Style.RESET_ALL)
206 else:
207 colored_header.append(line)
208
209# Print the colored header
210for line in colored_header:
211 print(line)
212
213if __name__ == "__main__":
214 nasa_manager = NASAEPICManager('nasa_api.txt')
215
216 while True: # Loop for the menu
217 # Display options for the user
218 print("_" * 83)
219 print("\nMetadata Options:\n")
220 print("1. Natural")
221 print("2. Enhanced")
222 print("3. Cloud")
223 print("4. Aerosol")
224 print("5. Exit") # Option to exit
225
226 # Get user's choice
227 option = input("\nSelect an option (1-5): ")
228
229 if option == '5': # Exit option
230 print("\nExiting Program... GoodBye!\n")
231 break # Exit the loop and end the program
232
233 # Map the user's choice to the corresponding URL component
234 option_mapping = {
235 '1': 'natural',
236 '2': 'enhanced',
237 '3': 'cloud',
238 '4': 'aerosol'
239 }
240
241 # Check if the user's choice is valid
242 if option in option_mapping:
243 selected_option = option_mapping[option]
244
245 # Get current date to compare with user input
246 current_date = datetime.now().date()
247
248 # Check if start date is before the epoch date (June 13, 2015)
249 epoch_dates = {
250 'natural': '2015-06-13',
251 'enhanced': '2015-08-07',
252 'aerosol': '2020-09-04',
253 'cloud': '2023-01-03'
254 }
255
256 # Get start date from the user
257 print("_" * 83)
258 print("\n\t\t\t:: Start Date Format: (YYYY-MM-DD) ::\n")
259 start_date_str = input(f"Enter a 'Start Date' after {epoch_dates[selected_option]}: ")
260 print("_" * 83)
261 start_date = date_parser.parse(start_date_str).date()
262
263 # Check if start date is before the epoch date (June 13, 2015)
264 epoch_date = datetime(2015, 6, 13).date()
265 if start_date < epoch_date:
266 print(Fore.RED + "\nError: Start date cannot be before June 13, 2015!\n" + Style.RESET_ALL)
267 continue # Restart the loop for a new input
268
269 # Check if start date is in the future
270 if start_date > current_date:
271 print(Fore.RED + "\nError: Start date cannot be in the future!\n" + Style.RESET_ALL)
272 continue # Restart the loop for a new input
273
274 # Check if the user wants to log a single day or a range of dates
275 end_date_str = input("\nEnter the end date (YYYY-MM-DD) or press [ENTER] to log a single day: ")
276 if end_date_str:
277 end_date = date_parser.parse(end_date_str).date()
278
279 # Check if end date is in the future
280 if end_date > current_date:
281 print(Fore.RED + "\nError: End date cannot be in the future!\n" + Style.RESET_ALL)
282 continue # Restart the loop for a new input
283
284 # Check if end date is before the epoch date (June 13, 2015)
285 if end_date < epoch_date:
286 print(Fore.RED + "\nError: End date cannot be before June 13, 2015!\n" + Style.RESET_ALL)
287 continue # Restart the loop for a new input
288 else:
289 end_date = start_date
290
291 # Specify the filename for the formatted metadata
292 filename = f'{start_date_str}_{end_date_str}_{selected_option}.txt'
293
294 # Iterate through the date range and log data for each day
295 current_date = start_date
296 all_metadata = [] # List to store metadata for all dates
297
298 while current_date <= end_date:
299 date_str = current_date.strftime('%Y-%m-%d')
300 metadata_url = f"https://api.nasa.gov/EPIC/api/{selected_option}/date/{date_str}?api_key=TEMPL4ujtA3LyN0qbFh4imFv7gxDfUG9WoU0eWOu"
301 metadata = nasa_manager.make_api_request(metadata_url)
302
303 if metadata:
304 print(f"Metadata for {date_str}:")
305 print(metadata)
306 all_metadata.extend(metadata) # Append metadata to the list
307
308 current_date += timedelta(days=1)
309
310 # Create the file if it doesn't exist
311 if all_metadata:
312 write_metadata_to_file(all_metadata, filename)
313 print(f"\nFormatted metadata has been written to {filename}\n")
314 else:
315 print("\nNo valid entries found in the specified date range.\n")
316 else:
317 print("\nInvalid option! Please select a valid option (1-5).\n")
318