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.
Files changed (182) hide show
  1. farmwise_api/__init__.py +12 -0
  2. farmwise_api/_vendor/__init__.py +1 -0
  3. farmwise_api/_vendor/hubeaupyutils/LICENSE +21 -0
  4. farmwise_api/_vendor/hubeaupyutils/__init__.py +37 -0
  5. farmwise_api/_vendor/hubeaupyutils/hubeau.py +582 -0
  6. farmwise_api/_vendor/hubeaupyutils/wrappers.py +292 -0
  7. farmwise_api/adapters/API_readers/CHMI_Meteo/CHMI_meteo.py +289 -0
  8. farmwise_api/adapters/API_readers/CHMI_Meteo/__init__.py +1 -0
  9. farmwise_api/adapters/API_readers/ESDAC/ESDAC_mappings/ESDAC_mappings.py +11 -0
  10. farmwise_api/adapters/API_readers/ESDAC/ESDAC_mappings/__init__.py +1 -0
  11. farmwise_api/adapters/API_readers/ESDAC/ESDAC_wrapper.py +1 -0
  12. farmwise_api/adapters/API_readers/ESDAC/__init__.py +1 -0
  13. farmwise_api/adapters/API_readers/EuroCropV2/EuroCropV2_read.py +90 -0
  14. farmwise_api/adapters/API_readers/EuroCropV2/__init__.py +1 -0
  15. farmwise_api/adapters/API_readers/EuroCropV2/mappings/EuroCropV2_mappings.py +6 -0
  16. farmwise_api/adapters/API_readers/EuroCropV2/mappings/__init__.py +1 -0
  17. farmwise_api/adapters/API_readers/EuroCropV2/utils/__init__.py +1 -0
  18. farmwise_api/adapters/API_readers/EuroCropV2/utils/extractors.py +101 -0
  19. farmwise_api/adapters/API_readers/EuroCropV2/utils/preparation.py +142 -0
  20. farmwise_api/adapters/API_readers/IFSGRID/IFSGRID_read.py +67 -0
  21. farmwise_api/adapters/API_readers/IFSGRID/__init__.py +1 -0
  22. farmwise_api/adapters/API_readers/IFSGRID/mappings/IFSGRID_mappings.py +116 -0
  23. farmwise_api/adapters/API_readers/IFSGRID/mappings/__init__.py +1 -0
  24. farmwise_api/adapters/API_readers/IFSGRID/utils/__init__.py +1 -0
  25. farmwise_api/adapters/API_readers/IFSGRID/utils/definitions.json +371 -0
  26. farmwise_api/adapters/API_readers/IFSGRID/utils/extraction.py +138 -0
  27. farmwise_api/adapters/API_readers/IFSGRID/utils/preparation.py +116 -0
  28. farmwise_api/adapters/API_readers/UA_sw_quality/UA_sw_quality_mappings/UA_sw_quality_mapping.py +38 -0
  29. farmwise_api/adapters/API_readers/UA_sw_quality/UA_sw_quality_mappings/__init__.py +1 -0
  30. farmwise_api/adapters/API_readers/UA_sw_quality/__init__.py +1 -0
  31. farmwise_api/adapters/API_readers/UA_sw_quality/ukrainian_surface_water.py +142 -0
  32. farmwise_api/adapters/API_readers/__init__.py +1 -0
  33. farmwise_api/adapters/API_readers/cds/__init__.py +1 -0
  34. farmwise_api/adapters/API_readers/cds/cds_land_cover.py +46 -0
  35. farmwise_api/adapters/API_readers/cds/cds_mappings/__init__.py +1 -0
  36. farmwise_api/adapters/API_readers/cds/cds_mappings/cds_land_cover_mapping.py +1 -0
  37. farmwise_api/adapters/API_readers/cds/cds_mappings/cds_single_levels_mapping.py +44 -0
  38. farmwise_api/adapters/API_readers/cds/cds_single_levels.py +220 -0
  39. farmwise_api/adapters/API_readers/cds/cds_utils/__init__.py +1 -0
  40. farmwise_api/adapters/API_readers/cds/cds_utils/cds_data_read.py +128 -0
  41. farmwise_api/adapters/API_readers/corine/__init__.py +1 -0
  42. farmwise_api/adapters/API_readers/corine/corine_mappings/__init__.py +1 -0
  43. farmwise_api/adapters/API_readers/corine/corine_mappings/corine_mapping.py +130 -0
  44. farmwise_api/adapters/API_readers/corine/corine_read.py +226 -0
  45. farmwise_api/adapters/API_readers/correctiv/__init__.py +1 -0
  46. farmwise_api/adapters/API_readers/correctiv/correctiv_read.py +97 -0
  47. farmwise_api/adapters/API_readers/correctiv/mappings/__init__.py +1 -0
  48. farmwise_api/adapters/API_readers/correctiv/mappings/correctiv_mappings.py +18 -0
  49. farmwise_api/adapters/API_readers/correctiv/utils/__init__.py +1 -0
  50. farmwise_api/adapters/API_readers/correctiv/utils/extractors.py +139 -0
  51. farmwise_api/adapters/API_readers/correctiv/utils/preparation.py +56 -0
  52. farmwise_api/adapters/API_readers/eea/__init__.py +1 -0
  53. farmwise_api/adapters/API_readers/eea/eea_mappings/__init__.py +1 -0
  54. farmwise_api/adapters/API_readers/eea/eea_mappings/eea_mappings.py +13 -0
  55. farmwise_api/adapters/API_readers/eea/eea_read.py +176 -0
  56. farmwise_api/adapters/API_readers/epa_ireland/__init__.py +1 -0
  57. farmwise_api/adapters/API_readers/epa_ireland/constants/EPA_coordinates.csv +89 -0
  58. farmwise_api/adapters/API_readers/epa_ireland/epa_gw.py +178 -0
  59. farmwise_api/adapters/API_readers/epa_ireland/epa_ireland_mappings/__init__.py +1 -0
  60. farmwise_api/adapters/API_readers/epa_ireland/epa_ireland_mappings/epa_ireland_mapping.py +12 -0
  61. farmwise_api/adapters/API_readers/geosphere/__init__.py +1 -0
  62. farmwise_api/adapters/API_readers/geosphere/geosphere.py +162 -0
  63. farmwise_api/adapters/API_readers/geosphere/geosphere_mapping/__init__.py +1 -0
  64. farmwise_api/adapters/API_readers/geosphere/geosphere_mapping/geosphere_mapping.py +12 -0
  65. farmwise_api/adapters/API_readers/gios/__init__.py +1 -0
  66. farmwise_api/adapters/API_readers/gios/constants/gios_coordinates.csv +217 -0
  67. farmwise_api/adapters/API_readers/gios/gios_api_stations.py +50 -0
  68. farmwise_api/adapters/API_readers/gios/gios_mappings/__init__.py +1 -0
  69. farmwise_api/adapters/API_readers/gios/gios_mappings/gios_mapping.py +264 -0
  70. farmwise_api/adapters/API_readers/gios/gios_scraper.py +224 -0
  71. farmwise_api/adapters/API_readers/gios/gios_utils.py +27 -0
  72. farmwise_api/adapters/API_readers/gios_gw/__init__.py +1 -0
  73. farmwise_api/adapters/API_readers/gios_gw/gios_gw.py +250 -0
  74. farmwise_api/adapters/API_readers/gios_gw/gios_gw_mappings/__init__.py +1 -0
  75. farmwise_api/adapters/API_readers/gios_gw/gios_gw_mappings/gios_gw_mapping.py +78 -0
  76. farmwise_api/adapters/API_readers/gios_gw/gios_gw_stations.py +98 -0
  77. farmwise_api/adapters/API_readers/hubeau/__init__.py +1 -0
  78. farmwise_api/adapters/API_readers/hubeau/constants/farmwise_gwquality_parameters.csv +37 -0
  79. farmwise_api/adapters/API_readers/hubeau/constants/farmwise_gwquality_points_sel_for_hubeau.csv +1877 -0
  80. farmwise_api/adapters/API_readers/hubeau/constants/farmwise_gwquantity_piezos_sel_for_hubeau.csv +2148 -0
  81. farmwise_api/adapters/API_readers/hubeau/constants/farmwise_sw_quality_stations_sel_for_hubeau.csv +5141 -0
  82. farmwise_api/adapters/API_readers/hubeau/get_piezo_stations.py +54 -0
  83. farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/__init__.py +1 -0
  84. farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/hubeau_mapping_piezo.py +11 -0
  85. farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/hubeau_mapping_sw_quality.py +81 -0
  86. farmwise_api/adapters/API_readers/hubeau/hubeau_mappings/hubeau_mapping_wq.py +78 -0
  87. farmwise_api/adapters/API_readers/hubeau/hubeau_piezo_read_vbrgm.py +338 -0
  88. farmwise_api/adapters/API_readers/hubeau/hubeau_sw_quality_read.py +395 -0
  89. farmwise_api/adapters/API_readers/hubeau/hubeau_units.py +261 -0
  90. farmwise_api/adapters/API_readers/hubeau/hubeau_wq_read.py +383 -0
  91. farmwise_api/adapters/API_readers/imgw/__init__.py +1 -0
  92. farmwise_api/adapters/API_readers/imgw/imgw_api_stations.py +65 -0
  93. farmwise_api/adapters/API_readers/imgw/imgw_api_synop_daily.py +277 -0
  94. farmwise_api/adapters/API_readers/imgw/imgw_mappings/__init__.py +1 -0
  95. farmwise_api/adapters/API_readers/imgw/imgw_mappings/synop_mapping.py +73 -0
  96. farmwise_api/adapters/API_readers/imgw_hydro/__init__.py +1 -0
  97. farmwise_api/adapters/API_readers/imgw_hydro/imgw_api_hydro_daily.py +168 -0
  98. farmwise_api/adapters/API_readers/imgw_hydro/imgw_hydro_stations_api.py +45 -0
  99. farmwise_api/adapters/API_readers/imgw_hydro/imgw_mappings/__init__.py +1 -0
  100. farmwise_api/adapters/API_readers/imgw_hydro/imgw_mappings/imgw_hydro_mappings.py +25 -0
  101. farmwise_api/adapters/API_readers/irish_meteo/EPA_ireland_stations.csv +2084 -0
  102. farmwise_api/adapters/API_readers/irish_meteo/__init__.py +1 -0
  103. farmwise_api/adapters/API_readers/irish_meteo/irish_meteo_mappings/__init__.py +1 -0
  104. farmwise_api/adapters/API_readers/irish_meteo/irish_meteo_mappings/irish_meteo_mapping.py +9 -0
  105. farmwise_api/adapters/API_readers/irish_meteo/irish_ms_daily.py +159 -0
  106. farmwise_api/adapters/API_readers/quadica/DATA_SETUP.md +31 -0
  107. farmwise_api/adapters/API_readers/quadica/__init__.py +0 -0
  108. farmwise_api/adapters/API_readers/quadica/mappings/quadica_mappings.py +217 -0
  109. farmwise_api/adapters/API_readers/quadica/mappings/quadica_table.html +184 -0
  110. farmwise_api/adapters/API_readers/quadica/quadica_read.py +84 -0
  111. farmwise_api/adapters/API_readers/quadica/utils/__init__.py +0 -0
  112. farmwise_api/adapters/API_readers/quadica/utils/definition.py +19 -0
  113. farmwise_api/adapters/API_readers/quadica/utils/extractors.py +209 -0
  114. farmwise_api/adapters/API_readers/quadica/utils/preparation.py +110 -0
  115. farmwise_api/adapters/API_readers/soilgrids/__init__.py +1 -0
  116. farmwise_api/adapters/API_readers/soilgrids/soilgrids_call.py +113 -0
  117. farmwise_api/adapters/API_readers/soilgrids/soilgrids_mappings/__init__.py +1 -0
  118. farmwise_api/adapters/API_readers/soilgrids/soilgrids_mappings/soilgrids_mapping.py +57 -0
  119. farmwise_api/adapters/API_readers/wetterdienst/__init__.py +1 -0
  120. farmwise_api/adapters/API_readers/wetterdienst/wetterdienst_dwd.py +255 -0
  121. farmwise_api/adapters/API_readers/wetterdienst/wetterdienst_mapping/__init__.py +1 -0
  122. farmwise_api/adapters/API_readers/wetterdienst/wetterdienst_mapping/dwd_mapping.py +11 -0
  123. farmwise_api/adapters/__init__.py +1 -0
  124. farmwise_api/adapters/mappings/__init__.py +1 -0
  125. farmwise_api/adapters/mappings/data_source_mapping.py +310 -0
  126. farmwise_api/adapters/mappings/units.py +29 -0
  127. farmwise_api/cli.py +50 -0
  128. farmwise_api/core/__init__.py +1 -0
  129. farmwise_api/core/harmonization.py +336 -0
  130. farmwise_api/core/main_call.py +545 -0
  131. farmwise_api/core/quality_assess.py +376 -0
  132. farmwise_api/core/utils/__init__.py +1 -0
  133. farmwise_api/core/utils/access_policy.py +55 -0
  134. farmwise_api/core/utils/cells_to_coordinates.py +85 -0
  135. farmwise_api/core/utils/coordinates_to_cells.py +81 -0
  136. farmwise_api/core/utils/country_bboxes.py +25 -0
  137. farmwise_api/core/utils/country_codes +1502 -0
  138. farmwise_api/core/utils/data_manifest.py +47 -0
  139. farmwise_api/core/utils/data_operators.py +8 -0
  140. farmwise_api/core/utils/fetch_data.py +56 -0
  141. farmwise_api/core/utils/generate_cells.py +86 -0
  142. farmwise_api/core/utils/imgw_utils.py +63 -0
  143. farmwise_api/core/utils/interpolate_data.py +259 -0
  144. farmwise_api/core/utils/map_ploter.py +764 -0
  145. farmwise_api/core/utils/merge_bboxes.py +6 -0
  146. farmwise_api/core/utils/name_to_coordinates.py +44 -0
  147. farmwise_api/core/utils/overlap_checks.py +88 -0
  148. farmwise_api/core/utils/paths.py +292 -0
  149. farmwise_api/core/utils/proj_env.py +140 -0
  150. farmwise_api/core/utils/reproject_raster.py +35 -0
  151. farmwise_api/core/within_source_aggregation.py +151 -0
  152. farmwise_api/server/__init__.py +1 -0
  153. farmwise_api/server/api_utils.py +42 -0
  154. farmwise_api/server/crud.py +36 -0
  155. farmwise_api/server/db_users.py +62 -0
  156. farmwise_api/server/email_utils.py +69 -0
  157. farmwise_api/server/hashing_utils.py +11 -0
  158. farmwise_api/server/jobs.py +128 -0
  159. farmwise_api/server/logging_config.py +11 -0
  160. farmwise_api/server/main.py +71 -0
  161. farmwise_api/server/routers/__init__.py +1 -0
  162. farmwise_api/server/routers/auth.py +49 -0
  163. farmwise_api/server/routers/data_call.py +477 -0
  164. farmwise_api/server/routers/frontpage.py +597 -0
  165. farmwise_api/server/scheduler.py +16 -0
  166. farmwise_api/server/schemas.py +184 -0
  167. farmwise_api/server/security.py +106 -0
  168. farmwise_api/server/services.py +20 -0
  169. farmwise_api/server/sql_schemas.py +12 -0
  170. farmwise_api/server/static/farmwise.png +0 -0
  171. farmwise_api/server/static/farmwise_logo.png +0 -0
  172. farmwise_api/server/static/style.css +34 -0
  173. farmwise_api/server/static/upwr.png +0 -0
  174. farmwise_api/server/user_database.py +26 -0
  175. farmwise_api-0.1.0.dist-info/METADATA +465 -0
  176. farmwise_api-0.1.0.dist-info/RECORD +182 -0
  177. farmwise_api-0.1.0.dist-info/WHEEL +5 -0
  178. farmwise_api-0.1.0.dist-info/entry_points.txt +2 -0
  179. farmwise_api-0.1.0.dist-info/licenses/DATA_LICENSES.md +433 -0
  180. farmwise_api-0.1.0.dist-info/licenses/LICENSE +193 -0
  181. farmwise_api-0.1.0.dist-info/licenses/farmwise_api/_vendor/hubeaupyutils/LICENSE +21 -0
  182. farmwise_api-0.1.0.dist-info/top_level.txt +1 -0
@@ -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
+