firms-sdk 0.1.0__tar.gz
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.
firms_sdk-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firms-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Basic SDK Client to retrive hotspots data from FIRMS API
|
|
5
|
+
Author: Sebástian Andrés Narváez Salcedo
|
|
6
|
+
Author-email: sebnarvaez19@gmail.com
|
|
7
|
+
Requires-Python: >=3.14
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
10
|
+
Requires-Dist: pandas (>=3.0.0,<4.0.0)
|
|
11
|
+
Requires-Dist: requests (>=2.32.5,<3.0.0)
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
|
|
14
|
+
# FIRMS SDK
|
|
15
|
+
|
|
16
|
+
Basic SDK Client to retrive hotspots data from FIRMS API.
|
|
17
|
+
The client requires the MAP_KEY from NASA and the region of interest to be instanciated.
|
|
18
|
+
|
|
19
|
+
```py
|
|
20
|
+
from firms_sdk import FIRMSClient
|
|
21
|
+
|
|
22
|
+
MAP_KEY = "******************************c7"
|
|
23
|
+
REGION = "-75.2736908751,10.2543646415,-74.7146900698,11.1024970837"
|
|
24
|
+
client = FIRMSClient(MAP_KEY, REGION)
|
|
25
|
+
```
|
|
26
|
+
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# FIRMS SDK
|
|
2
|
+
|
|
3
|
+
Basic SDK Client to retrive hotspots data from FIRMS API.
|
|
4
|
+
The client requires the MAP_KEY from NASA and the region of interest to be instanciated.
|
|
5
|
+
|
|
6
|
+
```py
|
|
7
|
+
from firms_sdk import FIRMSClient
|
|
8
|
+
|
|
9
|
+
MAP_KEY = "******************************c7"
|
|
10
|
+
REGION = "-75.2736908751,10.2543646415,-74.7146900698,11.1024970837"
|
|
11
|
+
client = FIRMSClient(MAP_KEY, REGION)
|
|
12
|
+
```
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "firms-sdk"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Basic SDK Client to retrive hotspots data from FIRMS API"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Sebástian Andrés Narváez Salcedo",email = "sebnarvaez19@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.14"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pandas (>=3.0.0,<4.0.0)",
|
|
12
|
+
"requests (>=2.32.5,<3.0.0)"
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.poetry]
|
|
16
|
+
packages = [{include = "firms_sdk", from = "src"}]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
20
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from urllib.error import HTTPError, URLError
|
|
3
|
+
|
|
4
|
+
from pandas import DataFrame, Series, concat, read_csv
|
|
5
|
+
from requests import get
|
|
6
|
+
from requests.exceptions import RequestException
|
|
7
|
+
|
|
8
|
+
from src.firms_sdk.utils import SensorType, format_table, datetime_and_ranges_in_year
|
|
9
|
+
from src.firms_sdk.exceptions import FetchException, check_sensor, check_day_range
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Utils:
|
|
13
|
+
"""Utilities for data fetching."""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
self.datetime_and_ranges_in_year = datetime_and_ranges_in_year
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FIRMSClient:
|
|
20
|
+
"""FIRMS Client generator to fetch hotspots data from all sensors."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, map_key: str, region: str = "world", region_name: str | None = None):
|
|
23
|
+
self.base_url = "https://firms.modaps.eosdis.nasa.gov/"
|
|
24
|
+
self.map_key = map_key
|
|
25
|
+
self.region = region
|
|
26
|
+
self.instance_date = datetime.now()
|
|
27
|
+
self.region_name = region_name
|
|
28
|
+
self.utils = Utils()
|
|
29
|
+
|
|
30
|
+
def __str__(self):
|
|
31
|
+
return f"FIRMS Client initiated at {self.instance_date}\
|
|
32
|
+
\nRegion: {self.region_name + " - " if self.region_name else ""}[{self.region}]"
|
|
33
|
+
|
|
34
|
+
def __repr__(self):
|
|
35
|
+
return self.__str__()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def status(self) -> Series:
|
|
39
|
+
"""Fetch MAP KEY status info.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
FetchException: unable to reach server for data.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Series: transactions limit, transactions done, and refreshing time info
|
|
46
|
+
"""
|
|
47
|
+
try:
|
|
48
|
+
response = get(self.base_url + "mapserver/mapkey_status/?MAP_KEY=" + self.map_key, timeout=3600)
|
|
49
|
+
data = response.json()
|
|
50
|
+
s = Series(data)
|
|
51
|
+
s.name = "Status"
|
|
52
|
+
return s
|
|
53
|
+
except RequestException as e:
|
|
54
|
+
raise FetchException(e.args)
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def data_available(self) -> DataFrame:
|
|
58
|
+
"""Available sensors to fetch data.
|
|
59
|
+
|
|
60
|
+
Raises:
|
|
61
|
+
FetchException: unable to reach server for data.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
DataFrame: Sensors data.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
f_url = self.base_url + "api/data_availability/csv/" + self.map_key + "/all"
|
|
68
|
+
return read_csv(f_url)
|
|
69
|
+
except (HTTPError, URLError) as e:
|
|
70
|
+
raise FetchException(e.args)
|
|
71
|
+
|
|
72
|
+
def get_data(self, sensor: SensorType, day_range: int, date_init: datetime) -> DataFrame:
|
|
73
|
+
"""Fetch hotspots from sensor in the specified date range.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
sensor (SensorType): Sensor of interest.
|
|
77
|
+
day_range (int): Number of days in advance to search (between 1-5).
|
|
78
|
+
date_init (datetime): date of interest.
|
|
79
|
+
|
|
80
|
+
Raises:
|
|
81
|
+
FetchException: Data not available.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
DataFrame: hotspots dataframe.
|
|
85
|
+
"""
|
|
86
|
+
check_day_range(day_range)
|
|
87
|
+
check_sensor(sensor)
|
|
88
|
+
f_url = f"{self.base_url}api/area/csv/{self.map_key}/{sensor}/{self.region}/{day_range}/{date_init.strftime("%Y-%m-%d")}"
|
|
89
|
+
try:
|
|
90
|
+
df = read_csv(f_url, parse_dates=["acq_date"])
|
|
91
|
+
d, hh, mm = df["acq_date"], df["acq_time"] // 100, df["acq_time"] % 100
|
|
92
|
+
dt_vector = [datetime(d.year, d.month, d.day, hour, minute) for d, hour, minute in zip(d, hh, mm)]
|
|
93
|
+
df = df.drop(columns=["acq_date", "acq_time"])
|
|
94
|
+
df["datetime"] = dt_vector
|
|
95
|
+
return format_table(df)
|
|
96
|
+
except (HTTPError, URLError) as e:
|
|
97
|
+
raise FetchException(e.args)
|
|
98
|
+
|
|
99
|
+
def get_sp_data(self, day_range: int, date_init: datetime) -> DataFrame:
|
|
100
|
+
"""Fetch validate hotspots from FIRMS.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
day_range (int): Number of days in advance to search (between 1-5).
|
|
104
|
+
date_init (datetime): date of interest.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
DataFrame: hotspots dataframe.
|
|
108
|
+
"""
|
|
109
|
+
sensors: list[SensorType] = ["MODIS_SP", "VIIRS_NOAA20_SP", "VIIRS_SNPP_SP"]
|
|
110
|
+
dfs = [self.get_data(sensor, day_range, date_init) for sensor in sensors]
|
|
111
|
+
return concat(dfs, ignore_index=True)
|
|
112
|
+
|
|
113
|
+
def get_nrt_data(self, day_range: int, date_init: datetime) -> DataFrame:
|
|
114
|
+
"""Fetch near-real-time hotspots from FIRMS (no validated by NASA).
|
|
115
|
+
|
|
116
|
+
Args:
|
|
117
|
+
day_range (int): Number of days in advance to search (between 1-5).
|
|
118
|
+
date_init (datetime): date of interest.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
DataFrame: hotspots dataframe.
|
|
122
|
+
"""
|
|
123
|
+
sensors: list[SensorType] = ["MODIS_NRT", "VIIRS_NOAA20_NRT", "VIIRS_NOAA21_NRT", "VIIRS_SNPP_NRT"]
|
|
124
|
+
dfs = [self.get_data(sensor, day_range, date_init) for sensor in sensors]
|
|
125
|
+
return format_table(concat(dfs, ignore_index=True))
|
|
126
|
+
|
|
127
|
+
def get_all_data(self, day_range: int, date_init: datetime) -> DataFrame:
|
|
128
|
+
"""Fetch near-real-time and validated hotspots data.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
day_range (int): Number of days in advance to search (between 1-5).
|
|
132
|
+
date_init (datetime): date of interest.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
DataFrame: hotspots dataframe.
|
|
136
|
+
"""
|
|
137
|
+
return format_table(concat([self.get_sp_data(day_range, date_init), self.get_nrt_data(day_range, date_init)], ignore_index=True))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from src.firms_sdk.utils import SensorType
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FetchException(Exception):
|
|
7
|
+
"""Fetch Exception message."""
|
|
8
|
+
|
|
9
|
+
def __init__(self, *args):
|
|
10
|
+
super().__init__(*args)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class SensorException(FetchException):
|
|
14
|
+
"""No valid sensor error message."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, value: Any):
|
|
17
|
+
message = f"Sensor must be one between: \
|
|
18
|
+
'MODIS_NRT', 'MODIS_SP', 'VIIRS_NOAA20_NRT', 'VIIRS_NOAA20_SP', 'VIIRS_NOAA21_NRT', 'VIIRS_SNPP_NRT', 'VIIRS_SNPP_SP' \
|
|
19
|
+
, not {value}"
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class DayRangeException(FetchException):
|
|
24
|
+
"""No valid day range."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, value: Any):
|
|
27
|
+
message = f"Day range must be a value between 1 or 5 days in advance, not {value}"
|
|
28
|
+
super().__init__(message)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_sensor(sensor: SensorType):
|
|
32
|
+
"""Validate sensor input.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
sensor (SensorType): sensor to validate.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
TypeError: Sensor is not string.
|
|
39
|
+
SensorException: Sensor is not one of available.
|
|
40
|
+
"""
|
|
41
|
+
available_sensors = [
|
|
42
|
+
"MODIS_NRT",
|
|
43
|
+
"MODIS_SP",
|
|
44
|
+
"VIIRS_NOAA20_NRT",
|
|
45
|
+
"VIIRS_NOAA20_SP",
|
|
46
|
+
"VIIRS_NOAA21_NRT",
|
|
47
|
+
"VIIRS_SNPP_NRT",
|
|
48
|
+
"VIIRS_SNPP_SP",
|
|
49
|
+
]
|
|
50
|
+
if not isinstance(sensor, str):
|
|
51
|
+
raise TypeError(f"Sensor must be string type, not {type(sensor)}")
|
|
52
|
+
if sensor not in available_sensors:
|
|
53
|
+
raise SensorException(sensor)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def check_day_range(day_range: int):
|
|
57
|
+
"""Validate day_range input.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
day_range (int): day range to validate.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TypeError: Day range is not int.
|
|
64
|
+
DayRangeException: Day range is not between 1-5 range.
|
|
65
|
+
"""
|
|
66
|
+
if not isinstance(day_range, int):
|
|
67
|
+
raise TypeError(f"Day range must be integer, not {type(day_range)}")
|
|
68
|
+
if day_range <= 0 or day_range > 5:
|
|
69
|
+
raise DayRangeException(day_range)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from typing import Literal
|
|
3
|
+
import numpy as np
|
|
4
|
+
from pandas import DataFrame, StringDtype
|
|
5
|
+
|
|
6
|
+
SensorType = Literal[
|
|
7
|
+
"MODIS_NRT",
|
|
8
|
+
"MODIS_SP",
|
|
9
|
+
"VIIRS_NOAA20_NRT",
|
|
10
|
+
"VIIRS_NOAA20_SP",
|
|
11
|
+
"VIIRS_NOAA21_NRT",
|
|
12
|
+
"VIIRS_SNPP_NRT",
|
|
13
|
+
"VIIRS_SNPP_SP",
|
|
14
|
+
"all",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_table(df: DataFrame) -> DataFrame:
|
|
19
|
+
"""Format FIRMS table for all available options.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
df (DataFrame): base dataframe.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
(DataFrame): dataframe processed.
|
|
26
|
+
"""
|
|
27
|
+
type_map = {
|
|
28
|
+
"latitude": np.dtype("float64"),
|
|
29
|
+
"longitude": np.dtype("float64"),
|
|
30
|
+
"datetime": np.dtype("datetime64[us]"),
|
|
31
|
+
"scan": np.dtype("float64"),
|
|
32
|
+
"track": np.dtype("float64"),
|
|
33
|
+
"satellite": StringDtype(),
|
|
34
|
+
"instrument": StringDtype(),
|
|
35
|
+
"type": np.dtype("int64"),
|
|
36
|
+
"version": np.dtype("float64"),
|
|
37
|
+
"confidence": StringDtype(),
|
|
38
|
+
"brightness": np.dtype("float64"),
|
|
39
|
+
"bright_t31": np.dtype("float64"),
|
|
40
|
+
"bright_ti4": np.dtype("float64"),
|
|
41
|
+
"bright_ti5": np.dtype("float64"),
|
|
42
|
+
}
|
|
43
|
+
for key, value in type_map.items():
|
|
44
|
+
try:
|
|
45
|
+
df[key] = df[key].astype(value)
|
|
46
|
+
except KeyError:
|
|
47
|
+
df[key] = None
|
|
48
|
+
df[key] = df[key].astype(value)
|
|
49
|
+
return df[type_map.keys()]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def datetime_and_ranges_in_year(year: int) -> list[tuple[datetime, int]]:
|
|
53
|
+
"""Get datetime and day_ranges pairs list for the specified year.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
year (int): year of interest.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
list[tuple[datetime, int]]: list of datetime-day_range pairs.
|
|
60
|
+
"""
|
|
61
|
+
first_date = datetime(year, 1, 1)
|
|
62
|
+
final_date = datetime(year, 12, 31)
|
|
63
|
+
dates = [(first_date, 5)]
|
|
64
|
+
while True:
|
|
65
|
+
current_date = dates[len(dates) - 1][0]
|
|
66
|
+
current_day_range = dates[len(dates) - 1][1]
|
|
67
|
+
if current_date + timedelta(days=current_day_range) >= final_date:
|
|
68
|
+
break
|
|
69
|
+
next_date = current_date + timedelta(days=current_day_range)
|
|
70
|
+
to_final_date = final_date - next_date
|
|
71
|
+
if to_final_date.days > 5:
|
|
72
|
+
dates.append((next_date, 5))
|
|
73
|
+
else:
|
|
74
|
+
dates.append((next_date, to_final_date.days))
|
|
75
|
+
return dates
|