izisat 0.1.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.
@@ -0,0 +1,554 @@
1
+ import requests
2
+ from tqdm import tqdm
3
+ from loguru import logger
4
+ from datetime import datetime
5
+ from izisat.misc.utils import Utils
6
+ from izisat.misc.files import Files
7
+ from izisat.misc.dates import Dates
8
+
9
+
10
+ class Connections:
11
+ def __init__(self):
12
+ pass
13
+
14
+
15
+ def access_token(self, username: str, password: str) -> str:
16
+ """
17
+ Obtain an access token for accessing the Sentinel API.
18
+
19
+ Parameters:
20
+ -----------
21
+ username: str
22
+ The username for authentication.
23
+ password: str
24
+ The password for authentication.
25
+
26
+ Returns:
27
+ --------
28
+ access_token, refresh_token, dt_now: Tuple[str, str, datetime]
29
+ A tuple containing the access token, refresh token, and the current datetime when the tokens were obtained.
30
+ """
31
+ logger.info("Trying to establish connection with Copernicus API.")
32
+ data = {
33
+ "client_id": "cdse-public",
34
+ "username": username,
35
+ "password": password,
36
+ "grant_type": "password",
37
+
38
+ }
39
+ try:
40
+ r = requests.post("https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token",
41
+ data=data,
42
+ )
43
+ dt_now = datetime.now()
44
+ r.raise_for_status()
45
+ except Exception as e:
46
+ raise Exception(
47
+ f"Keycloak token creation failed. Response from the server was: {r.json()}"
48
+ )
49
+
50
+ logger.success("Connection estalished")
51
+ access_token = r.json()["access_token"]
52
+ refresh_token = r.json()["refresh_token"]
53
+ return access_token, refresh_token, dt_now
54
+
55
+ def refresh_access_token(self, refresh_token: str):
56
+ """
57
+ Refresh the access token using the provided refresh token.
58
+
59
+ Parameters:
60
+ -----------
61
+ refresh_token : str
62
+ The refresh token obtained during the initial authentication process.
63
+
64
+ Returns:
65
+ --------
66
+ r.json()["access_token"], dt_now: Tuple[str, datetime]
67
+ A tuple containing the new access token and the current datetime when the refresh was performed.
68
+ """
69
+ data = {
70
+ "client_id": "cdse-public",
71
+ "refresh_token": refresh_token,
72
+ "grant_type": "refresh_token",
73
+ }
74
+
75
+ try:
76
+ r = requests.post(
77
+ "https://identity.dataspace.copernicus.eu/auth/realms/CDSE/protocol/openid-connect/token",
78
+ data=data,
79
+ )
80
+ dt_now = datetime.now()
81
+ r.raise_for_status()
82
+ except Exception as e:
83
+ raise Exception(
84
+ f"Access token refresh failed. Reponse from the server was: {r.json()}"
85
+ )
86
+
87
+ return r.json()["access_token"], dt_now
88
+
89
+ def retrieve_sent_prod_from_query(self, params):
90
+ """
91
+ Retrieve Sentinel products from a specified query.
92
+
93
+ Parameters:
94
+ -----------
95
+ params: str
96
+ The query parameters to be appended to the URL.
97
+
98
+ Returns:
99
+ ---------
100
+ response.json()['value']: list
101
+ List of Sentinel products, each represented as a dictionary.
102
+ None
103
+ if no products are found.
104
+ """
105
+ logger.info("Searching Sentinel products...")
106
+ url = "https://catalogue.dataspace.copernicus.eu/odata/v1/Products"
107
+ url_complet = url + params
108
+ response = requests.get(url_complet)
109
+ if response.status_code == 200:
110
+ if not response.json()['value']:
111
+ logger.warning("There is no Sentinel data available with this parameters.")
112
+ return None
113
+
114
+ for product in response.json()['value']:
115
+ if product['Online']:
116
+ product_name = response.json()['value']
117
+ logger.success(f"Found successfully {len(product_name)} products!")
118
+ return response.json()['value']
119
+ else:
120
+ # If status code is not 200, raise an exception to trigger the retry mechanism
121
+ response.raise_for_status()
122
+
123
+
124
+ def retrieve_bands_for_resolution(self, session, headers, response, resolution, band_list, url_index):
125
+ """
126
+ Retrieve links for bands at a specific resolution for Sentinel 2 type L2A.
127
+
128
+ Parameters:
129
+ -----------
130
+ session: requests.Session
131
+ The active session for making HTTP requests.
132
+ headers: dict
133
+ The headers to include in the HTTP request.
134
+ response: requests.Response
135
+ The response object containing information about the product.
136
+ resolution: str
137
+ The resolution category ('10m', '20m', '60m').
138
+ band_list: list
139
+ List of bands to retrieve links for.
140
+ url_index: int
141
+ The index in the response JSON where the URL for bands is located.
142
+
143
+ Returns:
144
+ --------
145
+ url_bands_resolution: list
146
+ List of lists containing band names and their corresponding URLs.
147
+
148
+ Example:
149
+ --------
150
+ session = requests.Session()
151
+ headers = {'Authorization': 'Bearer YOUR_ACCESS_TOKEN'}
152
+ response = session.get("https://example.com/api/products/1234")
153
+ resolution = "10m"
154
+ band_list = ["band1", "band2"]
155
+ url_index = 0
156
+
157
+ bands_links = retrieve_bands_for_resolution(session, headers, response, resolution, band_list, url_index)
158
+
159
+ """
160
+ url_resolution = response.json()["result"][url_index]["Nodes"]["uri"]
161
+ response_resolution = session.get(url_resolution, headers=headers, stream=True)
162
+ url_bands_resolution = []
163
+
164
+ for band in band_list:
165
+ url_bands = response_resolution.json()["result"]
166
+ for product in url_bands:
167
+ # Ensure the band name contains the resolution string (e.g., "10m", "20m")
168
+ # and the specific band (e.g., "B02")
169
+ if f"_{resolution}" in product["Name"] and band in product["Name"]:
170
+ item_to_append = [product["Name"], product["Nodes"]["uri"]]
171
+ url_bands_resolution.append(item_to_append)
172
+
173
+ logger.success(f"Links successfully retrieved for bands {band_list} in resolution {resolution}")
174
+ return url_bands_resolution
175
+
176
+
177
+
178
+
179
+ def get_links_l1c_product(self, access_token, product_id, bands):
180
+ """
181
+ Retrieve links for bands in a Level-1C (L1C) product.
182
+
183
+ Parameters:
184
+ -----------
185
+ access_token: str
186
+ The access token for authentication.
187
+ product_id: str
188
+ The ID of the Sentinel product.
189
+ bands: list
190
+ List of bands to retrieve links for.
191
+
192
+ Returns:
193
+ --------
194
+ url_l1c_bands: list
195
+ List of lists containing band names and their corresponding URLs.
196
+
197
+ Example:
198
+ --------
199
+ access_token = "YOUR_ACCESS_TOKEN"
200
+ product_id = "1234567890"
201
+ bands = ["band1", "band2"]
202
+
203
+ l1c_bands_links = get_links_l1c_product(access_token, product_id, bands)
204
+ """
205
+ url_l1c_bands = []
206
+ headers = {'Authorization': f'Bearer {access_token}'}
207
+ url = f"https://zipper.dataspace.copernicus.eu/odata/v1/Products({product_id})/Nodes"
208
+
209
+ try:
210
+ logger.info("Starting connection to search for band links...")
211
+ session = requests.Session()
212
+ session.headers.update(headers)
213
+ response = session.get(url, headers=headers, stream=True)
214
+
215
+ #### Trying to download specific bands #########################
216
+ url_1 = response.json()['result'][0]["Nodes"]['uri']
217
+ response_1 = session.get(url_1, headers=headers, stream=True)
218
+ url_2 = response_1.json()['result']
219
+ granule_index = next((index for index, d in enumerate(url_2) if d.get('Id') == 'GRANULE'), None)
220
+ if granule_index is not None:
221
+ url_2 = url_2[granule_index]["Nodes"]["uri"]
222
+ response_2 = session.get(url_2, headers=headers, stream=True)
223
+ url_3 = response_2.json()["result"][0]["Nodes"]["uri"]
224
+ response_3 = session.get(url_3, headers=headers, stream=True)
225
+ url_4 = response_3.json()["result"][1]["Nodes"]["uri"]
226
+ response_4 = session.get(url_4, headers=headers, stream=True)
227
+ products = response_4.json()["result"]
228
+ for product in products:
229
+ for band in bands:
230
+ if band in product["Name"]:
231
+ itens_to_append = [product["Name"], product["Nodes"]["uri"]]
232
+ url_l1c_bands.append(itens_to_append)
233
+ break
234
+ logger.success(f"Links for bands {bands} successfully retrieved")
235
+
236
+ return url_l1c_bands
237
+
238
+ except Exception as e:
239
+ raise requests.exceptions.RequestException(e)
240
+
241
+
242
+
243
+
244
+ def get_links_l2a_product(self, access_token, product_id, bands):
245
+ """
246
+ Retrieve links for bands in a Level-2A (L2A) product.
247
+
248
+ Parameters:
249
+ -----------
250
+ access_token: str
251
+ The access token for authentication.
252
+ product_id: str
253
+ The ID of the Sentinel product.
254
+ bands: dict
255
+ Dictionary mapping resolutions to lists of bands.
256
+
257
+ Returns:
258
+ --------
259
+ resolution_links: dict
260
+ Dictionary containing resolution-wise lists of band names and their corresponding URLs.
261
+
262
+ Example:
263
+ --------
264
+ access_token = "YOUR_ACCESS_TOKEN"
265
+ product_id = "1234567890"
266
+ bands = {"10m": ["band1", "band2"], "20m": ["band3", "band4"]}
267
+
268
+ l2a_links = get_links_l2a_product(access_token, product_id, bands)
269
+ """
270
+ resolution_links = {}
271
+ headers = {'Authorization': f'Bearer {access_token}'}
272
+ url = f"https://zipper.dataspace.copernicus.eu/odata/v1/Products({product_id})/Nodes"
273
+
274
+ try:
275
+ logger.info("Starting connection to search for band links...")
276
+ session = requests.Session()
277
+ session.headers.update(headers)
278
+ response = session.get(url, headers=headers, stream=True)
279
+
280
+ #### Trying to download specific bands #########################
281
+ url_1 = response.json()['result'][0]["Nodes"]['uri']
282
+ response_1 = session.get(url_1, headers=headers, stream=True)
283
+ url_2 = next(item['Nodes']['uri'] for item in response_1.json().get('result', []) if item.get('Id') == 'GRANULE')
284
+ response_2 = session.get(url_2, headers=headers, stream=True)
285
+ url_3 = response_2.json()["result"][0]["Nodes"]["uri"]
286
+ response_3 = session.get(url_3, headers=headers, stream=True)
287
+ url_4 = next(item['Nodes']['uri'] for item in response_3.json().get('result', []) if item.get('Id') == 'IMG_DATA')
288
+ response_4 = session.get(url_4, headers=headers, stream=True)
289
+
290
+ # Mapping resolution to url_index
291
+ resolution_to_url_index = {'10m': 0,
292
+ '20m': 1,
293
+ '60m': 2
294
+ }
295
+
296
+ for resolution, band_list in bands.items():
297
+ logger.info(f"Searching links for resolution {resolution}...")
298
+ url_index = resolution_to_url_index.get(resolution, -1)
299
+
300
+ if url_index != -1:
301
+ resolution_links[resolution] = self.retrieve_bands_for_resolution(
302
+ session, headers, response_4, resolution, band_list, url_index)
303
+ else:
304
+ # Handle the case when resolution is not found in the mapping
305
+ logger.warning(f"Resolution {resolution} not mapped to a valid url_index.")
306
+
307
+ except Exception as e:
308
+ raise requests.exceptions.RequestException(e)
309
+
310
+ return resolution_links
311
+
312
+
313
+
314
+ def retrieve_bands_links(self, access_token, products_info, bands_dict):
315
+ """
316
+ Retrieve links for bands in Sentinel products.
317
+
318
+ Parameters:
319
+ -----------
320
+ access_token: str
321
+ The access token for authentication.
322
+ products_info: list
323
+ List of lists containing information about Sentinel products.
324
+ bands_dict: dict
325
+ Dictionary mapping product types to dictionaries of resolutions and their corresponding bands.
326
+
327
+ Returns:
328
+ --------
329
+ all_links: dict
330
+ Dictionary containing links for bands in Sentinel products.
331
+ The structure is {product_name: {product_type: {resolution: [(band_name, band_link), ...], ...}, ...}, ...}.
332
+
333
+ Example:
334
+ --------
335
+ access_token = "YOUR_ACCESS_TOKEN"
336
+ products_info = [
337
+ ["product_id_1", "product_name_1", "path_1", "date_1", "tile_1", "platform_1", "L1C"],
338
+ ["product_id_2", "product_name_2", "path_2", "date_2", "tile_2", "platform_2", "L2A"],
339
+ ]
340
+ bands_dict = {
341
+ "L1C": ["band1", "band2"],
342
+ "L2A": {"10m": ["band3", "band4"], "20m": ["band5", "band6"]},
343
+ }
344
+
345
+ links = retrieve_bands_links(access_token, products_info, bands_dict)
346
+ """
347
+ all_links = {}
348
+
349
+ for product_info in products_info:
350
+ product_id = product_info[0]
351
+ product_type = product_info[6]
352
+ product_name = product_info[1]
353
+ product_name = product_name.replace(".SAFE", "")
354
+ logger.info(f"Getting bands links for: {product_name}")
355
+
356
+ if product_type == "L1C":
357
+ logger.info(f"{product_name} is type {product_type}...")
358
+ l1c_bands = bands_dict["L1C"]
359
+ links_l1c = self.get_links_l1c_product(access_token, product_id, l1c_bands)
360
+ all_links.setdefault(product_name, {}).setdefault("L1C", links_l1c)
361
+ elif product_type == "L2A":
362
+ logger.info(f"{product_name} is type {product_type}...")
363
+ l2a_bands = bands_dict["L2A"]
364
+ links_l2a = self.get_links_l2a_product(access_token, product_id, l2a_bands)
365
+
366
+ all_links.setdefault(product_name, {}).setdefault("L2A", links_l2a)
367
+
368
+ return all_links
369
+
370
+
371
+
372
+ def download(self, access_token, url, output_path, product_name, name):
373
+ """
374
+ Download data from a specified URL using an access token.
375
+
376
+ Parameters:
377
+ -----------
378
+ access_token: str
379
+ The access token obtained from the Sentinel API for authentication.
380
+ url: str
381
+ The URL of the data to be downloaded.
382
+ output_path: str
383
+ The local path where the downloaded data will be saved.
384
+ product_name: str
385
+ The name of the Sentinel product associated with the data.
386
+ name: str
387
+ The name or identifier for the downloaded data.
388
+
389
+ Returns:
390
+ --------
391
+ None
392
+ """
393
+ headers = {'Authorization': f'Bearer {access_token}'}
394
+
395
+ try:
396
+ logger.info("Starting connection to download data...")
397
+ session = requests.Session()
398
+ session.headers.update(headers)
399
+ response = session.get(url, headers=headers, stream=True)
400
+
401
+ if response.status_code == 200:
402
+ logger.success("Connection estabilished!")
403
+ logger.info(f"Starting download of: {name}")
404
+ # Get the file size from the 'Content-Length' header of the response
405
+ total_size = int(response.headers.get('Content-Length', 0))
406
+
407
+ # Open a file for writing in binary mode
408
+ with open(output_path, "wb") as file, tqdm(desc="Downloading", total=total_size, unit="B", unit_scale=True, unit_divisor=1024, ) as bar:
409
+ # Iterate over the content in chunks (chunk_size=8192 bytes)
410
+ for chunk in response.iter_content(chunk_size=8192):
411
+ # Check if the chunk is not empty
412
+ if chunk:
413
+ # Write the chunk to the file
414
+ file.write(chunk)
415
+ # Update the progress bar with the size of the current chunk
416
+ bar.update(len(chunk))
417
+ logger.success(f"Data {name} successfuly downloaded for product {product_name}")
418
+ else:
419
+ # If the response status code is not 200, raise an exception
420
+ response.raise_for_status()
421
+ except Exception as e:
422
+ raise requests.exceptions.RequestException(e)
423
+
424
+ def download_bands(self, access_token, products_info, bands_links, base_dir, dt_access_token, refresh_token, tile):
425
+ """
426
+ Download bands for Sentinel products based on the provided links.
427
+
428
+ Parameters:
429
+ -----------
430
+ access_token: str
431
+ The access token obtained from the Sentinel API for authentication.
432
+ products_info: List[List]
433
+ List of lists containing information about Sentinel products.
434
+ bands_links: Dict[str, Any]
435
+ Dictionary containing links to bands for each Sentinel product.
436
+ base_dir: str
437
+ The base directory where the downloaded bands will be saved.
438
+
439
+ Returns:
440
+ --------
441
+ None
442
+ """
443
+
444
+ utils = Utils()
445
+ files = Files()
446
+ dates = Dates()
447
+ for product_info in products_info:
448
+ product_id = product_info[0]
449
+ product_type = product_info[6]
450
+ product_name = product_info[1]
451
+ product_date = product_info[3]
452
+ product_platform = product_info[5]
453
+ product_tile = product_info[4]
454
+ product_name = product_name.replace(".SAFE", "")
455
+ logger.info(f"Starting download process for product {product_name}")
456
+
457
+
458
+ if tile == product_tile:
459
+ if product_name in bands_links:
460
+ if "MSIL1C" in product_name:
461
+ product = bands_links[product_name]
462
+ key = next(iter(product))
463
+ # Access the nested lists
464
+ products_link_lists = product[key]
465
+ for nested_list in products_link_lists:
466
+ name, link = nested_list
467
+ link_mod = utils.modify_string(link)
468
+ filepath = files.check_file_exist(base_dir, product_platform, product_date, product_tile, product_type, name)
469
+ logger.info(f"Starting download process for band {name}")
470
+ if filepath[0] == True:
471
+ logger.warning(f"Band {name} Already downloaded")
472
+ else:
473
+ dt_now = datetime.now()
474
+ expired = dates.is_token_expired(dt_access_token, dt_now)
475
+
476
+ if expired:
477
+ access_token, dt_access_token = self.refresh_access_token(refresh_token)
478
+ logger.info("Data has not been downloaded. Starting data downloaded...")
479
+ self.download(access_token, link_mod, filepath[1], product_name, name)
480
+
481
+ elif "MSIL2A" in product_name:
482
+ product = bands_links[product_name]
483
+ key = next(iter(product))
484
+ # Access the nested lists
485
+ products_link_lists = product[key]
486
+ for resolution, bands_links_list in products_link_lists.items():
487
+ for band in bands_links_list:
488
+ name = band[0]
489
+ link = band[1]
490
+ link_mod = utils.modify_string(link)
491
+ filepath = files.check_file_exist(base_dir, product_platform, product_date, product_tile, product_type, name, resolution)
492
+
493
+ logger.info(f"Starting download process for band {name}")
494
+ if filepath[0] == True:
495
+ logger.warning(f"Band {name} Already downloaded")
496
+ else:
497
+ logger.info("Data has not been downloaded. Starting data downloaded...")
498
+ dt_now = datetime.now()
499
+ expired = dates.is_token_expired(dt_access_token, dt_now)
500
+
501
+ if expired:
502
+ access_token, dt_access_token = self.refresh_access_token(refresh_token)
503
+ self.download(access_token, link_mod, filepath[1], product_name, name)
504
+
505
+ elif tile == None:
506
+ if product_name in bands_links:
507
+ if "MSIL1C" in product_name:
508
+ product = bands_links[product_name]
509
+ key = next(iter(product))
510
+ # Access the nested lists
511
+ products_link_lists = product[key]
512
+ for nested_list in products_link_lists:
513
+ name, link = nested_list
514
+ link_mod = utils.modify_string(link)
515
+ filepath = files.check_file_exist(base_dir, product_platform, product_date, product_tile, product_type, name)
516
+ logger.info(f"Starting download process for band {name}")
517
+ if filepath[0] == True:
518
+ logger.warning(f"Band {name} Already downloaded")
519
+ else:
520
+ dt_now = datetime.now()
521
+ expired = dates.is_token_expired(dt_access_token, dt_now)
522
+
523
+ if expired:
524
+ access_token, dt_access_token = self.refresh_access_token(refresh_token)
525
+ logger.info("Data has not been downloaded. Starting data downloaded...")
526
+ self.download(access_token, link_mod, filepath[1], product_name, name)
527
+
528
+ elif "MSIL2A" in product_name:
529
+ product = bands_links[product_name]
530
+ key = next(iter(product))
531
+ # Access the nested lists
532
+ products_link_lists = product[key]
533
+ for resolution, bands_links_list in products_link_lists.items():
534
+ for band in bands_links_list:
535
+ name = band[0]
536
+ link = band[1]
537
+ link_mod = utils.modify_string(link)
538
+ filepath = files.check_file_exist(base_dir, product_platform, product_date, product_tile, product_type, name, resolution)
539
+
540
+ logger.info(f"Starting download process for band {name}")
541
+ if filepath[0] == True:
542
+ logger.warning(f"Band {name} Already downloaded")
543
+ else:
544
+ logger.info("Data has not been downloaded. Starting data downloaded...")
545
+ dt_now = datetime.now()
546
+ expired = dates.is_token_expired(dt_access_token, dt_now)
547
+
548
+ if expired:
549
+ access_token, dt_access_token = self.refresh_access_token(refresh_token)
550
+ self.download(access_token, link_mod, filepath[1], product_name, name)
551
+ else:
552
+ logger.warning("Tile not selected to download")
553
+
554
+
izisat/misc/dates.py ADDED
@@ -0,0 +1,24 @@
1
+ import datetime
2
+ from loguru import logger
3
+
4
+ class Dates:
5
+ def __init__(self):
6
+ pass
7
+
8
+ def is_token_expired(self, dt_access_token, dt_download):
9
+ """
10
+ Check if the provided access token has expired.
11
+
12
+ Parameters:
13
+ -----------
14
+ access_token: str
15
+ The access token to be checked for expiration.
16
+
17
+ Returns:
18
+ --------
19
+ bool
20
+ True if the token has expired, False otherwise.
21
+ """
22
+
23
+ # Check if the current time is 9 minutes or more past the expiration time
24
+ return dt_download > dt_access_token + datetime.timedelta(minutes=9)