atmoswing-api 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.
app/__init__.py ADDED
File without changes
app/main.py ADDED
@@ -0,0 +1,23 @@
1
+ import logging
2
+ from fastapi import FastAPI
3
+ from app.routes import meta, forecasts, aggregations
4
+
5
+ # Configure logging
6
+ logging.basicConfig(
7
+ level=logging.INFO,
8
+ format="%(asctime)s -%(levelname)s - %(message)s",
9
+ handlers=[
10
+ logging.StreamHandler()
11
+ ]
12
+ )
13
+
14
+ app = FastAPI(
15
+ title="AtmoSwing Web Forecast API",
16
+ description="API to provide forecasts generated by AtmoSwing.",
17
+ version="1.0.0",
18
+ )
19
+
20
+ # Include the routes
21
+ app.include_router(meta.router, prefix="/meta", tags=["Metadata"])
22
+ app.include_router(forecasts.router, prefix="/forecasts", tags=["Data from a single forecast"])
23
+ app.include_router(aggregations.router, prefix="/aggregations", tags=["Aggregated forecast data"])
app/models/__init__.py ADDED
File without changes
app/models/forecast.py ADDED
@@ -0,0 +1,120 @@
1
+ from pydantic import BaseModel, field_validator
2
+ from typing import List
3
+ from datetime import datetime
4
+
5
+
6
+ class ReferenceValues(BaseModel):
7
+ axis: List[float]
8
+ values: List[float]
9
+
10
+ @field_validator("axis")
11
+ def round_axis(cls, v: List[float]) -> List[float]:
12
+ return [round(float(value), 2) for value in v]
13
+
14
+ @field_validator("values")
15
+ def round_values(cls, v: List[float]) -> List[float]:
16
+ return [round(float(value), 2) for value in v]
17
+
18
+
19
+ class Analog(BaseModel):
20
+ date: datetime
21
+ value: float
22
+ criteria: float
23
+ rank: int
24
+
25
+ @field_validator("value")
26
+ def round_value(cls, v: float) -> float:
27
+ return round(float(v), 2)
28
+
29
+ @field_validator("criteria")
30
+ def round_criteria(cls, v: float) -> float:
31
+ return round(float(v), 3)
32
+
33
+
34
+ class Analogs(BaseModel):
35
+ analogs: List[Analog]
36
+
37
+
38
+ class AnalogValues(BaseModel):
39
+ values: List[float]
40
+
41
+ @field_validator("values")
42
+ def round_values(cls, v: List[float]) -> List[float]:
43
+ return [round(float(value), 2) for value in v]
44
+
45
+
46
+ class AnalogValuesPercentiles(BaseModel):
47
+ percentiles: List[int]
48
+ values: List[float]
49
+
50
+ @field_validator("values")
51
+ def round_values(cls, v: List[float]) -> List[float]:
52
+ return [round(float(value), 2) for value in v]
53
+
54
+
55
+ class AnalogDates(BaseModel):
56
+ dates: List[datetime]
57
+
58
+
59
+ class AnalogCriteria(BaseModel):
60
+ criteria: List[float]
61
+
62
+ @field_validator("criteria")
63
+ def round_criteria(cls, v: List[float]) -> List[float]:
64
+ return [round(float(value), 3) for value in v]
65
+
66
+
67
+ class SeriesAnalogValues(BaseModel):
68
+ series_values: List[List[float]]
69
+
70
+ @field_validator("series_values")
71
+ def round_series(cls, v: List[List[float]]) -> List[List[float]]:
72
+ return [[round(float(value), 2) for value in series] for series in v]
73
+
74
+
75
+ class SeriesAnalogValuesPercentile(BaseModel):
76
+ percentile: int
77
+ series_values: List[float]
78
+
79
+ @field_validator("series_values")
80
+ def round_series(cls, v: List[float]) -> List[float]:
81
+ return [round(float(value), 2) for value in v]
82
+
83
+
84
+ class SeriesAnalogValuesPercentiles(BaseModel):
85
+ forecast_date: datetime
86
+ target_dates: List[datetime]
87
+ series_percentiles: List[SeriesAnalogValuesPercentile]
88
+
89
+
90
+ class SeriesAnalogValuesPercentilesHistory(BaseModel):
91
+ past_forecasts: List[SeriesAnalogValuesPercentiles]
92
+
93
+
94
+ class EntitiesAnalogValuesPercentile(BaseModel):
95
+ entity_ids: List[int]
96
+ values: List[float]
97
+
98
+ @field_validator("values")
99
+ def round_values(cls, v: List[float]) -> List[float]:
100
+ return [round(float(value), 2) for value in v]
101
+
102
+
103
+ class SeriesSynthesisPerMethod(BaseModel):
104
+ method_id: str
105
+ target_dates: List[datetime]
106
+ values: List[float]
107
+
108
+ @field_validator("values")
109
+ def round_values(cls, v: List[float]) -> List[float]:
110
+ return [round(float(value), 2) for value in v]
111
+
112
+
113
+ class SeriesSynthesisTotal(BaseModel):
114
+ time_step: int
115
+ target_dates: List[datetime]
116
+ values: List[float]
117
+
118
+ @field_validator("values")
119
+ def round_values(cls, v: List[float]) -> List[float]:
120
+ return [round(float(value), 2) for value in v]
app/models/meta.py ADDED
@@ -0,0 +1,23 @@
1
+ from pydantic import BaseModel
2
+ from typing import List
3
+ from typing import Optional
4
+
5
+ class Method(BaseModel):
6
+ id: str
7
+ name: str
8
+
9
+ class Configuration(BaseModel):
10
+ id: str
11
+ name: str
12
+
13
+ class MethodConfig(BaseModel):
14
+ id: str
15
+ name: str
16
+ configurations: List[Configuration]
17
+
18
+ class Entity(BaseModel):
19
+ id: int
20
+ name: str
21
+ x: float
22
+ y: float
23
+ official_id: Optional[str] = None
app/routes/__init__.py ADDED
File without changes
@@ -0,0 +1,89 @@
1
+ import logging
2
+ from typing import List
3
+ from functools import lru_cache
4
+ from fastapi import APIRouter, HTTPException, Depends, Query
5
+ from typing_extensions import Annotated
6
+
7
+ import config
8
+ from app.models.forecast import *
9
+ from app.services.aggregations import *
10
+
11
+ router = APIRouter()
12
+ debug = False
13
+
14
+
15
+ @lru_cache
16
+ def get_settings():
17
+ return config.Settings()
18
+
19
+
20
+ # Helper function to handle requests and catch exceptions
21
+ async def _handle_request(func, settings: config.Settings, region: str, **kwargs):
22
+ try:
23
+ result = await func(settings.data_dir, region, **kwargs)
24
+ if debug:
25
+ logging.info(f"Result from {func.__name__}: {result}")
26
+ if result is None:
27
+ raise ValueError("The result is None")
28
+ return result
29
+ except FileNotFoundError:
30
+ logging.error(f"Files not found for region: {region}")
31
+ raise HTTPException(status_code=404, detail="Region or forecast not found")
32
+ except Exception as e:
33
+ logging.error(f"An unexpected error occurred: {e}")
34
+ raise HTTPException(status_code=500, detail="Internal Server Error")
35
+
36
+
37
+ @router.get("/{region}/{forecast_date}/{method}/{lead_time}/analog-values-percentile/{percentile}",
38
+ summary="Analog values for a given region, forecast_date, method, "
39
+ "lead time, and percentile, aggregated by selecting the "
40
+ "relevant configuration per entity",
41
+ response_model=EntitiesAnalogValuesPercentile)
42
+ async def entities_analog_values_percentile(
43
+ region: str,
44
+ forecast_date: str,
45
+ method: str,
46
+ lead_time: int|str,
47
+ percentile: int,
48
+ settings: Annotated[config.Settings, Depends(get_settings)]):
49
+ """
50
+ Get the analog dates for a given region, forecast_date, method, configuration, and lead_time.
51
+ """
52
+ return await _handle_request(get_entities_analog_values_percentile, settings,
53
+ region, forecast_date=forecast_date, method=method,
54
+ lead_time=lead_time, percentile=percentile)
55
+
56
+
57
+ @router.get("/{region}/{forecast_date}/series-synthesis-per-method/{percentile}",
58
+ summary="Largest values for a given region, forecast_date, method, "
59
+ "and percentile, aggregated by selecting the largest values for "
60
+ "the relevant configurations per entity",
61
+ response_model=List[SeriesSynthesisPerMethod])
62
+ async def series_synthesis_per_method(
63
+ region: str,
64
+ forecast_date: str,
65
+ percentile: int,
66
+ settings: Annotated[config.Settings, Depends(get_settings)]):
67
+ """
68
+ Get the largest analog values for a given region, forecast_date, and percentile.
69
+ """
70
+ return await _handle_request(get_series_synthesis_per_method, settings,
71
+ region, forecast_date=forecast_date,
72
+ percentile=percentile)
73
+
74
+
75
+ @router.get("/{region}/{forecast_date}/series-synthesis-total/{percentile}",
76
+ summary="Largest values for a given region, forecast_date, "
77
+ "and percentile, aggregated by time steps",
78
+ response_model=List[SeriesSynthesisTotal])
79
+ async def series_synthesis_total(
80
+ region: str,
81
+ forecast_date: str,
82
+ percentile: int,
83
+ settings: Annotated[config.Settings, Depends(get_settings)]):
84
+ """
85
+ Get the largest analog values for a given region, forecast_date, and percentile.
86
+ """
87
+ return await _handle_request(get_series_synthesis_total, settings,
88
+ region, forecast_date=forecast_date,
89
+ percentile=percentile)
@@ -0,0 +1,251 @@
1
+ import logging
2
+ from typing import List
3
+ from functools import lru_cache
4
+ from fastapi import APIRouter, HTTPException, Depends, Query
5
+ from typing_extensions import Annotated
6
+
7
+ import config
8
+ from app.models.forecast import *
9
+ from app.services.forecasts import *
10
+
11
+ router = APIRouter()
12
+ debug = False
13
+
14
+
15
+ @lru_cache
16
+ def get_settings():
17
+ return config.Settings()
18
+
19
+
20
+ # Helper function to handle requests and catch exceptions
21
+ async def _handle_request(func, settings: config.Settings, region: str, **kwargs):
22
+ try:
23
+ result = await func(settings.data_dir, region, **kwargs)
24
+ if debug:
25
+ logging.info(f"Result from {func.__name__}: {result}")
26
+ if result is None:
27
+ raise ValueError("The result is None")
28
+ return result
29
+ except FileNotFoundError:
30
+ logging.error(f"Files not found for region: {region}")
31
+ raise HTTPException(status_code=404, detail="Region or forecast not found")
32
+ except Exception as e:
33
+ logging.error(f"An unexpected error occurred: {e}")
34
+ raise HTTPException(status_code=500, detail="Internal Server Error")
35
+
36
+
37
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{lead_time}/analog-dates",
38
+ summary="Analog dates for a given forecast and target date",
39
+ response_model=AnalogDates)
40
+ async def analog_dates(
41
+ region: str,
42
+ forecast_date: str,
43
+ method: str,
44
+ configuration: str,
45
+ lead_time: int|str,
46
+ settings: Annotated[config.Settings, Depends(get_settings)]):
47
+ """
48
+ Get the analog dates for a given region, forecast date, method, configuration, and lead time.
49
+ """
50
+ return await _handle_request(get_analog_dates, settings, region,
51
+ forecast_date=forecast_date, method=method,
52
+ configuration=configuration, lead_time=lead_time)
53
+
54
+
55
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{lead_time}/analogy-criteria",
56
+ summary="Analog criteria for a given forecast and target date",
57
+ response_model=AnalogCriteria)
58
+ async def analog_criteria(
59
+ region: str,
60
+ forecast_date: str,
61
+ method: str,
62
+ configuration: str,
63
+ lead_time: int|str,
64
+ settings: Annotated[config.Settings, Depends(get_settings)]):
65
+ """
66
+ Get the analog criteria for a given region, forecast date, method, configuration, and lead time.
67
+ """
68
+ return await _handle_request(get_analog_criteria, settings, region,
69
+ forecast_date=forecast_date, method=method,
70
+ configuration=configuration, lead_time=lead_time)
71
+
72
+
73
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{lead_time}/entities-values-percentile/{percentile}",
74
+ summary="Values for all entities for a given quantile, forecast and target date",
75
+ response_model=EntitiesAnalogValuesPercentile)
76
+ async def entities_analog_values_percentile(
77
+ region: str,
78
+ forecast_date: str,
79
+ method: str,
80
+ configuration: str,
81
+ lead_time: int|str,
82
+ percentile: int,
83
+ settings: Annotated[config.Settings, Depends(get_settings)]):
84
+ """
85
+ Get the precipitation values for a given region, forecast date, method, configuration, lead time, and percentile.
86
+ """
87
+ return await _handle_request(get_entities_analog_values_percentile, settings, region,
88
+ forecast_date=forecast_date, method=method,
89
+ configuration=configuration, lead_time=lead_time,
90
+ percentile=percentile)
91
+
92
+
93
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/reference-values",
94
+ summary="Reference values (e.g. for different return periods) for a given entity",
95
+ response_model=ReferenceValues)
96
+ async def reference_values(
97
+ region: str,
98
+ forecast_date: str,
99
+ method: str,
100
+ configuration: str,
101
+ entity: int,
102
+ settings: Annotated[config.Settings, Depends(get_settings)]):
103
+ """
104
+ Get the reference values for a given region, forecast date, method, configuration, and entity.
105
+ """
106
+ return await _handle_request(get_reference_values, settings, region,
107
+ forecast_date=forecast_date, method=method,
108
+ configuration=configuration, entity=entity)
109
+
110
+
111
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/series-values-best-analogs",
112
+ summary="Analog values of the best analogs for a given entity (time series)",
113
+ response_model=SeriesAnalogValues)
114
+ async def series_analog_values_best(
115
+ region: str,
116
+ forecast_date: str,
117
+ method: str,
118
+ configuration: str,
119
+ entity: int,
120
+ settings: Annotated[config.Settings, Depends(get_settings)],
121
+ number: int = 10):
122
+ """
123
+ Get the precipitation values for the best analogs and for a given region, forecast date, method, configuration, and entity.
124
+ """
125
+ return await _handle_request(get_series_analog_values_best, settings, region,
126
+ forecast_date=forecast_date, method=method,
127
+ configuration=configuration, entity=entity,
128
+ number=number)
129
+
130
+
131
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/series-values-percentiles",
132
+ summary="Values for one entity for a given quantile, forecast and target date",
133
+ response_model=SeriesAnalogValuesPercentiles)
134
+ async def series_analog_values_percentiles(
135
+ region: str,
136
+ forecast_date: str,
137
+ method: str,
138
+ configuration: str,
139
+ entity: int,
140
+ settings: Annotated[config.Settings, Depends(get_settings)],
141
+ percentiles: List[int] = Query([20, 60, 90])):
142
+ """
143
+ Get the precipitation values for the provided percentiles and for a given region, forecast date, method, configuration, and entity.
144
+ """
145
+ return await _handle_request(get_series_analog_values_percentiles, settings, region,
146
+ forecast_date=forecast_date, method=method,
147
+ configuration=configuration, entity=entity,
148
+ percentiles=percentiles)
149
+
150
+
151
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/series-values-percentiles-history",
152
+ summary="Values for one entity for a given quantile, forecast and target date",
153
+ response_model=SeriesAnalogValuesPercentilesHistory)
154
+ async def series_analog_values_percentiles_history(
155
+ region: str,
156
+ forecast_date: str,
157
+ method: str,
158
+ configuration: str,
159
+ entity: int,
160
+ settings: Annotated[config.Settings, Depends(get_settings)],
161
+ percentiles: List[int] = Query([20, 60, 90]),
162
+ number: int = 5):
163
+ """
164
+ Get the precipitation values for the provided percentiles and for a given region, forecast date, method, configuration, and entity.
165
+ """
166
+ return await _handle_request(get_series_analog_values_percentiles_history, settings,
167
+ region, forecast_date=forecast_date, method=method,
168
+ configuration=configuration, entity=entity,
169
+ percentiles=percentiles, number=number)
170
+
171
+
172
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/{lead_time}/analogs",
173
+ summary="Details of the analogs (rank, date, criteria, value) for a given forecast and entity",
174
+ response_model=Analogs)
175
+ async def analogs(
176
+ region: str,
177
+ forecast_date: str,
178
+ method: str,
179
+ configuration: str,
180
+ entity: int,
181
+ lead_time: int|str,
182
+ settings: Annotated[config.Settings, Depends(get_settings)]):
183
+ """
184
+ Get the analogs for a given region, forecast date, method, configuration, entity, and lead time.
185
+ """
186
+ return await _handle_request(get_analogs, settings, region,
187
+ forecast_date=forecast_date, method=method,
188
+ configuration=configuration, entity=entity,
189
+ lead_time=lead_time)
190
+
191
+
192
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/{lead_time}/analog-values",
193
+ summary="Analog values for a given entity and target date",
194
+ response_model=AnalogValues)
195
+ async def analog_values(
196
+ region: str,
197
+ forecast_date: str,
198
+ method: str,
199
+ configuration: str,
200
+ entity: int,
201
+ lead_time: int|str,
202
+ settings: Annotated[config.Settings, Depends(get_settings)]):
203
+ """
204
+ Get the precipitation values for a given region, forecast date, method, configuration, entity, lead time.
205
+ """
206
+ return await _handle_request(get_analog_values, settings, region,
207
+ forecast_date=forecast_date, method=method,
208
+ configuration=configuration, entity=entity,
209
+ lead_time=lead_time)
210
+
211
+
212
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/{lead_time}/analog-values-percentiles",
213
+ summary="Values for one entity for a given quantile, forecast and target date",
214
+ response_model=AnalogValuesPercentiles)
215
+ async def analog_values_percentiles(
216
+ region: str,
217
+ forecast_date: str,
218
+ method: str,
219
+ configuration: str,
220
+ entity: int,
221
+ lead_time: int|str,
222
+ settings: Annotated[config.Settings, Depends(get_settings)],
223
+ percentiles: List[int] = Query([20, 60, 90])):
224
+ """
225
+ Get the precipitation values for a given region, forecast date, method, configuration, entity, lead time, and percentile.
226
+ """
227
+ return await _handle_request(get_analog_values_percentiles, settings, region,
228
+ forecast_date=forecast_date, method=method,
229
+ configuration=configuration, entity=entity,
230
+ lead_time=lead_time, percentiles=percentiles)
231
+
232
+
233
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/{entity}/{lead_time}/analog-values-best",
234
+ summary="Values for one entity for a given quantile, forecast and target date",
235
+ response_model=AnalogValues)
236
+ async def analog_values_best(
237
+ region: str,
238
+ forecast_date: str,
239
+ method: str,
240
+ configuration: str,
241
+ entity: int,
242
+ lead_time: int|str,
243
+ settings: Annotated[config.Settings, Depends(get_settings)],
244
+ number: int = 10):
245
+ """
246
+ Get the precipitation values for the best analogs and for a given region, forecast date, method, configuration, entity, and lead time.
247
+ """
248
+ return await _handle_request(get_analog_values_best, settings, region,
249
+ forecast_date=forecast_date, method=method,
250
+ configuration=configuration, entity=entity,
251
+ lead_time=lead_time, number=number)
app/routes/meta.py ADDED
@@ -0,0 +1,85 @@
1
+ import logging
2
+ from functools import lru_cache
3
+ from fastapi import APIRouter, HTTPException, Depends
4
+ from typing_extensions import Annotated
5
+ from typing import List
6
+
7
+ import config
8
+ from app.services.meta import get_last_forecast_date_from_files, \
9
+ get_method_list, get_method_configs_list, get_entities_list
10
+ from app.models.meta import Entity, Method, MethodConfig
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ @lru_cache
16
+ def get_settings():
17
+ return config.Settings()
18
+
19
+
20
+ # Helper function to handle requests and catch exceptions
21
+ async def _handle_request(func, settings: config.Settings, region: str, **kwargs):
22
+ try:
23
+ return await func(settings.data_dir, region, **kwargs)
24
+ except FileNotFoundError:
25
+ logging.error(f"Files not found for region: {region}")
26
+ raise HTTPException(status_code=404, detail="Region or forecast not found")
27
+ except Exception as e:
28
+ logging.error(f"An unexpected error occurred: {e}")
29
+ raise HTTPException(status_code=500, detail="Internal Server Error")
30
+
31
+
32
+ @router.get("/{region}/last-forecast-date",
33
+ summary="Last available forecast date")
34
+ async def get_last_forecast_date(
35
+ region: str,
36
+ settings: Annotated[config.Settings, Depends(get_settings)]):
37
+ """
38
+ Get the last available forecast date for a given region.
39
+ """
40
+ return await _handle_request(get_last_forecast_date_from_files, settings, region)
41
+
42
+
43
+ @router.get("/{region}/{forecast_date}/methods",
44
+ summary="List of available methods",
45
+ response_model=List[Method])
46
+ async def list_methods(
47
+ region: str,
48
+ forecast_date: str,
49
+ settings: Annotated[config.Settings, Depends(get_settings)]):
50
+ """
51
+ Get the list of available methods for a given region.
52
+ """
53
+ return await _handle_request(get_method_list, settings, region,
54
+ forecast_date=forecast_date)
55
+
56
+
57
+ @router.get("/{region}/{forecast_date}/methods-and-configs",
58
+ summary="List of available methods and configurations",
59
+ response_model=List[MethodConfig])
60
+ async def list_methods_and_configs(
61
+ region: str,
62
+ forecast_date: str,
63
+ settings: Annotated[config.Settings, Depends(get_settings)]):
64
+ """
65
+ Get the list of available methods and configs for a given region.
66
+ """
67
+ return await _handle_request(get_method_configs_list, settings, region,
68
+ forecast_date=forecast_date)
69
+
70
+
71
+ @router.get("/{region}/{forecast_date}/{method}/{configuration}/entities",
72
+ summary="List of available entities",
73
+ response_model=List[Entity])
74
+ async def list_entities(
75
+ region: str,
76
+ forecast_date: str,
77
+ method: str,
78
+ configuration: str,
79
+ settings: Annotated[config.Settings, Depends(get_settings)]):
80
+ """
81
+ Get the list of available entities for a given region, forecast_date, method, and configuration.
82
+ """
83
+ return await _handle_request(get_entities_list, settings, region,
84
+ forecast_date=forecast_date, method=method,
85
+ configuration=configuration)
File without changes