farmwise-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.
- farmwise_api/__init__.py +12 -0
- farmwise_api/_vendor/__init__.py +1 -0
- farmwise_api/_vendor/hubeaupyutils/LICENSE +21 -0
- farmwise_api/_vendor/hubeaupyutils/__init__.py +37 -0
- farmwise_api/_vendor/hubeaupyutils/hubeau.py +582 -0
- farmwise_api/_vendor/hubeaupyutils/wrappers.py +292 -0
- farmwise_api/adapters/API_readers/CHMI_Meteo/CHMI_meteo.py +289 -0
- farmwise_api/adapters/API_readers/CHMI_Meteo/__init__.py +1 -0
- farmwise_api/adapters/API_readers/ESDAC/ESDAC_mappings/ESDAC_mappings.py +11 -0
- farmwise_api/adapters/API_readers/ESDAC/ESDAC_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/ESDAC/ESDAC_wrapper.py +1 -0
- farmwise_api/adapters/API_readers/ESDAC/__init__.py +1 -0
- farmwise_api/adapters/API_readers/EuroCropV2/EuroCropV2_read.py +90 -0
- farmwise_api/adapters/API_readers/EuroCropV2/__init__.py +1 -0
- farmwise_api/adapters/API_readers/EuroCropV2/mappings/EuroCropV2_mappings.py +6 -0
- farmwise_api/adapters/API_readers/EuroCropV2/mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/EuroCropV2/utils/__init__.py +1 -0
- farmwise_api/adapters/API_readers/EuroCropV2/utils/extractors.py +101 -0
- farmwise_api/adapters/API_readers/EuroCropV2/utils/preparation.py +142 -0
- farmwise_api/adapters/API_readers/IFSGRID/IFSGRID_read.py +67 -0
- farmwise_api/adapters/API_readers/IFSGRID/__init__.py +1 -0
- farmwise_api/adapters/API_readers/IFSGRID/mappings/IFSGRID_mappings.py +116 -0
- farmwise_api/adapters/API_readers/IFSGRID/mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/IFSGRID/utils/__init__.py +1 -0
- farmwise_api/adapters/API_readers/IFSGRID/utils/definitions.json +371 -0
- farmwise_api/adapters/API_readers/IFSGRID/utils/extraction.py +138 -0
- farmwise_api/adapters/API_readers/IFSGRID/utils/preparation.py +116 -0
- farmwise_api/adapters/API_readers/UA_sw_quality/UA_sw_quality_mappings/UA_sw_quality_mapping.py +38 -0
- farmwise_api/adapters/API_readers/UA_sw_quality/UA_sw_quality_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/UA_sw_quality/__init__.py +1 -0
- farmwise_api/adapters/API_readers/UA_sw_quality/ukrainian_surface_water.py +142 -0
- farmwise_api/adapters/API_readers/__init__.py +1 -0
- farmwise_api/adapters/API_readers/cds/__init__.py +1 -0
- farmwise_api/adapters/API_readers/cds/cds_land_cover.py +46 -0
- farmwise_api/adapters/API_readers/cds/cds_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/cds/cds_mappings/cds_land_cover_mapping.py +1 -0
- farmwise_api/adapters/API_readers/cds/cds_mappings/cds_single_levels_mapping.py +44 -0
- farmwise_api/adapters/API_readers/cds/cds_single_levels.py +220 -0
- farmwise_api/adapters/API_readers/cds/cds_utils/__init__.py +1 -0
- farmwise_api/adapters/API_readers/cds/cds_utils/cds_data_read.py +128 -0
- farmwise_api/adapters/API_readers/corine/__init__.py +1 -0
- farmwise_api/adapters/API_readers/corine/corine_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/corine/corine_mappings/corine_mapping.py +130 -0
- farmwise_api/adapters/API_readers/corine/corine_read.py +226 -0
- farmwise_api/adapters/API_readers/correctiv/__init__.py +1 -0
- farmwise_api/adapters/API_readers/correctiv/correctiv_read.py +97 -0
- farmwise_api/adapters/API_readers/correctiv/mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/correctiv/mappings/correctiv_mappings.py +18 -0
- farmwise_api/adapters/API_readers/correctiv/utils/__init__.py +1 -0
- farmwise_api/adapters/API_readers/correctiv/utils/extractors.py +139 -0
- farmwise_api/adapters/API_readers/correctiv/utils/preparation.py +56 -0
- farmwise_api/adapters/API_readers/eea/__init__.py +1 -0
- farmwise_api/adapters/API_readers/eea/eea_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/eea/eea_mappings/eea_mappings.py +13 -0
- farmwise_api/adapters/API_readers/eea/eea_read.py +176 -0
- farmwise_api/adapters/API_readers/epa_ireland/__init__.py +1 -0
- farmwise_api/adapters/API_readers/epa_ireland/constants/EPA_coordinates.csv +89 -0
- farmwise_api/adapters/API_readers/epa_ireland/epa_gw.py +178 -0
- farmwise_api/adapters/API_readers/epa_ireland/epa_ireland_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/epa_ireland/epa_ireland_mappings/epa_ireland_mapping.py +12 -0
- farmwise_api/adapters/API_readers/geosphere/__init__.py +1 -0
- farmwise_api/adapters/API_readers/geosphere/geosphere.py +162 -0
- farmwise_api/adapters/API_readers/geosphere/geosphere_mapping/__init__.py +1 -0
- farmwise_api/adapters/API_readers/geosphere/geosphere_mapping/geosphere_mapping.py +12 -0
- farmwise_api/adapters/API_readers/gios/__init__.py +1 -0
- farmwise_api/adapters/API_readers/gios/constants/gios_coordinates.csv +217 -0
- farmwise_api/adapters/API_readers/gios/gios_api_stations.py +50 -0
- farmwise_api/adapters/API_readers/gios/gios_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/gios/gios_mappings/gios_mapping.py +264 -0
- farmwise_api/adapters/API_readers/gios/gios_scraper.py +224 -0
- farmwise_api/adapters/API_readers/gios/gios_utils.py +27 -0
- farmwise_api/adapters/API_readers/gios_gw/__init__.py +1 -0
- farmwise_api/adapters/API_readers/gios_gw/gios_gw.py +250 -0
- farmwise_api/adapters/API_readers/gios_gw/gios_gw_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/gios_gw/gios_gw_mappings/gios_gw_mapping.py +78 -0
- farmwise_api/adapters/API_readers/gios_gw/gios_gw_stations.py +98 -0
- farmwise_api/adapters/API_readers/hubeau/__init__.py +1 -0
- farmwise_api/adapters/API_readers/hubeau/constants/farmwise_gwquality_parameters.csv +37 -0
- farmwise_api/adapters/API_readers/hubeau/constants/farmwise_gwquality_points_sel_for_hubeau.csv +1877 -0
- farmwise_api/adapters/API_readers/hubeau/constants/farmwise_gwquantity_piezos_sel_for_hubeau.csv +2148 -0
- farmwise_api/adapters/API_readers/hubeau/constants/farmwise_sw_quality_stations_sel_for_hubeau.csv +5141 -0
- farmwise_api/adapters/API_readers/hubeau/get_piezo_stations.py +54 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/hubeau_mapping_piezo.py +11 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/hubeau_mapping_sw_quality.py +81 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/hubeau_mapping_wq.py +78 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_piezo_read_vbrgm.py +338 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_sw_quality_read.py +395 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_units.py +261 -0
- farmwise_api/adapters/API_readers/hubeau/hubeau_wq_read.py +383 -0
- farmwise_api/adapters/API_readers/imgw/__init__.py +1 -0
- farmwise_api/adapters/API_readers/imgw/imgw_api_stations.py +65 -0
- farmwise_api/adapters/API_readers/imgw/imgw_api_synop_daily.py +277 -0
- farmwise_api/adapters/API_readers/imgw/imgw_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/imgw/imgw_mappings/synop_mapping.py +73 -0
- farmwise_api/adapters/API_readers/imgw_hydro/__init__.py +1 -0
- farmwise_api/adapters/API_readers/imgw_hydro/imgw_api_hydro_daily.py +168 -0
- farmwise_api/adapters/API_readers/imgw_hydro/imgw_hydro_stations_api.py +45 -0
- farmwise_api/adapters/API_readers/imgw_hydro/imgw_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/imgw_hydro/imgw_mappings/imgw_hydro_mappings.py +25 -0
- farmwise_api/adapters/API_readers/irish_meteo/EPA_ireland_stations.csv +2084 -0
- farmwise_api/adapters/API_readers/irish_meteo/__init__.py +1 -0
- farmwise_api/adapters/API_readers/irish_meteo/irish_meteo_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/irish_meteo/irish_meteo_mappings/irish_meteo_mapping.py +9 -0
- farmwise_api/adapters/API_readers/irish_meteo/irish_ms_daily.py +159 -0
- farmwise_api/adapters/API_readers/quadica/DATA_SETUP.md +31 -0
- farmwise_api/adapters/API_readers/quadica/__init__.py +0 -0
- farmwise_api/adapters/API_readers/quadica/mappings/quadica_mappings.py +217 -0
- farmwise_api/adapters/API_readers/quadica/mappings/quadica_table.html +184 -0
- farmwise_api/adapters/API_readers/quadica/quadica_read.py +84 -0
- farmwise_api/adapters/API_readers/quadica/utils/__init__.py +0 -0
- farmwise_api/adapters/API_readers/quadica/utils/definition.py +19 -0
- farmwise_api/adapters/API_readers/quadica/utils/extractors.py +209 -0
- farmwise_api/adapters/API_readers/quadica/utils/preparation.py +110 -0
- farmwise_api/adapters/API_readers/soilgrids/__init__.py +1 -0
- farmwise_api/adapters/API_readers/soilgrids/soilgrids_call.py +113 -0
- farmwise_api/adapters/API_readers/soilgrids/soilgrids_mappings/__init__.py +1 -0
- farmwise_api/adapters/API_readers/soilgrids/soilgrids_mappings/soilgrids_mapping.py +57 -0
- farmwise_api/adapters/API_readers/wetterdienst/__init__.py +1 -0
- farmwise_api/adapters/API_readers/wetterdienst/wetterdienst_dwd.py +255 -0
- farmwise_api/adapters/API_readers/wetterdienst/wetterdienst_mapping/__init__.py +1 -0
- farmwise_api/adapters/API_readers/wetterdienst/wetterdienst_mapping/dwd_mapping.py +11 -0
- farmwise_api/adapters/__init__.py +1 -0
- farmwise_api/adapters/mappings/__init__.py +1 -0
- farmwise_api/adapters/mappings/data_source_mapping.py +310 -0
- farmwise_api/adapters/mappings/units.py +29 -0
- farmwise_api/cli.py +50 -0
- farmwise_api/core/__init__.py +1 -0
- farmwise_api/core/harmonization.py +336 -0
- farmwise_api/core/main_call.py +545 -0
- farmwise_api/core/quality_assess.py +376 -0
- farmwise_api/core/utils/__init__.py +1 -0
- farmwise_api/core/utils/access_policy.py +55 -0
- farmwise_api/core/utils/cells_to_coordinates.py +85 -0
- farmwise_api/core/utils/coordinates_to_cells.py +81 -0
- farmwise_api/core/utils/country_bboxes.py +25 -0
- farmwise_api/core/utils/country_codes +1502 -0
- farmwise_api/core/utils/data_manifest.py +47 -0
- farmwise_api/core/utils/data_operators.py +8 -0
- farmwise_api/core/utils/fetch_data.py +56 -0
- farmwise_api/core/utils/generate_cells.py +86 -0
- farmwise_api/core/utils/imgw_utils.py +63 -0
- farmwise_api/core/utils/interpolate_data.py +259 -0
- farmwise_api/core/utils/map_ploter.py +764 -0
- farmwise_api/core/utils/merge_bboxes.py +6 -0
- farmwise_api/core/utils/name_to_coordinates.py +44 -0
- farmwise_api/core/utils/overlap_checks.py +88 -0
- farmwise_api/core/utils/paths.py +292 -0
- farmwise_api/core/utils/proj_env.py +140 -0
- farmwise_api/core/utils/reproject_raster.py +35 -0
- farmwise_api/core/within_source_aggregation.py +151 -0
- farmwise_api/server/__init__.py +1 -0
- farmwise_api/server/api_utils.py +42 -0
- farmwise_api/server/crud.py +36 -0
- farmwise_api/server/db_users.py +62 -0
- farmwise_api/server/email_utils.py +69 -0
- farmwise_api/server/hashing_utils.py +11 -0
- farmwise_api/server/jobs.py +128 -0
- farmwise_api/server/logging_config.py +11 -0
- farmwise_api/server/main.py +71 -0
- farmwise_api/server/routers/__init__.py +1 -0
- farmwise_api/server/routers/auth.py +49 -0
- farmwise_api/server/routers/data_call.py +477 -0
- farmwise_api/server/routers/frontpage.py +597 -0
- farmwise_api/server/scheduler.py +16 -0
- farmwise_api/server/schemas.py +184 -0
- farmwise_api/server/security.py +106 -0
- farmwise_api/server/services.py +20 -0
- farmwise_api/server/sql_schemas.py +12 -0
- farmwise_api/server/static/farmwise.png +0 -0
- farmwise_api/server/static/farmwise_logo.png +0 -0
- farmwise_api/server/static/style.css +34 -0
- farmwise_api/server/static/upwr.png +0 -0
- farmwise_api/server/user_database.py +26 -0
- farmwise_api-0.1.0.dist-info/METADATA +465 -0
- farmwise_api-0.1.0.dist-info/RECORD +182 -0
- farmwise_api-0.1.0.dist-info/WHEEL +5 -0
- farmwise_api-0.1.0.dist-info/entry_points.txt +2 -0
- farmwise_api-0.1.0.dist-info/licenses/DATA_LICENSES.md +433 -0
- farmwise_api-0.1.0.dist-info/licenses/LICENSE +193 -0
- farmwise_api-0.1.0.dist-info/licenses/farmwise_api/_vendor/hubeaupyutils/LICENSE +21 -0
- farmwise_api-0.1.0.dist-info/top_level.txt +1 -0
farmwise_api/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Public Python API for FARMWISE."""
|
|
2
|
+
|
|
3
|
+
from farmwise_api.core.utils.proj_env import ensure_usable_proj_data
|
|
4
|
+
|
|
5
|
+
# Runs before anything imports GDAL, which resolves PROJ_LIB on first use.
|
|
6
|
+
# A no-op unless the host points PROJ at a database GDAL cannot read.
|
|
7
|
+
ensure_usable_proj_data()
|
|
8
|
+
|
|
9
|
+
from farmwise_api.core.main_call import read_data # noqa: E402
|
|
10
|
+
|
|
11
|
+
__all__ = ["read_data"]
|
|
12
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Third-party code bundled with FARMWISE under its original licences."""
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2024 Abel Henriot, Adrien Manlay, Thibault Hallouin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
from .hubeau import *
|
|
5
|
+
from .wrappers import *
|
|
6
|
+
|
|
7
|
+
__version__ = '0.1.0'
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
WRAPPERS = {
|
|
11
|
+
'piezometry': HubPiezometrie,
|
|
12
|
+
'hydrometry': HubHydrometrie,
|
|
13
|
+
'withdrawal': HubPrelevements,
|
|
14
|
+
'groundwater_qual': HubQualSout,
|
|
15
|
+
'river_qual' : HubQualRiv,
|
|
16
|
+
# 'river_temperature': HubTemperRiv,
|
|
17
|
+
# 'river_drought': HubOnde,
|
|
18
|
+
# 'drinking_quality': HubQualAEP,
|
|
19
|
+
# 'services': HubServices,
|
|
20
|
+
# 'coastal': HubLittoral,
|
|
21
|
+
# 'fish': HubFish,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def init_api(api='piezometry', version=1):
|
|
26
|
+
""" HubEau class accessor, instancies a wrapper class for any api
|
|
27
|
+
methods get_station(), get_data(), [get_data_tr()]
|
|
28
|
+
"""
|
|
29
|
+
# interface for instancing wrapper class. Simplier than Multiple Heritage
|
|
30
|
+
if not api.lower() in list(API.keys()):
|
|
31
|
+
raise ValueError(
|
|
32
|
+
'API `{}` is not available. Please use one of the following: {} or use HubeauUtils to construct your API request'.format(
|
|
33
|
+
api, ', '.join(list(API.keys()))
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
_api = WRAPPERS.get(api.lower())
|
|
37
|
+
return _api(version=version)
|
|
@@ -0,0 +1,582 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
HubeauUtils
|
|
6
|
+
|
|
7
|
+
Fonctions collection intended do:
|
|
8
|
+
- pipe parameters to proper format for requesting
|
|
9
|
+
- to fetch data from Hub'eau APIS
|
|
10
|
+
- pipe the data to pandas dataframe
|
|
11
|
+
- operate transformations on data
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import requests
|
|
16
|
+
import pandas as pd
|
|
17
|
+
|
|
18
|
+
from datetime import datetime, timedelta
|
|
19
|
+
from numpy import ndarray
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
DEFAULT_PARAMS = {
|
|
23
|
+
'size': 20_000,
|
|
24
|
+
'sort': 'asc',
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
RESPONSE_CODE = {
|
|
28
|
+
# Response code - For memory
|
|
29
|
+
200 : 'OK, all results fetched',
|
|
30
|
+
206 : 'OK, but results remains',
|
|
31
|
+
400 : 'Wrong request',
|
|
32
|
+
401 : 'Unauthorized',
|
|
33
|
+
403 : 'Forbidden',
|
|
34
|
+
404 : 'Not Found',
|
|
35
|
+
500 : 'Server internal error',
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
API = {
|
|
40
|
+
'piezometry': {
|
|
41
|
+
'endpoint': 'niveaux_nappes',
|
|
42
|
+
'operator_sta': 'stations',
|
|
43
|
+
'operator_obs': 'chroniques',
|
|
44
|
+
'operator_tr' : 'chroniques_tr'
|
|
45
|
+
},
|
|
46
|
+
'hydrometry': {
|
|
47
|
+
'endpoint': 'hydrometrie',
|
|
48
|
+
'operator_sta': 'referentiel/stations',
|
|
49
|
+
'operator_obs': 'obs_elab', # observationsElaborees
|
|
50
|
+
'operator_tr' : 'observations_tr'
|
|
51
|
+
},
|
|
52
|
+
'withdrawal': {
|
|
53
|
+
'endpoint' : 'prelevements',
|
|
54
|
+
'operator_sta' : 'referentiel/points_prelevement',
|
|
55
|
+
'operator_obs' : 'chroniques',
|
|
56
|
+
'operator_tr' : None,
|
|
57
|
+
},
|
|
58
|
+
'groundwater_qual': {
|
|
59
|
+
'endpoint': 'qualite_nappes',
|
|
60
|
+
'operator_sta' : 'stations',
|
|
61
|
+
'operator_obs' : 'analyses',
|
|
62
|
+
'operator_tr' : None,
|
|
63
|
+
},
|
|
64
|
+
'river_qual' : {
|
|
65
|
+
'endpoint': 'qualite_rivieres',
|
|
66
|
+
'operator_sta' : 'station_pc',
|
|
67
|
+
'operator_obs' : 'analyse_pc'
|
|
68
|
+
},
|
|
69
|
+
'river_temperature': {
|
|
70
|
+
'endpoint': 'temperature',
|
|
71
|
+
'operator_sta' : 'station',
|
|
72
|
+
'operator_obs' : 'chronique'
|
|
73
|
+
},
|
|
74
|
+
'river_drought': {
|
|
75
|
+
'endpoint': 'ecoulement', #réseau ONDE in french
|
|
76
|
+
'operator_sta' : 'stations',
|
|
77
|
+
'operator_obs' : 'observations'
|
|
78
|
+
# 'operator_tr' : 'campagnes' # not tr, add another ?
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def get_api_description(api='piezometry'):
|
|
84
|
+
if not api.lower() in list(API.keys()):
|
|
85
|
+
raise ValueError(
|
|
86
|
+
'API `{}` is not available. Please use one of the following: {}'.format(
|
|
87
|
+
api, ', '.join(list(API.keys()))
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
return API.get(api)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _check_parameters(**kwargs):
|
|
94
|
+
""" Filter/clean kwargs for requests parameters """
|
|
95
|
+
kwargs = {k: join_list(v) if isinstance(v, (list, tuple, pd.Series, ndarray)) else v for k, v in kwargs.items()} # list as str
|
|
96
|
+
kwargs['size'] = min(kwargs.get('size', 20_000), 20_000) # 20_000 is the max allowed
|
|
97
|
+
kwargs = {k: v for k, v in kwargs.items() if v} # remove False or None values
|
|
98
|
+
return kwargs
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def join_list(values, ascii=False):
|
|
102
|
+
if ascii:
|
|
103
|
+
return "%2C".join(map(str, values))
|
|
104
|
+
else:
|
|
105
|
+
return ",".join(map(str, values))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _session_timeout(url, sec=10):
|
|
109
|
+
""" block request after x seconds
|
|
110
|
+
not used now --> __future__
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
session = requests.get(url, timeout=sec)
|
|
114
|
+
except requests.exceptions.Timeout:
|
|
115
|
+
session.ok = False
|
|
116
|
+
return session
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# TODO change to _get_json_data from Thib ?
|
|
121
|
+
# simplier, and nice recursive implementation
|
|
122
|
+
def _get_json_data(url: str, data: list) -> list:
|
|
123
|
+
r = requests.get(url)
|
|
124
|
+
|
|
125
|
+
if r.status_code == 200:
|
|
126
|
+
data.extend(r.json()['data'])
|
|
127
|
+
return data
|
|
128
|
+
elif r.status_code == 206:
|
|
129
|
+
data.extend(r.json()['data'])
|
|
130
|
+
if r.json()['next']:
|
|
131
|
+
return _get_json_data(r.json()['next'], data)
|
|
132
|
+
else:
|
|
133
|
+
# catch edge case (bug?) where return code is 206 (i.e.
|
|
134
|
+
# remaining data to be fetched) but next URL is None (e.g.
|
|
135
|
+
# this is the case for https://hubeau.eaufrance.fr/api/
|
|
136
|
+
# v1/prelevements/referentiel/points_prelevement?
|
|
137
|
+
# code_departement=40&page=2&size=10000)
|
|
138
|
+
return data
|
|
139
|
+
else:
|
|
140
|
+
raise RuntimeError(
|
|
141
|
+
f"data retrieval failed for query with URL {url}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _curl_api(url, params={}, verbose=False):
|
|
147
|
+
"""
|
|
148
|
+
Get request from an url
|
|
149
|
+
using requests module and some parameters the API of an url is
|
|
150
|
+
requested. Data (answer) are added according to status of response.
|
|
151
|
+
|
|
152
|
+
Parameters
|
|
153
|
+
----------
|
|
154
|
+
url: str, the url to request
|
|
155
|
+
params: dict, parameters passed to `requests` method. Must match the API parameters of requested `url`.
|
|
156
|
+
verbose: bool, option to print details about errors.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
------
|
|
160
|
+
get: raw answer
|
|
161
|
+
data: the data fetched from API
|
|
162
|
+
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
answ = requests.get(url, params=params)
|
|
166
|
+
url_req = answ.url
|
|
167
|
+
|
|
168
|
+
if verbose:
|
|
169
|
+
print(f'Requesting: {url_req}')
|
|
170
|
+
|
|
171
|
+
status_code = answ.status_code
|
|
172
|
+
data = []
|
|
173
|
+
|
|
174
|
+
if status_code in [200, 206]:
|
|
175
|
+
|
|
176
|
+
if status_code == 200:
|
|
177
|
+
data.append(answ.json()['data'])
|
|
178
|
+
|
|
179
|
+
while status_code == 206:
|
|
180
|
+
if verbose:
|
|
181
|
+
print('code_206, more results')
|
|
182
|
+
data.append(answ.json()['data'])
|
|
183
|
+
|
|
184
|
+
if answ.json()['next'] is not None:
|
|
185
|
+
next_url = answ.json()['next']
|
|
186
|
+
if verbose:
|
|
187
|
+
print(f'url next : {next_url}')
|
|
188
|
+
answ = requests.get(next_url)
|
|
189
|
+
status_code = answ.status_code
|
|
190
|
+
if status_code not in [200, 206]:
|
|
191
|
+
if verbose:
|
|
192
|
+
print(f'Error code: {status_code}, reason:\n{answ.reason}')
|
|
193
|
+
print(f'parameters used : {params}')
|
|
194
|
+
if status_code == 400 and verbose:
|
|
195
|
+
print('Total size limit might have been reached. (20 000 data).',
|
|
196
|
+
f'Possible solution : request with more parameters to cut the answer in smaller pieces.\n{answ.json()}')
|
|
197
|
+
print(f'Allready fetch data size : {sum([len(e) for e in data])} in {len(data)} element in list')
|
|
198
|
+
else:
|
|
199
|
+
# status was 206 but no next page
|
|
200
|
+
status_code = 200
|
|
201
|
+
if verbose:
|
|
202
|
+
print(status_code)
|
|
203
|
+
else:
|
|
204
|
+
# 1st code is not 200,206
|
|
205
|
+
print(f'Error code: {status_code},\n url = {url_req}')
|
|
206
|
+
try:
|
|
207
|
+
print(f'reason:\n{answ.reason}\n{answ.json()}')
|
|
208
|
+
except requests.JSONDecodeError:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
return answ, data
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class HubeauUtils:
|
|
216
|
+
"""
|
|
217
|
+
HubeauUtils
|
|
218
|
+
-----------
|
|
219
|
+
Generic object to request Hubeau'API endpoints.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
def __init__(self, endpoint='niveaux_nappes', operator='chroniques', version=1):
|
|
223
|
+
|
|
224
|
+
self.hubeau_url = "https://hubeau.eaufrance.fr/api/v{}/".format(version)
|
|
225
|
+
self.doc_operator = "api-docs"
|
|
226
|
+
|
|
227
|
+
self._endpoint = endpoint
|
|
228
|
+
self._operator = operator
|
|
229
|
+
|
|
230
|
+
if operator.endswith('.csv'):
|
|
231
|
+
raise ValueError('csv format request is not yet implemented. Please use json operator')
|
|
232
|
+
|
|
233
|
+
self.all_endpoints = [
|
|
234
|
+
# default, can be reset with setter func
|
|
235
|
+
"qualite_nappes",
|
|
236
|
+
"niveaux_nappes",
|
|
237
|
+
"qualite_rivieres",
|
|
238
|
+
"temperature",
|
|
239
|
+
"hydrometrie",
|
|
240
|
+
"prelevements",
|
|
241
|
+
]
|
|
242
|
+
|
|
243
|
+
@property
|
|
244
|
+
def endpoint(self): # getter
|
|
245
|
+
return self._endpoint
|
|
246
|
+
@endpoint.setter
|
|
247
|
+
def endpoint(self, endpoint: str):
|
|
248
|
+
self._endpoint = endpoint
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def operator(self): # getter
|
|
252
|
+
return self._operator
|
|
253
|
+
@operator.setter
|
|
254
|
+
def operator(self, operator: str):
|
|
255
|
+
self._operator = operator
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def get_apis_documentation(self, endpoint: str|None = None)->dict:
|
|
259
|
+
"""
|
|
260
|
+
Get documentation for a specific endpoint
|
|
261
|
+
Param:
|
|
262
|
+
endpoint: string. Authorized value : one of the available endpoint given by HubeauUtils.available_endpoints()
|
|
263
|
+
"""
|
|
264
|
+
if endpoint is None:
|
|
265
|
+
endpoint = self.endpoint
|
|
266
|
+
if endpoint not in self.all_endpoints:
|
|
267
|
+
raise ValueError(print(f'Parameter {endpoint} is not authorized. Please refer to API\'s documentation.'))
|
|
268
|
+
|
|
269
|
+
url = f'{self.hubeau_url}/{endpoint}/{self.doc_operator}'
|
|
270
|
+
get_answ = requests.get(url)
|
|
271
|
+
|
|
272
|
+
if get_answ.status_code in [200]:
|
|
273
|
+
api_doc = get_answ.json()
|
|
274
|
+
else:
|
|
275
|
+
# print('Response Error - the API status code was not 200')
|
|
276
|
+
api_doc = {}
|
|
277
|
+
|
|
278
|
+
return api_doc
|
|
279
|
+
|
|
280
|
+
def get_operators(self) -> list:
|
|
281
|
+
"""
|
|
282
|
+
Renvoie l'ensemble des opérations possibles pour le endpoint spécifié en paramètre
|
|
283
|
+
Param:
|
|
284
|
+
endpoint: string.
|
|
285
|
+
Authorized value : one of the available endpoint given by HubeauUtils.available_endpoints()
|
|
286
|
+
"""
|
|
287
|
+
docu = self.get_apis_documentation(endpoint=self.endpoint)
|
|
288
|
+
available_operators = [
|
|
289
|
+
docu['paths'][k]['get']['operationId'].replace(' csv','.csv') \
|
|
290
|
+
for k in docu['paths'].keys()
|
|
291
|
+
]
|
|
292
|
+
return available_operators
|
|
293
|
+
|
|
294
|
+
def get_parameters(self, doc=None, operator=None, name_only: bool=False) -> dict:
|
|
295
|
+
"""
|
|
296
|
+
list the available parameters in the get method for a specific operator
|
|
297
|
+
in the selected endpoint
|
|
298
|
+
"""
|
|
299
|
+
if doc is None:
|
|
300
|
+
doc = self.get_apis_documentation()
|
|
301
|
+
if operator is None:
|
|
302
|
+
operator = self.operator
|
|
303
|
+
params = [
|
|
304
|
+
doc['paths'][key]['get']['parameters'] for key in doc['paths'].keys() \
|
|
305
|
+
if key.endswith(operator)
|
|
306
|
+
]
|
|
307
|
+
if name_only:
|
|
308
|
+
params = [elem['name'] for elem in params[0]]
|
|
309
|
+
return params
|
|
310
|
+
|
|
311
|
+
def get_from_api(
|
|
312
|
+
self,
|
|
313
|
+
parameters: dict={'size':200},
|
|
314
|
+
operator=None,
|
|
315
|
+
verbose: bool=False,
|
|
316
|
+
return_type='pandas',
|
|
317
|
+
debug=False
|
|
318
|
+
):
|
|
319
|
+
"""
|
|
320
|
+
This fonction is used to send request to hubeau and get results as
|
|
321
|
+
json or pandas.DataFrame
|
|
322
|
+
|
|
323
|
+
Parameters
|
|
324
|
+
----------
|
|
325
|
+
parameters: dict
|
|
326
|
+
e.g : size: size of the page
|
|
327
|
+
bbox=str, need to be in format 'long_min,lat_min,long_max,lat_max'
|
|
328
|
+
srid=int, e.g 4326
|
|
329
|
+
|
|
330
|
+
operator: str, Optionnal.
|
|
331
|
+
the operator to requests. By default, set to the operator chosen at class instanciation.
|
|
332
|
+
Custom value allow to request a different operator of same endpoint, without creating another instance
|
|
333
|
+
of `HubeauUtils`
|
|
334
|
+
|
|
335
|
+
return_type: str, optionnal.
|
|
336
|
+
Default is 'pandas'
|
|
337
|
+
accepted values =
|
|
338
|
+
'pandas' (or 'dataframe', return only df),
|
|
339
|
+
"answer" return answer from requests, data in json, data in pandas.DataFrame
|
|
340
|
+
Returns
|
|
341
|
+
-------
|
|
342
|
+
get answer as provided by requests library, data in json format, data in pandas dataframe format
|
|
343
|
+
|
|
344
|
+
"""
|
|
345
|
+
|
|
346
|
+
if operator is None:
|
|
347
|
+
operator = self.operator
|
|
348
|
+
|
|
349
|
+
dict_param = _check_parameters(**parameters)
|
|
350
|
+
url = f'{self.hubeau_url}{self.endpoint}/{operator}?'
|
|
351
|
+
|
|
352
|
+
get, data = _curl_api(url, dict_param, verbose)
|
|
353
|
+
|
|
354
|
+
if len(data) > 0:
|
|
355
|
+
df = pd.concat([pd.DataFrame.from_dict(elem) for elem in data]) # or pandas.read_json() ?
|
|
356
|
+
else:
|
|
357
|
+
df = pd.DataFrame() # return an empty data.frame to avoid type error if no data
|
|
358
|
+
|
|
359
|
+
if return_type.lower() in ['pandas', 'dataframe', 'df']:
|
|
360
|
+
return df
|
|
361
|
+
else:
|
|
362
|
+
return get, data, df
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
class _AbstractHub(HubeauUtils):
|
|
367
|
+
|
|
368
|
+
def __init__(self, api, version=1):
|
|
369
|
+
self.api_key = api
|
|
370
|
+
self.api = get_api_description(api)
|
|
371
|
+
super().__init__(self.api.get('endpoint'), self.api.get('operator_sta'), version)
|
|
372
|
+
|
|
373
|
+
def _get_station(self, verbose=False, **kwargs):
|
|
374
|
+
return self.get_from_api(operator=self.api.get('operator_sta'), verbose=verbose, parameters=kwargs)
|
|
375
|
+
|
|
376
|
+
def _get_data(
|
|
377
|
+
self,
|
|
378
|
+
operator=None,
|
|
379
|
+
fields=[],
|
|
380
|
+
labels=[],
|
|
381
|
+
only_valid_data=False,
|
|
382
|
+
date_fmt='%Y-%m-%d',
|
|
383
|
+
freq='D',
|
|
384
|
+
agg_func=None,
|
|
385
|
+
verbose=False,
|
|
386
|
+
**kwargs
|
|
387
|
+
):
|
|
388
|
+
""" get [consolidated|real-time] data from api
|
|
389
|
+
"""
|
|
390
|
+
|
|
391
|
+
if operator is None:
|
|
392
|
+
operator = self.api.get('operator_obs')
|
|
393
|
+
|
|
394
|
+
df = self.get_from_api(operator=operator, parameters=kwargs, verbose=verbose)
|
|
395
|
+
|
|
396
|
+
if df.empty:
|
|
397
|
+
return df
|
|
398
|
+
|
|
399
|
+
if only_valid_data:
|
|
400
|
+
df = self._filter_valid_data(df, mode=self.api_key)
|
|
401
|
+
|
|
402
|
+
if len(fields) > 0:
|
|
403
|
+
df = df.loc[:, fields]
|
|
404
|
+
if len(fields) == len(labels):
|
|
405
|
+
df.columns = labels
|
|
406
|
+
|
|
407
|
+
df = self._set_date_index(df, date_fmt=date_fmt)
|
|
408
|
+
df = df.sort_index()
|
|
409
|
+
|
|
410
|
+
if agg_func is not None:
|
|
411
|
+
df = df.resample(freq).agg(agg_func)
|
|
412
|
+
|
|
413
|
+
return df
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@staticmethod
|
|
417
|
+
def _filter_valid_data(df, mode: str, verbose=False):
|
|
418
|
+
if mode == 'piezometry':
|
|
419
|
+
df_filter = df.loc[
|
|
420
|
+
# drop wrong/uncertain measures
|
|
421
|
+
(df['qualification'].str.lower() != 'incertaine') & (df['qualification'].str.lower() != 'incorrecte') &
|
|
422
|
+
# drop dynamic (d) and out of water/dry (s) measures
|
|
423
|
+
(df['code_nature_mesure'].str.lower() != "d") & (df['code_nature_mesure'].str.lower() != "s"),
|
|
424
|
+
:
|
|
425
|
+
]
|
|
426
|
+
elif mode == 'hydrometry':
|
|
427
|
+
df_filter = df.loc[
|
|
428
|
+
# Relevant values are `12` ("dubious"),
|
|
429
|
+
# `16` ("correct"), and `20` ("good").
|
|
430
|
+
df['code_qualification'].isin([16, 20]), :
|
|
431
|
+
]
|
|
432
|
+
else:
|
|
433
|
+
if verbose:
|
|
434
|
+
print('Warning, all data are kept. Filter valid data only available for `piezometry` and `hydrometry` for now.')
|
|
435
|
+
df_filter = df.copy()
|
|
436
|
+
return df_filter
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
@staticmethod
|
|
440
|
+
def _set_date_index(df, date_fmt='%Y-%m-%dT%H:%M:%SZ'):
|
|
441
|
+
col_date = [
|
|
442
|
+
x for x in df.columns \
|
|
443
|
+
if x in ['date', 'date_mesure', 'date_obs_elab', 'date_obs', 'annee', 'date_debut_prelevement', 'date_prelevement']
|
|
444
|
+
][0]
|
|
445
|
+
# Remarks:
|
|
446
|
+
# * In the 'river_qual' (qualite_rivieres) api, datetime is split into two columns (date_prelevement, heure_prelevement).
|
|
447
|
+
|
|
448
|
+
# Dev note by Marc Laurencelle:
|
|
449
|
+
# NOT NECESSARY YET: For most purposes, only the Date part (without time) is used, so xxxxx for now,
|
|
450
|
+
# and yet it may be interesting to exploit the Hour info by improving the disabled code below,
|
|
451
|
+
# that is why it is backed-up here:
|
|
452
|
+
# # Separated hour info: in 'river_qual' api only:
|
|
453
|
+
# col_heure = [
|
|
454
|
+
# x for x in df.columns \
|
|
455
|
+
# if x in ['heure_prelevement']
|
|
456
|
+
# ][0]
|
|
457
|
+
# if len(col_heure) == 0:
|
|
458
|
+
# datetimedata = df[col_date]
|
|
459
|
+
# else:
|
|
460
|
+
# print("TESTING prep datetimedata:")
|
|
461
|
+
# print("TYPE OF df[col_date] =")
|
|
462
|
+
# print(df[col_date].dtype)
|
|
463
|
+
# print(type(df[col_date]))
|
|
464
|
+
|
|
465
|
+
# datetimedata = df[col_date] + ' ' + df[col_heure]
|
|
466
|
+
# df['date'] = pd.to_datetime(datetimedata, format=date_fmt)
|
|
467
|
+
# xxxxx END of disabled block.
|
|
468
|
+
|
|
469
|
+
df['date'] = pd.to_datetime(df[col_date], format=date_fmt)
|
|
470
|
+
df_timeindex = df.set_index('date')
|
|
471
|
+
if col_date in df_timeindex.columns:
|
|
472
|
+
# case date field was not `date`
|
|
473
|
+
df_timeindex = df_timeindex.drop(col_date, axis=1)
|
|
474
|
+
return df_timeindex
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
@staticmethod
|
|
478
|
+
def _merge_tr(df, df_tr):
|
|
479
|
+
dff = pd.concat([df, df_tr])
|
|
480
|
+
dff = dff[~dff.index.duplicated(keep='first')] # priority on valid data if same index
|
|
481
|
+
return dff
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def _get_consolidated_and_realtime_data(
|
|
485
|
+
self,
|
|
486
|
+
fields=[], labels=[],
|
|
487
|
+
fields_tr=[], labels_tr=[],
|
|
488
|
+
add_real_time=True,
|
|
489
|
+
agg_tr='mean',
|
|
490
|
+
only_valid_data=True,
|
|
491
|
+
verbose=False,
|
|
492
|
+
params_tr={},
|
|
493
|
+
**params
|
|
494
|
+
):
|
|
495
|
+
""" Wrapper function to get consolidated and realtime data all at once.
|
|
496
|
+
|
|
497
|
+
Parameters
|
|
498
|
+
----------
|
|
499
|
+
|
|
500
|
+
fields: list of str, Optionnal.
|
|
501
|
+
Fields to keep in consolidated data.
|
|
502
|
+
|
|
503
|
+
labels: list of str, Optionnal.
|
|
504
|
+
labels to rename fields kept in consolidated data.
|
|
505
|
+
|
|
506
|
+
fields_tr: list of str, Optionnal.
|
|
507
|
+
Fields to keep in real-time data.
|
|
508
|
+
|
|
509
|
+
labels_tr: list of str, Optionnal.
|
|
510
|
+
labels to rename fields kept in real-time data.
|
|
511
|
+
|
|
512
|
+
add_real_time: bool, Optionnal (default is True).
|
|
513
|
+
add real time data or only get consolidated data.
|
|
514
|
+
|
|
515
|
+
only_valid_data: bool, Optionnal (default is True).
|
|
516
|
+
filter valid data.
|
|
517
|
+
For groundwater : uncertain, incorrect, drougth, pumped data are drop.
|
|
518
|
+
For river discharge: only good, correct and dubious are kept.
|
|
519
|
+
|
|
520
|
+
agg_tr: str, Optionnal (default is `mean`).
|
|
521
|
+
Function to use to aggregate realtime data from hourly to daily.
|
|
522
|
+
if None is passed, no aggregation is performed.
|
|
523
|
+
|
|
524
|
+
verbose: bool, Optionnal (default is False).
|
|
525
|
+
|
|
526
|
+
params_tr: dict, Optionnal.
|
|
527
|
+
additional parameters to pass to `get_data_tr()` (ie real time data).
|
|
528
|
+
|
|
529
|
+
kwargs:
|
|
530
|
+
any parameters to pass to get_data (ie consolidated data)
|
|
531
|
+
|
|
532
|
+
Returns
|
|
533
|
+
-------
|
|
534
|
+
pandas.DataFrame with data
|
|
535
|
+
"""
|
|
536
|
+
|
|
537
|
+
code = [x for x in params.keys() if re.match('code.*', x)][0]
|
|
538
|
+
code_station = params.get(code)
|
|
539
|
+
|
|
540
|
+
df = self._get_data(
|
|
541
|
+
fields=fields,
|
|
542
|
+
labels=labels,
|
|
543
|
+
only_valid_data=only_valid_data,
|
|
544
|
+
verbose=verbose,
|
|
545
|
+
**params
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
if df.empty:
|
|
549
|
+
if verbose:
|
|
550
|
+
print('Error while fecthing data, nothing for station : {}'.format(code_station))
|
|
551
|
+
print('Parameters: {}'.format(params))
|
|
552
|
+
last_date = datetime.now() - timedelta(days=93) # max 3 months days in TR API
|
|
553
|
+
else:
|
|
554
|
+
last_date = df.index.max()
|
|
555
|
+
|
|
556
|
+
# get realtime data
|
|
557
|
+
if add_real_time and last_date < datetime.now():
|
|
558
|
+
df_tr = self._get_data(
|
|
559
|
+
operator=self.api.get('operator_tr'),
|
|
560
|
+
fields=fields_tr,
|
|
561
|
+
labels=labels_tr,
|
|
562
|
+
agg_func=agg_tr,
|
|
563
|
+
date_fmt='%Y-%m-%dT%H:%M:%SZ',
|
|
564
|
+
date_debut_mesure=last_date,
|
|
565
|
+
verbose=verbose,
|
|
566
|
+
**{**DEFAULT_PARAMS, **params_tr}
|
|
567
|
+
)
|
|
568
|
+
if df_tr.empty:
|
|
569
|
+
if verbose:
|
|
570
|
+
print("No TR values : {}".format(code_station) )
|
|
571
|
+
else:
|
|
572
|
+
df = self._merge_tr(df, df_tr)
|
|
573
|
+
|
|
574
|
+
if not df.empty:
|
|
575
|
+
df = df.assign(indice=code_station)
|
|
576
|
+
df = df.set_index('indice', append=True)
|
|
577
|
+
else:
|
|
578
|
+
df = pd.DataFrame() # force true empty df if no `valid` data ! otherwise, empty with one col but no mutliindex...
|
|
579
|
+
|
|
580
|
+
return df
|
|
581
|
+
|
|
582
|
+
|