Skip to content

Download Sentinel-3 Data

Configuration

s3lst_ds.data_download.download_sentinel3_config.Sentinel3Config dataclass

Configurations for querying, downloading and filtering Sentinel-3 companion LST and SYN products (to be used in download_sentinel3.py).

Querying is done through OData API (https://documentation.dataspace.copernicus.eu/APIs/OData.html). A query on the LST products is firstly done so that they satisfy the given criteria (timeliness, start_sensing_dates, end_sensing_dates and geometry_query). The resultant query items are filtered for extra criteria (cloud_cover_lims, relative_orbit and orbit_dir). Only after that, a query for the companion SYN products is done for each filtered LST product. If a companion SYN product exists, the LST product is downloaded and subject to even more filtering criteria (cloud_cover_max_aoi as according to geometry_aoi). If the LST product satisfies such criteria, the companion SYN product is then downloaded.

For more details on LST and SYN products access https://sentiwiki.copernicus.eu/web/slstr-products#S3-SLSTR-Products-L2-LST-Products and https://sentiwiki.copernicus.eu/web/synergy-products#SYNERGYProducts-L2SYNProductsS3-Synergy-Products-L2-SYN-Products, respectively.

Note that the script download_sentinel3.py is such that the order of the returned querying items is with respect to start sensing time in ascending order (oldest to newest).

Attributes:

Name Type Description
out_dir Path

The directory path to save the LST and SYN downloaded data.

timeliness ({NR, NT}, optional)

Timeliness of the LST and SYN products to consider in the query. This may be either: - "NR" (Near Real Time); - "NT" (Non-Time Critical).

If not issued, both timeliness products are considered.

Note that the revisit times of Sentinel-3 OLCI (SYN) and SLSTR (LST) are less than 2 days and less than 1 day, respectively (https://sentiwiki.copernicus.eu/web/s3-mission#S3Mission-OrbitS3-Mission-Orbittrue), and the uploading time after acquisition for NR (Near Real Time), and NT (Non-Time Critical) products less than 3 hours, and after 48 hours (but possibly up to 1 month), respectively, for OLCI (https://sentiwiki.copernicus.eu/__attachments/1672112/OMPC.ACR.HBK.001%20-%20Sentinel%203%20OLCI%20Land%20Handbook%20-%201.3.pdf#page=24) and within 3 hours, and within 24-48 hours, respectively, for SLSTR (https://sentiwiki.copernicus.eu/__attachments/1672112/OMPC.ACR.HBK.002%20-%20Sentinel%203%20SLSTR%20Land%20Handbook%202024%20-%201.4.pdf#page=26).

start_sensing_dates (str or list[str], optional)

Oldest start sensing date of the LST and SYN products to consider in the query, with format "%Y-%m-%d". If not issued, it is set as 31 days before the newest end sensing date (end_sensing_dates). Note that the respective time is set to millisecond 0 of the date (in UTC) and the filtering is such that the limit is included in the admissible datetime range. If a list is issued, end_sensing_dates must also correspond to a list - with each entry of one associated with the respective entry of the other - and a query is done for each resultant datetime range defined by the paired start and end sensing dates.

end_sensing_dates (str or list[str], optional)

Newest end sensing date of the LST and SYN products to consider in the query, with format "%Y-%m-%d". If not issued, it is set as current date. Note that the respective time is set to last millisecond of the date (in UTC), and the filtering is such that the limit is included in the admissible datetime range. If a list is issued, start_sensing_dates must also correspond to a list. If a list is issued, start_sensing_dates must also correspond to a list - with each entry of one associated with the respective entry of the other - and a query is done for each resultant datetime range defined by the paired start and end sensing dates.

geometry_aoi (str, Path, optional)

WKT string or path to AOI geometry file to filter the LST downloaded data with respect to cloud cover fraction. Data would be considered of interest if the cloud cover fraction in the AOI does not exceed parameter cloud_cover_max_aoi. Data with higher cloud cover fraction in the AOI would be deleted. Note that if a path to a file is issued, the file format must be supported by geopandas.read_file() (e.g. .shp, .geojson, .json, .gpkg). Also note that this filtering is solely considered if process is set to True.

geometry_query (str, Path, optional)

WKT string or path to WKT geometry file to intersect the querying LST and SYN data with (note that an intersection includes its boundary). Data would be considered of interest if its geometry has at least one point in common with this one. Note that file's geometry should correspond to a polygon with same start and end vertices. Furthermore, its coordinates must be expressed in EPSG:4326. The parameter is not only used for querying but also for subsetting the downloaded Sentinel-3 products (if mask_clouds is True). If geometry_aoi is issued, but geometry_query is not, geometry_query would be set as the convex hull of geometry_aoi.

cloud_cover_lims (tuple[float, float], optional)

Minimum and maximum cloud cover fraction (in percentage) that the queried LST data must have. Data with a cloud cover fraction out of the issued range would not be considered. The parameter is solely used for filtering the queried products before downloading them.

relative_orbit (list[int], optional)

Relative orbit numbers that the queried LST data must have. Data with numbers out of the issued list would not be considered. The parameter is solely used for filtering the queried products before downloading them.

More information on relative orbit numbers may be obtained from Sentinel-3 Mission Wiki page (https://sentiwiki.copernicus.eu/web/s3-mission#S3Mission-OrbitS3-Mission-Orbittrue)

orbit_dir ({ASCENDING, DESCENDING}, optional)

Orbit direction that the LST queried data must have. Data with directions out of the issued list would not be considered. The parameter is solely used for filtering the queried products before downloading them.

cloud_cover_max_aoi float, default=100

Maximum cloud cover fraction (in percentage) that the LST data must have in the AOI (associated with parameter geometry_aoi). Data with a higher value would be deleted. The parameter is solely used for filtering after downloading and if parameter geometry_aoi is issued and process is set to True.

process bool, default=True

Whether to process the downloaded products by georeferencing, subsetting them to bands and domain of interest and in the case of the LST product also masking out the clouded pixels (if mask_clouds is True).

process_engine {"rioxarray", "snappy"}, default="rioxarray"

The alias of the backend Python package to use in the processing of the Sentinel-3 products. This is either - "rioxarray"; - "snappy" (that is, the aliases for the respective backend Python packages).

filter_max_footprint_aoi_overlap bool, default=False

If True and more than one LST product is queried for a given date and AOI, only the one with the maximum footprint overlap with the AOI is considered. Furthermore, if there are multiple products with maximum footprint/AOI overlap, the one with the lowest cloud cover fraction in the footprint is considered.

log_mode {None, "console", "file", "both"}, default="both"

The logging mode for the querying, downloading and filtering processes: - None: No logging is done; - "console": Logging is done to console only; - "file": Logging is done to a log file only; - "both": Logging is done to both console and a log file. Note the log file would be defined as sentinel3_download.log at out_dir.

Methods:

Name Description
__init__
Source code in src/s3lst_ds/data_download/download_sentinel3_config.py
@dataclass
class Sentinel3Config:
    """
    Configurations for querying, downloading and filtering Sentinel-3 companion LST and
    SYN products (to be used in download_sentinel3.py).

    Querying is done through OData API
    (https://documentation.dataspace.copernicus.eu/APIs/OData.html). A query on the LST
    products is firstly done so that they satisfy the given criteria (`timeliness`,
    `start_sensing_dates`, `end_sensing_dates` and `geometry_query`). The resultant
    query items are filtered for extra criteria (`cloud_cover_lims`, `relative_orbit`
    and `orbit_dir`). Only after that, a query for the companion SYN products is done
    for each filtered LST product. If a companion SYN product exists, the LST product is
    downloaded and subject to even more filtering criteria (`cloud_cover_max_aoi` as
    according to `geometry_aoi`). If the LST product satisfies such criteria, the
    companion SYN product is then downloaded.

    For more details on LST and SYN products access
    https://sentiwiki.copernicus.eu/web/slstr-products#S3-SLSTR-Products-L2-LST-Products
    and
    https://sentiwiki.copernicus.eu/web/synergy-products#SYNERGYProducts-L2SYNProductsS3-Synergy-Products-L2-SYN-Products,
    respectively.

    Note that the script download_sentinel3.py is such that the order of the returned
    querying items is with respect to start sensing time in ascending order (oldest to
    newest).

    Attributes
    ----------

    out_dir : Path
        The directory path to save the LST and SYN downloaded data.

    timeliness: {"NR", "NT"}, optional
        Timeliness of the LST and SYN products to consider in the query. This may be
        either:
            - `"NR"` (Near Real Time);
            - `"NT"` (Non-Time Critical).

        If not issued, both timeliness products are considered.

        Note that the revisit times of Sentinel-3 OLCI (SYN) and SLSTR (LST) are less
        than 2 days and less than 1 day, respectively
        (https://sentiwiki.copernicus.eu/web/s3-mission#S3Mission-OrbitS3-Mission-Orbittrue),
        and the uploading time after acquisition for NR (Near Real Time), and NT
        (Non-Time Critical) products less than 3 hours, and after 48 hours (but possibly
        up to 1 month), respectively, for OLCI
        (https://sentiwiki.copernicus.eu/__attachments/1672112/OMPC.ACR.HBK.001%20-%20Sentinel%203%20OLCI%20Land%20Handbook%20-%201.3.pdf#page=24)
        and within 3 hours, and within 24-48 hours, respectively, for SLSTR
        (https://sentiwiki.copernicus.eu/__attachments/1672112/OMPC.ACR.HBK.002%20-%20Sentinel%203%20SLSTR%20Land%20Handbook%202024%20-%201.4.pdf#page=26).


    start_sensing_dates : str or list[str], optional
        Oldest start sensing date of the LST and SYN products to consider in the query,
        with format "%Y-%m-%d". If not issued, it is set as 31 days before the newest
        end sensing date (`end_sensing_dates`). Note that the respective time is set to
        millisecond 0 of the date (in UTC) and the filtering is such that the limit is
        included in the admissible datetime range. If a list is issued,
        `end_sensing_dates` must also correspond to a list - with each entry of one
        associated with the respective entry of the other - and a query is done for each
        resultant datetime range defined by the paired start and end sensing dates.

    end_sensing_dates : str or list[str], optional
        Newest end sensing date of the LST and SYN products to consider in the query,
        with format "%Y-%m-%d". If not issued, it is set as current date. Note that the
        respective time is set to last millisecond of the date (in UTC), and the
        filtering is such that the limit is included in the admissible datetime range.
        If a list is issued, `start_sensing_dates` must also correspond to a list. If a
        list is issued, `start_sensing_dates` must also correspond to a list - with each
        entry of one associated with the respective entry of the other - and a query is
        done for each resultant datetime range defined by the paired start and end
        sensing dates.

    geometry_aoi : str, Path, optional
        WKT string or path to AOI geometry file to filter the LST downloaded data with
        respect to cloud cover fraction. Data would be considered of interest if the
        cloud cover fraction in the AOI does not exceed parameter `cloud_cover_max_aoi`.
        Data with higher cloud cover fraction in the AOI would be deleted. Note that if
        a path to a file is issued, the file format must be supported by
        `geopandas.read_file()` (e.g. `.shp`, `.geojson`, `.json`, `.gpkg`). Also note
        that this filtering is solely considered if `process` is set to `True`.

    geometry_query : str, Path, optional
        WKT string or path to WKT geometry file to intersect the querying LST and SYN
        data with (note that an intersection includes its boundary). Data would be
        considered of interest if its geometry has at least one point in common with
        this one. Note that file's geometry should correspond to a polygon with same
        start and end vertices. Furthermore, its coordinates must be expressed in
        EPSG:4326. The parameter is not only used for querying but also for subsetting
        the downloaded Sentinel-3 products (if `mask_clouds` is `True`). If
        `geometry_aoi` is issued, but `geometry_query` is not, `geometry_query` would be
        set as the convex hull of `geometry_aoi`.

    cloud_cover_lims : tuple[float, float], optional
        Minimum and maximum cloud cover fraction (in percentage) that the queried LST
        data must have. Data with a cloud cover fraction out of the issued range would
        not be considered. The parameter is solely used for filtering the queried
        products before downloading them.

    relative_orbit : list[int], optional
        Relative orbit numbers that the queried LST data must have. Data with numbers
        out of the issued list would not be considered. The parameter is solely used for
        filtering the queried products before downloading them.

        More information on relative orbit numbers may be obtained from Sentinel-3
        Mission Wiki page
        (https://sentiwiki.copernicus.eu/web/s3-mission#S3Mission-OrbitS3-Mission-Orbittrue)

    orbit_dir : {"ASCENDING", "DESCENDING"}, optional
        Orbit direction that the LST queried data must have. Data with directions out of
        the issued list would not be considered. The parameter is solely used for
        filtering the queried products before downloading them.

    cloud_cover_max_aoi : float, default=100
        Maximum cloud cover fraction (in percentage) that the LST data must have in the
        AOI (associated with parameter `geometry_aoi`). Data with a higher value would
        be deleted. The parameter is solely used for filtering after downloading and if
        parameter `geometry_aoi` is issued and `process` is set to `True`.

    process : bool, default=True
        Whether to process the downloaded products by georeferencing, subsetting them to
        bands and domain of interest and in the case of the LST product also masking out
        the clouded pixels (if `mask_clouds` is `True`).

    process_engine : {"rioxarray", "snappy"}, default="rioxarray"
        The alias of the backend Python package to use in the processing of the
        Sentinel-3 products. This is either
            - [`"rioxarray"`](https://corteva.github.io/rioxarray/stable/);
            - [`"snappy"`](https://github.com/senbox-org/esa-snappy) (that is, the
            aliases for the respective backend Python packages).

    filter_max_footprint_aoi_overlap : bool, default=False
        If `True` and more than one LST product is queried for a given date and AOI,
        only the one with the maximum footprint overlap with the AOI is considered.
        Furthermore, if there are multiple products with maximum footprint/AOI overlap,
        the one with the lowest cloud cover fraction in the footprint is considered.

    log_mode : {None, "console", "file", "both"}, default="both"
        The logging mode for the querying, downloading and filtering processes:
            - `None`: No logging is done;
            - `"console"`: Logging is done to console only;
            - `"file"`: Logging is done to a log file only;
            - `"both"`: Logging is done to both console and a log file.
        Note the log file would be defined as `sentinel3_download.log` at `out_dir`.
    """

    out_dir: Path
    timeliness: Literal["NR", "NT"] | None = None
    start_sensing_dates: str | list[str] | None = None
    end_sensing_dates: str | list[str] | None = None
    geometry_aoi: str | Path | None = None
    geometry_query: str | Path | None = None
    cloud_cover_lims: tuple[float, float] | None = None
    relative_orbit: list[int] | None = None
    orbit_dir: list[Literal["ASCENDING", "DESCENDING"]] | None = None
    cloud_cover_max_aoi: float = 100
    process: bool = True
    process_engine: Literal["rioxarray", "snappy"] = "rioxarray"
    mask_clouds: bool = True
    filter_max_footprint_aoi_overlap: bool = False
    log_mode: Literal["console", "file", "both"] | None = "both"

cloud_cover_lims class-attribute instance-attribute

cloud_cover_lims: tuple[float, float] | None = None

cloud_cover_max_aoi class-attribute instance-attribute

cloud_cover_max_aoi: float = 100

end_sensing_dates class-attribute instance-attribute

end_sensing_dates: str | list[str] | None = None

filter_max_footprint_aoi_overlap class-attribute instance-attribute

filter_max_footprint_aoi_overlap: bool = False

geometry_aoi class-attribute instance-attribute

geometry_aoi: str | Path | None = None

geometry_query class-attribute instance-attribute

geometry_query: str | Path | None = None

log_mode class-attribute instance-attribute

log_mode: Literal['console', 'file', 'both'] | None = 'both'

mask_clouds class-attribute instance-attribute

mask_clouds: bool = True

orbit_dir class-attribute instance-attribute

orbit_dir: list[Literal['ASCENDING', 'DESCENDING']] | None = None

out_dir instance-attribute

out_dir: Path

process class-attribute instance-attribute

process: bool = True

process_engine class-attribute instance-attribute

process_engine: Literal['rioxarray', 'snappy'] = 'rioxarray'

relative_orbit class-attribute instance-attribute

relative_orbit: list[int] | None = None

start_sensing_dates class-attribute instance-attribute

start_sensing_dates: str | list[str] | None = None

timeliness class-attribute instance-attribute

timeliness: Literal['NR', 'NT'] | None = None

__init__

__init__(
    out_dir: Path,
    timeliness: Literal["NR", "NT"] | None = None,
    start_sensing_dates: str | list[str] | None = None,
    end_sensing_dates: str | list[str] | None = None,
    geometry_aoi: str | Path | None = None,
    geometry_query: str | Path | None = None,
    cloud_cover_lims: tuple[float, float] | None = None,
    relative_orbit: list[int] | None = None,
    orbit_dir: list[Literal["ASCENDING", "DESCENDING"]] | None = None,
    cloud_cover_max_aoi: float = 100,
    process: bool = True,
    process_engine: Literal["rioxarray", "snappy"] = "rioxarray",
    mask_clouds: bool = True,
    filter_max_footprint_aoi_overlap: bool = False,
    log_mode: Literal["console", "file", "both"] | None = "both",
) -> None

Caller

s3lst_ds.data_download.download_sentinel3.download_products

download_products(config: Sentinel3Config) -> None
Source code in src/s3lst_ds/data_download/download_sentinel3.py
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706
1707
1708
1709
1710
1711
1712
1713
1714
1715
1716
1717
1718
1719
1720
1721
1722
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734
1735
1736
1737
1738
1739
1740
1741
1742
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754
1755
1756
1757
1758
1759
1760
1761
1762
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774
1775
1776
1777
1778
1779
1780
1781
1782
1783
1784
1785
1786
1787
1788
1789
1790
1791
1792
1793
1794
1795
1796
1797
1798
1799
1800
1801
1802
1803
1804
1805
1806
1807
1808
1809
1810
1811
1812
1813
1814
1815
1816
1817
1818
1819
1820
1821
1822
1823
1824
1825
1826
1827
1828
1829
1830
1831
1832
1833
1834
1835
1836
1837
1838
1839
1840
1841
1842
1843
1844
1845
1846
1847
1848
1849
1850
1851
1852
1853
1854
1855
1856
1857
1858
1859
1860
1861
1862
1863
1864
1865
1866
1867
1868
1869
1870
1871
1872
1873
1874
1875
1876
1877
1878
1879
1880
1881
1882
1883
1884
1885
1886
1887
1888
1889
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
def download_products(
    # Configuration parameters for querying, downloading and filtering Sentinel-3 LST
    # and SYN products.
    config: Sentinel3Config,
) -> None:

    # ---> Handle logging

    # Create logger
    logger = RichLogger(
        name="download",
        level=logging.INFO,
        file_path=(Path(config.out_dir) / "download_sentinel3.log"),
        file_mode="w",
        log_mode=config.log_mode,
    )

    # ---> Handle configurations

    # Create instance of Sentinel-3 product processor if processing is to be done
    if config.process is True:
        sen3_processor = Sen3Processor(engine=config.process_engine, logger=logger)

    # Validate input oldest start and newest end sensing dates
    if (
        isinstance(config.start_sensing_dates, list)
        and not isinstance(config.end_sensing_dates, list)
    ) or (
        not isinstance(config.start_sensing_dates, list)
        and isinstance(config.end_sensing_dates, list)
    ):
        logger.error(
            "[bold red]Oldest start sensing date is a list but newest end sensing"
            " date is not. If one is a list the other must also be, with each of"
            " their elements being associated with the respective ones of the"
            " other."
            "\nThe run will be stopped."
            "[/bold red]",
        )

        raise ConfigError(
            "Oldest start sensing date is a list but newest end sensing date"
            " is not. If one is a list the other must also be, with each of their"
            " elements being associated with the respective ones of the other."
            "\nThe run will be stopped."
        )

    # Convert oldest start and newest end sensing dates to lists if they are not already
    config.start_sensing_dates = (
        [config.start_sensing_dates]
        if not isinstance(config.start_sensing_dates, list)
        else config.start_sensing_dates
    )  # type: ignore
    config.end_sensing_dates = (
        [config.end_sensing_dates]
        if not isinstance(config.end_sensing_dates, list)
        else config.end_sensing_dates
    )  # type: ignore

    # Set the sensing date range for the query if any of the limits are not provided in
    # the configuration file
    # NOTE:
    # - If the newest end date (`end_sensing_dates`) is missing, set it as the current
    #   date
    #
    # - If the oldest start date (`start_sensing_dates`) is missing, set it as 31 days
    # before newest end date (`end_sensing_dates`)
    #
    # Note that the revisit times of Sentinel-3 OLCI and SLSTR are less than 2 days and
    # less than 1 day, respectively
    # (https://sentiwiki.copernicus.eu/web/s3-mission#S3Mission-OrbitS3-Mission-Orbittrue),
    # and the uploading time after acquisition for NR (Near Real Time), and NT (Non-Time
    # Critical) products less than 3 hours, and after 48 hours (but possibly up to 1
    # month), respectively, for OLCI
    # (https://sentiwiki.copernicus.eu/__attachments/1672112/OMPC.ACR.HBK.001%20-%20Sentinel%203%20OLCI%20Land%20Handbook%20-%201.3.pdf#page=24)
    # and within 3 hours, and within 24-48 hours, respectively, for SLSTR
    # (https://sentiwiki.copernicus.eu/__attachments/1672112/OMPC.ACR.HBK.002%20-%20Sentinel%203%20SLSTR%20Land%20Handbook%202024%20-%201.4.pdf#page=26).
    for i in range(len(config.end_sensing_dates)):  # type: ignore
        if config.end_sensing_dates is None:
            config.end_sensing_dates[i] = (  # type: ignore
                pd.Timestamp.now().strftime("%Y-%m-%d")
            )
    for i in range(len(config.start_sensing_dates)):  # type: ignore
        if config.start_sensing_dates is None:
            config.start_sensing_dates[i] = (  # type: ignore
                pd.Timestamp(config.end_sensing_dates[i])  # type: ignore
                - pd.Timedelta(days=31)
            ).strftime("%Y-%m-%d")

    # Convert oldest start and newest end sensing dates to de facto dates (to also
    # handle cases in which other formats such as timestamps are issued)
    start_sensing_dates = [
        pd.Timestamp(date).strftime("%Y-%m-%d")
        for date in config.start_sensing_dates  # type: ignore
    ]
    end_sensing_dates = [
        pd.Timestamp(date).strftime("%Y-%m-%d")
        for date in config.end_sensing_dates  # type: ignore
    ]

    # Set start sensing time as millisecond 0 of the start sensing date (in UTC)
    start_sensing_times = [
        pd.Timestamp(date).strftime("%Y-%m-%dT%H:%M:%S.%f"[:-3] + "Z")
        for date in start_sensing_dates
    ]

    # Set end sensing time as last millisecond of the end sensing date (in UTC)
    end_sensing_times = [
        (
            pd.Timestamp(date) + pd.Timedelta(days=1) - pd.Timedelta(milliseconds=1)
        ).strftime("%Y-%m-%dT%H:%M:%S.%f"[:-3] + "Z")
        for date in end_sensing_dates
    ]

    logger.console.print()  # type: ignore
    logger.info(
        "Downloading Sentinel-3 products for dates"
        + ("\n- " if len(start_sensing_dates) > 1 else " ")
        + ",\n- ".join(
            [
                f"{start_sensing_date} to {end_sensing_date}"
                for start_sensing_date, end_sensing_date in zip(
                    start_sensing_dates, end_sensing_dates
                )
            ]
        )
        + "."
    )

    # Read the AOI geometry (if issued) as a GeoPandas DataFrame for which filtering of
    # the downloaded LST data is to be done with respect to cloud cover fraction
    aoi = load_aoi_to_gdf(config.geometry_aoi)

    # Read the querying geometry WKT (if issued) as a string. If not issued, but
    # `geometry_aoi` is, the `geometry_query` will be set as the convex hull of
    # `geometry_aoi`.
    # NOTE: Queried data would be considered of interest if its geometry has at least
    # one point in common with this one.
    if config.geometry_query is not None:
        if isinstance(config.geometry_query, Path):
            with open(config.geometry_query, "r") as f:
                config.geometry_query = f.read().strip()

    elif aoi is not None:
        config.geometry_query = to_convex_hull_wkt(aoi)

    # ---> Query Sentinel-3 LST products (and filter returned queried items)
    logger.console.print()  # type: ignore

    with logger.console.status(
        f"{'':7}Querying Sentinel-3 LST products[yellow]...[/yellow]",
        spinner="dots",
        spinner_style="bold blue",
    ):
        # Get list of all timestamps at the start of all months between defined start
        # and end sensing times including these start and end sensing times
        times = [
            [start_sensing_time]
            + pd.date_range(
                # Left bound
                start=pd.Timestamp(start_sensing_time),  # type: ignore
                # Right bound
                end=pd.Timestamp(end_sensing_time),  # type: ignore
                # "neither", to not include bounds in the date range
                inclusive="neither",
                # Use month start frequency
                freq="MS",
            )
            .strftime("%Y-%m-%dT%H:%M:%S.%f"[:-3] + "Z")
            .tolist()
            + [end_sensing_time]
            for start_sensing_time, end_sensing_time in zip(
                start_sensing_times, end_sensing_times
            )
        ]

        # Query
        # NOTE: several queries are performed. These are done month by month so that the
        # number of query items returned in each query is not greater than the API
        # limit,
        # 1000. The items from all queries are then concatenated into a single
        # DataFrame.
        lst_query = pd.concat(
            [
                pd.DataFrame(
                    query_cdse(
                        collection="SENTINEL-3",
                        includes=["SL_2_LST", f"_{config.timeliness}_"],
                        start_sensing_time=times[i][j],  # type: ignore
                        end_sensing_time=times[i][j + 1],  # type: ignore
                        geometry=config.geometry_query,
                        orderby_start_sensing_time="asc",
                        # WARNING: max_query_items may be ajusted if needed, but never
                        # surpassing 1000.
                        max_query_items=1000,
                        logger=logger,
                    )["value"],
                )
                for i in range(len(times))
                for j in range(len(times[i]) - 1)
            ],
            ignore_index=True,
        )

        # If not empty, filter queried items according to minimum and maximum cloud
        # cover fraction, relative orbit number and orbit direction
        if not lst_query.empty:
            # Remove duplicated items (which may happen if overlapping occurs between
            # each issued sensing daterange limits)
            lst_query = lst_query.drop_duplicates(subset="Name")

            # Express field "Attributes" of the queried items as expanded fields, each
            # one associated a single attribute
            lst_query = expand_attributes(lst_query)

            # Filter queried items according to minimum and maximum cloud cover
            # fraction, relative orbit number and orbit direction and disregard half and
            # full orbit products.
            # NOTE: half and full orbit products contain 4 underscores ("____") in the
            # frame along track coordinate of the <instance_id> substring in the product
            # name. For more details see
            # https://user.eumetsat.int/resources/user-guides/sentinel-safe-format-guide/.
            # NOTE: field "cloudCover" might not be in the data for some datetimes.
            lst_query = lst_query[~(lst_query["Name"].str[77:81] == "____")]

            if (
                "cloudCover" in lst_query.columns
                and config.cloud_cover_lims is not None
            ):
                lst_query = lst_query[
                    lst_query["cloudCover"].isna()
                    | (
                        (lst_query["cloudCover"] >= config.cloud_cover_lims[0])
                        & (lst_query["cloudCover"] <= config.cloud_cover_lims[1])
                    )
                ]
            if config.relative_orbit is not None:
                lst_query = lst_query[
                    lst_query["relativeOrbitNumber"].isin(config.relative_orbit)
                ]
            if config.orbit_dir is not None:
                lst_query = lst_query[
                    lst_query["orbitDirection"].isin(config.orbit_dir)
                ]

    # Check if query results are empty and raise error in that case
    if lst_query.empty:
        logger.warning(
            "[bold yellow]No Sentinel-3 LST products were found for the requested"
            " period, AOI, cloud cover fraction limits, relative orbit number or"
            " orbit direction."
            "\nThe run will stop.[/bold yellow]"
        )
        raise NoProductFoundError(
            "No Sentinel-3 LST products were found for the requested period, AOI,"
            " cloud cover fraction limits, relative orbit number or orbit"
            " direction.\nTry using different filtering criteria."
        )

    else:
        logger.info(
            f"[bold green]{len(lst_query)} Sentinel-3 LST products have been found"
            + " in the query:[/bold green][green]\n"
            + "\n".join(lst_query["Name"].tolist())
            + "[/green]"
        )

    # WARNING: For Open-Cosmos project only: consider solely the LST product that has
    # the maximum footprint overlap with the AOI. If there are multiple products with
    # maximum footprint overlap, select the one with the smallest cloud cover fraction.
    if config.filter_max_footprint_aoi_overlap:
        if len(lst_query) > 1:
            logger.info(
                "Solely the LST product that has the maximum footprint overlap with"
                " the AOI will be considered."
            )
            # Get footprint-AOI overlap (in percentage) for each queried item.
            # NOTE: if no AOI is defined, the overlap is set to 0 %.
            lst_query["FootprintAoiOverlap"] = lst_query["Footprint"].apply(
                lambda x: footprint_aoi_overlap(
                    footprint_wkt=x,
                    aoi_gdf=aoi,
                )
            )

            # Get maximum overlap value
            max_overlap = lst_query["FootprintAoiOverlap"].max()
            # Keep only products with maximum AOI overlap
            lst_query = lst_query[
                lst_query["FootprintAoiOverlap"] == max_overlap
            ].copy()

            # If multiple products have the maximum AOI overlap, select the one with the
            # lowest cloud cover fraction by firstly sorting the queried items by cloud
            # cover fraction in ascending order.
            if len(lst_query) > 1:
                if "cloudCover" in lst_query.columns:
                    lst_query = lst_query.sort_values(
                        by="cloudCover",
                        ascending=True,
                        na_position="last",
                    )
                    logger.warning(
                        "[bold yellow]Multiple products with maximum footprint/AOI"
                        " overlap detected. Product with lowest cloud cover"
                        " fraction in the footprint was selected.[/bold yellow]"
                    )
                else:
                    logger.warning(
                        "[bold yellow]Multiple products with maximum AOI overlap"
                        " detected, but discrimination with respect to cloud cover"
                        " fraction cannot be performed since 'cloudCover' attribute"
                        " is missing. First product was selected.[/bold yellow]"
                    )
        # Select the first product
        lst_query = lst_query.iloc[[0]].reset_index(drop=True)

    # Get LST products notable info from query results
    lst_infos = ProdInfo.load_from_df(lst_query)  # type: ignore

    # Create a CDSE authentication state object for handling the fetching of a valid
    # CDSE access token
    # NOTE: whenever auth.get_access_token() is called, the access token is
    # generated, refreshed or regenerated as required to make it valid.
    auth = CDSEAuthState(
        username=os.environ["CDSE_USER"],
        password=os.environ["CDSE_PASS"],
    )

    for lst_info in lst_infos:
        # ---> Query Sentinel-3 SYN products that accompany current LST product
        logger.console.print()  # type: ignore

        with logger.console.status(
            f"{'':7}Querying Sentinel-3 SYN product[yellow]...[/yellow]",
            spinner="dots",
            spinner_style="bold blue",
        ):
            # Query API for Sentinel-3 SYN products
            syn_query = pd.DataFrame(
                query_cdse(
                    collection="SENTINEL-3",
                    includes=["SY_2_SYN", f"_{config.timeliness}_"],
                    start_sensing_time=(
                        (
                            pd.Timestamp(lst_info.start_sensing_time)
                            - pd.Timedelta(seconds=1)
                        ).strftime("%Y-%m-%dT%H:%M:%S.%f"[:-3] + "Z")
                    ),
                    end_sensing_time=(
                        (
                            pd.Timestamp(lst_info.end_sensing_time)
                            + pd.Timedelta(seconds=1)
                        ).strftime("%Y-%m-%dT%H:%M:%S.%f"[:-3] + "Z")
                    ),
                    geometry=config.geometry_query,
                    orderby_start_sensing_time="asc",
                    max_query_items=1000,
                    logger=logger,
                )["value"]
            )

            if not syn_query.empty:
                # Express field "Attributes" of the queried items as expanded fields,
                # each one associated an attribute
                # NOTE: the field "Attributes" of the queried items contains a list of
                # dictionaries for each item. Each dictionary describes a particular
                # attribute. The attribute name and value is keyed by "Name" and
                # "Value", respectively.
                syn_query = expand_attributes(syn_query)

                # Filter SYN queried items so that that they "accompany" the current LST
                # product (for that, they must be associated with the same satellite
                # platform, orbit number and start and end sensing times).
                syn_query = syn_query[
                    (syn_query["platformSerialIdentifier"] == lst_info.platform)
                    & (syn_query["orbitNumber"] == lst_info.orbit)
                    # & (syn_query["Name"].str[82:85] == lst_info.generating_centre)
                    & (syn_query["Name"].str[16:31] == lst_info.start_sensing_time)
                    & (syn_query["Name"].str[32:47] == lst_info.end_sensing_time)
                ]

        # Check if the query results are not empty neither multiple. If they are, do not
        # download current LST product neither its companion SYN products and advance to
        # the next LST product. If the query results are singular, get the SYN product
        # info.
        if syn_query.empty:
            logger.warning(
                "[bold yellow]No companion Sentinel-3 SYN product was found in the"
                + " SYN query for LST's"
                + f"\n{lst_info.name!r}."
                + (
                    (
                        "\nThe LST product will not be downloaded and the process"
                        " will advance to the next one."
                    )
                    if config.filter_max_footprint_aoi_overlap is False
                    else (
                        "\nThe LST product will not be downloaded and the process"
                        " will terminate."
                    )
                )
                + "[/bold yellow]"
            )
            if config.filter_max_footprint_aoi_overlap is False:
                continue
            else:
                raise NoMatchingProductError(
                    "No matching Sentinel-3 SYN product was found for the LST product. "
                    "Try using different filtering criteria."
                )
        elif len(syn_query) > 1:
            logger.warning(
                "[bold yellow]Multiple companion Sentinel-3 SYN products were found"
                + " in the SYN query for LST's"
                + f"\n{lst_info.name!r}."
                + "\nTry using stricter querying filters to get a unique companion"
                + " product instead."
                + f"\nFound companion products:\n{
                    ',\n'.join(f'{name!r}' for name in syn_query['Name'].to_list())
                }."
                + (
                    (
                        "\nThe LST product will not be downloaded and the process"
                        " will advance to the next one."
                    )
                    if config.filter_max_footprint_aoi_overlap is False
                    else (
                        "\nThe LST product will not be downloaded and the process"
                        " will terminate."
                    )
                )
                + "[/bold yellow]"
            )
            if config.filter_max_footprint_aoi_overlap is False:
                continue
            else:
                raise MultipleMatchingProdutsError(
                    "Multiple companion Sentinel-3 SYN products were found in the SYN"
                    " query for LST's."
                    "\nTry using stricter querying filters to get a unique companion."
                )
        else:
            # Get SYN product info
            syn_info = ProdInfo.load_from_df(syn_query.iloc[[0]])[0]
            logger.info(
                "[bold green]Singular companion Sentinel-3 SYN product"
                + f"\n{syn_info.name!r}"
                + "\nwas found in the SYN query for LST's"
                + f"\n{lst_info.name!r}.[/bold green]"
                + "\nLST product will now be downloaded."
            )

        # ---> Download and unzip the current LST product

        # Download and unzip the current LST product
        download_product(
            prod_info=lst_info,
            path_out_dir=config.out_dir / lst_info.start_sensing_time,
            access_token=auth.get_access_token(),
            logger=logger,
        )

        # Get path to the unzipped LST product
        lst_path = config.out_dir / lst_info.start_sensing_time / lst_info.name

        if config.process is True:
            # ---> Reproject LST product to EPSG:4326, subset to bands of interest and
            # the bounding box of a geometry, write result to NetCDF file and delete
            # original file.
            # NOTE: it is important to subset the product to a bounding box since some
            # files are too huge to be handled directly (these files probably include
            # large regions not corresponding to the satellite overpassing one,
            # containing NODATA instances).

            # If a reprojected product does not exist, reproject the product if wanted
            if not lst_path.with_name(f"{lst_path.name}.nc").exists():
                logger.info("Product will now be reprojected and subsetted.")

                with logger.console.status(
                    f"{'':7}Reprojecting and subsetting the LST product[yellow]..."
                    + "[/yellow]",
                    spinner="dots",
                    spinner_style="bold blue",
                ):
                    try:
                        sen3_processor.process(
                            prod_path=lst_path,
                            geometry=config.geometry_query,
                        )
                        error = None
                    except Exception as e:  # noqa: BLE001
                        # Capture exception to log it after the console.status() scope
                        # or the message will be badly printed
                        error = e

                # If error occurred while checking, stop the run
                if error is not None:
                    if (
                        "zero-size array to reduction operation maximum which has no"
                        + " identity"
                        in str(error)
                    ):
                        logger.warning(
                            "[bold yellow]Empty array error when reprojecting or"
                            " subsetting LST product"
                            + f"\n{lst_info.name!r}."
                            + f"\nError message: {error}"
                            + (
                                "\nProduct was already invalid from source and"
                                " could not be used. Deleting product folder and"
                                " advancing to the next one."
                                if config.filter_max_footprint_aoi_overlap is False
                                else (
                                    "\nProduct was already invalid from source and"
                                    " cannot be used. Product folder will be"
                                    " deleted and the run will stop. Try using"
                                    " different filtering criteria."
                                )
                            )
                            + "[/bold yellow]"
                        )
                        # Delete the last product folder
                        delete_last_product(
                            folder=config.out_dir / lst_info.start_sensing_time,
                            logger=logger,
                        )
                        if config.filter_max_footprint_aoi_overlap is False:
                            continue
                        else:
                            raise InvalidProductError(
                                "Empty array error when reprojecting or subsetting LST"
                                + " product"
                                + f"\n{lst_info.name!r}."
                                + f"\nError message: {error}"
                                + "\nProduct was already invalid from source and could"
                                " not be used."
                                "\nProduct folder was, therefore, deleted."
                                " Try using different filtering criteria."
                            )
                    else:
                        logger.error(
                            "[bold red]Failed reprojecting or subsetting LST"
                            + f" product\n{lst_info.name!r}."
                            + f"\nError message: {error}"
                            + "\nFile is most likely corrupted (possibly due to a"
                            " stopped download or unzip process) and cannot be"
                            " used."
                            "\nProduct folder will be deleted and the run will"
                            " stop.[/bold red]",
                        )
                        # Delete the last product folder
                        delete_last_product(
                            folder=config.out_dir / lst_info.start_sensing_time,
                            logger=logger,
                        )
                        raise ReprojectionError(
                            "Failed reprojecting or subsetting LST product"
                            + f"\n{lst_info.name!r}."
                            + f"\nError message: {error}"
                            + "\nFile was most likely corrupted (possibly due to a"
                            " stopped download or unzip process) and could not be used."
                            + "\nProduct folder was, therefore, deleted."
                        )

                else:
                    logger.info(
                        "[bold green]Product reprojected and subsetted.[/bold green]"
                    )

            # If it already exists, no reprojection or subsection will be done
            else:
                logger.info(
                    "[bold green]Product is already reprojected and subsetted."
                    "[/bold green]"
                )

            # ---> Check cloud cover fraction in the AOI
            # Get path to the reprojected LST product
            lst_path = lst_path.with_name(f"{lst_path.name}.nc")

            if aoi is not None:
                with logger.console.status(
                    f"{'':7}Checking if AOI's cloud cover fraction in the LST product"
                    + " is smaller than the configured[cyan]"
                    + f" {config.cloud_cover_max_aoi}[/cyan] %[yellow]...[/yellow]",
                    spinner="dots",
                    spinner_style="bold blue",
                ):
                    try:
                        cloud_cover_aoi = compute_cloud_cover_aoi_lst(
                            prod_path=lst_path, aoi=aoi
                        )
                        error = None
                    except Exception as e:  # noqa: BLE001
                        # Capture exception to log it after the console.status() scope
                        # or the message will be badly printed
                        error = e

                # If error occurred while checking stop the run
                if error is not None:
                    logger.error(
                        "[bold red]Failed computing cloud cover fraction over the"
                        " AOI for LST product"
                        + f"\n{lst_info.name!r}."
                        + f"\nError message: {error}"
                        + "\nProduct folder will be deleted and the run stopped."
                        "[/bold red]",
                    )
                    # Delete the last product folder
                    delete_last_product(
                        folder=config.out_dir / lst_info.start_sensing_time,
                        logger=logger,
                    )
                    raise CloudCoverComputationError(
                        "Failed computing cloud cover fraction over the AOI for LST"
                        " product"
                        + f"\n{lst_info.name!r}."
                        + f"\nError message: {error}"
                        + "\nProduct folder was, therefore, deleted."
                    )

            # ---> Mask out clouded pixels in the LST data if wanted and the cloud cover
            # fraction in the issued AOI does not exceed the issued limit.
            if config.mask_clouds is True and (
                aoi is None
                or (aoi is not None and cloud_cover_aoi <= config.cloud_cover_max_aoi)
            ):
                mask_clouds(prod_path=lst_path, logger=logger)

        # ---> Download, unzip, reproject and subset companion SYN product if for the
        # LST product the cloud cover fraction in the issued AOI does not exceed the
        # issued limit. If the cloud cover fraction exceeds the limit, delete the LST
        # product, respective folder, and continue to the processing of the next LST
        # product.
        if (
            config.process is False
            or aoi is None
            or (aoi is not None and cloud_cover_aoi <= config.cloud_cover_max_aoi)
        ):
            if config.process is True and aoi is not None:
                logger.info(
                    "[bold green]Cloud cover fraction in the AOI for LST product is"
                    + f" {cloud_cover_aoi:.2f} %, not exceeding issued maximum"
                    + f" limit, {config.cloud_cover_max_aoi} %.[/bold green]"
                    + "\nThe companion SYN product will now be downloaded."
                )
            else:
                logger.info("The companion SYN product will now be downloaded.")

            # Download and unzip the companion SYN product
            download_product(
                prod_info=syn_info,
                path_out_dir=config.out_dir / lst_info.start_sensing_time,
                access_token=auth.get_access_token(),
                logger=logger,
            )

            # Get path to the unzipped SYN product
            syn_path = config.out_dir / lst_info.start_sensing_time / syn_info.name

            # Reproject SYN product to EPSG:4326, subset to bands of interest and the
            # bounding box of a geometry, write result to NetCDF file and delete
            # original file
            if config.process is True:
                if not syn_path.with_name(f"{syn_path.name}.nc").exists():
                    logger.info("Product will now be reprojected and subsetted.")

                    with logger.console.status(
                        f"{'':7}Reprojecting and subsetting the SYN product"
                        + "[yellow]...[/yellow]",
                        spinner="dots",
                        spinner_style="bold blue",
                    ):
                        try:
                            sen3_processor.process(
                                prod_path=syn_path,
                                geometry=config.geometry_query,
                            )

                            error = None
                        except Exception as e:  # noqa: BLE001
                            # Capture exception to log it after the console.status()
                            # scope or the message will be badly printed
                            error = e

                    # If error occurred while checking, stop the run
                    if error is not None:
                        if (
                            "zero-size array to reduction operation maximum which has"
                            + " no identity"
                            in str(error)
                        ):
                            logger.warning(
                                "[bold yellow]Empty array error when reprojecting"
                                + " or subsetting SYN product"
                                + f"\n{syn_info.name!r}."
                                + f"\nError message: {error}"
                                + (
                                    "\nProduct was already invalid from source and"
                                    " could not be used. Deleting product folder"
                                    " and advancing to the next one."
                                    if config.filter_max_footprint_aoi_overlap is False
                                    else (
                                        "\nProduct was already invalid from source"
                                        " and cannot be used. Product folder will"
                                        " be deleted and the run will stop. Try"
                                        " using different filtering criteria."
                                    )
                                )
                                + "[/bold yellow]"
                            )
                            # Delete the last product folder
                            delete_last_product(
                                folder=config.out_dir / syn_info.start_sensing_time,
                                logger=logger,
                            )

                            if config.filter_max_footprint_aoi_overlap is False:
                                continue
                            else:
                                raise InvalidProductError(
                                    "Empty array error when reprojecting or subsetting"
                                    + " SYN product"
                                    + f"\n{syn_info.name!r}."
                                    + f"\nError message: {error}"
                                    + "\nProduct was already invalid from source and"
                                    + " could not be used."
                                    + "\nProduct folder was, therefore, deleted."
                                    + " Try using different filtering criteria."
                                )

                        else:
                            logger.error(
                                "[bold red]Failed reprojecting or subsetting SYN"
                                " product"
                                + f"\n{syn_info.name!r}."
                                + f"\nError message: {error}"
                                + "\nFile is most likely corrupted (possibly due to"
                                " a stopped download or unzip process) and cannot"
                                " be used."
                                "\nProduct folder will be deleted and the run will"
                                " stop.[/bold red]"
                            )
                            # Delete the last product folder
                            delete_last_product(
                                folder=config.out_dir / syn_info.start_sensing_time,
                                logger=logger,
                            )
                            raise ReprojectionError(
                                "Failed reprojecting or subsetting SYN product."
                                + f"\n{syn_info.name!r}."
                                + f"\nError message: {error}"
                                + "\nFile was most likely corrupted (possibly due to a"
                                " stopped download or unzip process) and could not be"
                                " used."
                                "\nProduct folder was, therefore, deleted and"
                                " the run stopped."
                                "\nTry running again."
                            )
                    else:
                        logger.info(
                            "[bold green]Product reprojected and subsetted."
                            "[/bold green]"
                        )

                # If it already exists, no reprojection or subsection will be done
                else:
                    logger.info(
                        "[bold green]Product is already reprojected and subsetted."
                        + "[/bold green]"
                    )

        else:
            logger.warning(
                "[bold yellow]Cloud cover fraction in the AOI for LST product"
                + f"\n{lst_info.name!r}"
                + f"\nis {cloud_cover_aoi:.2f} %, which exceeds the issued limit,"
                + f" {config.cloud_cover_max_aoi} %."
                + "\nThis case will not be considered. The LST product will be"
                + " deleted as well as its subfolder if it only contained that"
                + " product, and the companion SYN product will not be downloaded. "
                + (
                    "The run will advance to the processing of the next LST product."
                    if config.filter_max_footprint_aoi_overlap is False
                    else "Run will stop. Try using different filtering criteria."
                )
                + "[/bold yellow]"
            )

            # Delete LST product
            try:
                lst_path.unlink()
                logger.warning(
                    f"[bold yellow]LST product {lst_path.name!r} was deleted."
                    + "[/bold yellow]"
                )
            except Exception as e:  # noqa: BLE001
                logger.error(
                    f"[bold red]Error removing LST product {lst_path.name!r}."
                    + f"\nError message: {e}"
                    + "\nRun will stop.[/bold red]",
                )
                raise CleanupError(
                    f"Error removing LST product {lst_path.name!r}."
                    + f"\nError message: {e}"
                )

            # If the subfolder of the deleted LST product is empty, delete it
            if not any(lst_path.parent.iterdir()):
                try:
                    shutil.rmtree(lst_path.parent)
                    logger.warning(
                        f"[bold yellow]Subfolder {lst_path.parent.name!r} was"
                        + " deleted.[/bold yellow]"
                    )
                except Exception as e:  # noqa: BLE001
                    logger.error(
                        "[bold red]Error removing subfolder"
                        + f" {lst_path.parent.name!r}\nError message: {e}"
                        + "\nRun will stop.[/bold red]",
                    )
                    raise CleanupError(
                        f"Error removing subfolder {lst_path.parent.name!r}"
                        + f"\nError message: {e}"
                    )
            if config.filter_max_footprint_aoi_overlap is False:
                continue
            else:
                raise CloudCoverLimitError(
                    "Cloud cover fraction in the AOI for the LST product"
                    + f"\n{lst_info.name!r}"
                    + f"\nis {cloud_cover_aoi:.2f} %, which exceeds the issued limit,"
                    + f" {config.cloud_cover_max_aoi} %."
                    + "\nThis case will not be considered."
                    + " Run will stop. Try using different filtering criteria."
                )