Coverage for toardb / data / data.py: 65%
132 statements
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 12:51 +0000
« prev ^ index » next coverage.py v7.14.0, created at 2026-05-12 12:51 +0000
1# SPDX-FileCopyrightText: 2021 Forschungszentrum Jülich GmbH
2# SPDX-License-Identifier: MIT
4"""
5Simple test API for data management
6"""
8import json
9from typing import List
10from fastapi import APIRouter, Depends, HTTPException, \
11 File, UploadFile, Request
12from sqlalchemy.orm import Session
13from sqlalchemy.engine import Engine
14from . import crud, schemas
15from toardb.utils.database import ToarDbSession, engine, get_engine, get_db
16from toardb.utils.utils import (
17 get_data_download_access_rights,
18 get_map_data_download_access_rights,
19 get_admin_access_rights,
20 get_data_change_access_rights
21)
22from toardb.utils.settings import request_limitations as limits
23import datetime as dt
25router = APIRouter()
27# CRUD: create, retrieve, update, delete
29# 1. retrieve
31#get all data of one timeseries
32@router.get('/data/timeseries/{timeseries_id}', response_model=schemas.Composite, response_model_exclude_unset=True, response_model_exclude_none=True)
33def get_data(timeseries_id: int, request: Request,
34 access: dict = Depends(get_data_download_access_rights),
35 db: Session = Depends(get_db)):
36 # check whether the user is sending the request via the dashboard
37 if not access['lfromdashboard']:
38 if access['status_code'] == 200:
39 if access['role'] == 'anonymous' and not limits['anonymous']['min_tsid'] <= timeseries_id <= limits['anonymous']['max_tsid']:
40 raise HTTPException(status_code=401, detail=f"Anonymous access only to timeseries within the ID range of {limits['anonymous']['min_tsid']} to {limits['anonymous']['max_tsid']}.")
41 else:
42 raise HTTPException(status_code=401, detail="Unauthorized.")
44 db_data = crud.get_data(db, timeseries_id=timeseries_id, path_params=request.path_params, query_params=request.query_params)
45 if db_data is None:
46 raise HTTPException(status_code=404, detail="Data not found.")
47 return db_data
50#get all data of one timeseries
51@router.get('/data/timeseries/id/{timeseries_id}', response_model=schemas.Composite, response_model_exclude_unset=True, response_model_exclude_none=True)
52def get_data2(timeseries_id: int, request: Request,
53 access: dict = Depends(get_data_download_access_rights),
54 db: Session = Depends(get_db)):
55 if access['status_code'] == 200:
56 if access['role'] == 'anonymous' and not limits['anonymous']['min_tsid'] <= timeseries_id <= limits['anonymous']['max_tsid']:
57 raise HTTPException(status_code=401, detail=f"Anonymous access only to timeseries within the ID range of {limits['anonymous']['min_tsid']} to {limits['anonymous']['max_tsid']}.")
59 db_data = crud.get_data(db, timeseries_id=timeseries_id, path_params=request.path_params, query_params=request.query_params)
60 if db_data is None:
61 raise HTTPException(status_code=404, detail="Data not found.")
62 return db_data
63 else:
64 raise HTTPException(status_code=401, detail="Unauthorized.")
67#get all data of one timeseries (including staging data)
68@router.get('/data/timeseries_with_staging/id/{timeseries_id}', response_model=schemas.Composite, response_model_exclude_unset=True, response_model_exclude_none=True)
69def get_data_with_staging(timeseries_id: int, flags: str = None, format: str = 'json',
70 access: dict = Depends(get_data_download_access_rights),
71 db: Session = Depends(get_db)):
72 if access['status_code'] == 200:
73 if access['role'] == 'anonymous' and not limits['anonymous']['min_tsid'] <= timeseries_id <= limits['anonymous']['max_tsid']:
74 raise HTTPException(status_code=401, detail=f"Anonymous access only to timeseries within the ID range of {limits['anonymous']['min_tsid']} to {limits['anonymous']['max_tsid']}.")
76 db_data = crud.get_data_with_staging(db, timeseries_id=timeseries_id, flags=flags, format=format)
77 if db_data is None:
78 raise HTTPException(status_code=404, detail="Data not found.")
79 return db_data
80 else:
81 raise HTTPException(status_code=401, detail="Unauthorized.")
84#get map data (for a special variable and timestamp)
85@router.get('/data/map/')
86def get_map_data(variable_id: int = 5, daterange: str = '2023-02-22 12:00,2023-02-22 12:00',
87 access: dict = Depends(get_map_data_download_access_rights),
88 db: Session = Depends(get_db)):
89 # check whether the user is sending the request via the dashboard
90 if not access['lfromdashboard']:
91 if access['status_code'] != 200:
92 raise HTTPException(status_code=401, detail="Unauthorized.")
93 db_data = crud.get_map_data(db, variable_id=variable_id, daterange=daterange)
94 if db_data is None:
95 raise HTTPException(status_code=404, detail="Data not found.")
96 return db_data
99#get the next available version for one timeseries
100@router.get('/data/timeseries/next_version/{timeseries_id}')
101def get_version(timeseries_id: int, request: Request, db: Session = Depends(get_db)):
102 version = crud.get_next_version(db, timeseries_id=timeseries_id, path_params=request.path_params, query_params=request.query_params)
103 return version
106@router.get('/data/timeseries_merged/', response_model=schemas.Composite, response_model_exclude_unset=True, response_model_exclude_none=True)
107def get_merged_data(variable_id: int, station_code: str, request: Request, daterange: str = None, has_role: str = None,
108 access: dict = Depends(get_data_download_access_rights),
109 db: Session = Depends(get_db)):
110 # check whether the user is sending the request via the dashboard
111 if not access['lfromdashboard']:
112 if access['status_code'] == 200:
113 if access['role'] == 'anonymous':
114 merging_list = crud.get_merging_list(db, variable_id=variable_id, station_code=station_code, daterange=daterange, role=has_role)
115 # gather all timeseries_ids involved in this request
116 ts_ids = set()
117 if merging_list[0] != []:
118 [ts_ids.add(ts_id[2]) for ts_id in merging_list[0]]
119 if merging_list[1] != []:
120 [ts_ids.add(ts_id) for ts_id in merging_list[1]]
121 min_request_id = -1 if not ts_ids else min(ts_ids)
122 max_request_id = -1 if not ts_ids else max(ts_ids)
123 if (not limits['anonymous']['min_tsid'] <= min_request_id or
124 not max_request_id <= limits['anonymous']['max_tsid']):
125 raise HTTPException(status_code=401, detail=f"Anonymous access only to timeseries within the ID range of {limits['anonymous']['min_tsid']} to {limits['anonymous']['max_tsid']}.")
126 else:
127 raise HTTPException(status_code=401, detail="Unauthorized.")
129 db_data = crud.get_merged_data(db, variable_id=variable_id, station_code=station_code, role=has_role, path_params=request.path_params, query_params=request.query_params)
130 if db_data is None:
131 raise HTTPException(status_code=404, detail="Data not found.")
132 return db_data
135@router.get('/data/get_merging_list/')
136def get_merging_list(variable_id: int, station_code: str, daterange: str, has_role: str = None, db: Session = Depends(get_db)):
137 return crud.get_merging_list(db, variable_id=variable_id, station_code=station_code, daterange=daterange, role=has_role)
140#get map data (for a special variable and timestamp)
141@router.get('/data/map/')
142def get_map_data(variable_id: int = 5, daterange: str = '2023-02-22 12:00,2023-02-22 12:00', db: Session = Depends(get_db)):
143 db_data = crud.get_map_data(db, variable_id=variable_id, daterange=daterange)
144 if db_data is None:
145 raise HTTPException(status_code=404, detail="Data not found.")
146 return db_data
149# post and patch only via authorization by Helmholtz AAI
151# 2. create
153@router.post('/data/timeseries/')
154def create_data(file: UploadFile = File(...),
155 toarqc_config_type: str = 'standard',
156 dry_run: bool = False,
157 force: bool = False,
158 access: dict = Depends(get_admin_access_rights),
159 db: Session = Depends(get_db),
160 engine: Engine = Depends(get_engine)):
161# # the next three lines are automatically done by database management,
162# # but we do want helpful error messages!
163# db_data = crud.get_data_by_datetime_and_timeseriesid(db, datetime=data.datetime, timeseries_id=data.timeseries_id)
164# if db_data:
165# raise HTTPException(status_code=400, detail="Data already registered.")
167# BUT:
168# we want to upload a whole file!
169#
170 # check whether the post is authorized (401: Unauthorized)
171 if access['status_code'] == 200:
172 response = crud.create_data(db, engine, author_id=access['auth_user_id'], input_handle=file,
173 toarqc_config_type=toarqc_config_type, dry_run=dry_run, force=force)
174 if response.status_code != 200 and response.status_code != 446:
175 msg = json.loads(response.body.decode('utf-8'))
176 # try to parse error messages from DBS (to be more understandable)
177 msg2 = "An error occurred in public.data insertion: <class 'psycopg2.errors.UniqueViolation'>"
178 if (msg == msg2):
179 msg = 'Data for timeseries already registered.'
180 raise HTTPException(status_code=400, detail=msg)
181 return response
182 else:
183 raise HTTPException(status_code=401, detail="Unauthorized.")
186@router.post('/data/timeseries/bulk/')
187def create_bulk_data(bulk: List[schemas.DataCreate],
188 toarqc_config_type: str = 'standard',
189 dry_run: bool = False,
190 force: bool = False,
191 access: dict = Depends(get_admin_access_rights),
192 db: Session = Depends(get_db),
193 engine: Engine = Depends(get_engine)):
194 # check whether the post is authorized (401: Unauthorized)
195 if access['status_code'] == 200:
196 response = crud.create_bulk_data(db, engine, bulk=bulk,
197 author_id=access['auth_user_id'], toarqc_config_type=toarqc_config_type, dry_run=dry_run,
198 force=force)
199 if response.status_code != 200:
200 msg = response.body.decode('utf-8')
201 # try to parse error messages from DBS (to be more understandable)
202 msg2 = '"An error occurred in public.data insertion: <class \'psycopg2.errors.UniqueViolation\'>"'
203 if (msg == msg2):
204 msg = 'Data for timeseries already registered.'
205 raise HTTPException(status_code=400, detail=msg)
206 return response
207 else:
208 raise HTTPException(status_code=401, detail="Unauthorized.")
211@router.post('/data/timeseries/record/')
212def create_data_record(series_id: int,
213 datetime: dt.datetime,
214 value: float,
215 flag: str,
216 version: str = None,
217 suppress_unique_violation: bool = False,
218 access: dict = Depends(get_admin_access_rights),
219 db: Session = Depends(get_db),
220 engine: Engine = Depends(get_engine)):
221 # check whether the post is authorized (401: Unauthorized)
222 if access['status_code'] == 200:
223 response = crud.create_data_record(db, engine, series_id=series_id, datetime=datetime,
224 value=value, flag=flag, version=version,
225 author_id=access['auth_user_id'])
226 if response.status_code != 200:
227 msg = response.body.decode('utf-8')
228 # try to parse error messages from DBS (to be more understandable)
229 msg2 = '"An error occurred in data insertion: <class \'psycopg2.errors.UniqueViolation\'>"'
230 if (msg == msg2):
231 if not suppress_unique_violation:
232 msg = 'Data for timeseries already registered.'
233 raise HTTPException(status_code=400, detail=msg)
234 else:
235 raise HTTPException(status_code=400, detail=msg)
236 return response
237 else:
238 raise HTTPException(status_code=401, detail="Unauthorized.")
240# 3. update
242@router.patch('/data/timeseries/')
243def patch_data(description: str,
244 version: str,
245 file: UploadFile = File(...),
246 toarqc_config_type: str = 'standard',
247 force: bool = False,
248 access: dict = Depends(get_data_change_access_rights),
249 db: Session = Depends(get_db),
250 engine: Engine = Depends(get_engine)):
251 # check whether the patch is authorized (401: Unauthorized)
252 if access['status_code'] == 200:
253 return crud.patch_data(db, engine, description=description, version=version, toarqc_config_type=toarqc_config_type,
254 force=force, author_id=access['auth_user_id'], input_handle=file)
255 else:
256 raise HTTPException(status_code=401, detail="Unauthorized.")
258@router.patch('/data/timeseries/bulk/')
259def patch_bulk_data(description: str,
260 version: str,
261 bulk: List[schemas.DataPatch],
262 toarqc_config_type: str = 'standard',
263 force: bool = False,
264 no_archive: bool = False,
265 access: dict = Depends(get_data_change_access_rights),
266 db: Session = Depends(get_db),
267 engine: Engine = Depends(get_engine)):
268 # check whether the patch is authorized (401: Unauthorized)
269 if access['status_code'] == 200:
270 return crud.patch_bulk_data(db, engine, description=description, version=version, bulk=bulk, author_id=access['auth_user_id'],
271 toarqc_config_type=toarqc_config_type, force=force, no_archive=no_archive)
272 else:
273 raise HTTPException(status_code=401, detail="Unauthorized.")