Coverage for tests/test_timeseries.py: 100%

406 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-03 20:32 +0000

1# SPDX-FileCopyrightText: 2021 Forschungszentrum Jülich GmbH 

2# SPDX-License-Identifier: MIT 

3 

4import pytest 

5import json 

6from sqlalchemy import insert 

7from toardb.timeseries.models import Timeseries, timeseries_timeseries_roles_table, \ 

8 s1_contributors_table 

9from toardb.timeseries.models_programme import TimeseriesProgramme 

10from toardb.timeseries.models_role import TimeseriesRole 

11from toardb.stationmeta.models import StationmetaCore, StationmetaGlobal 

12from toardb.stationmeta.schemas import get_geom_from_coordinates, Coordinates 

13from toardb.variables.models import Variable 

14from toardb.contacts.models import Person, Organisation, Contact 

15from toardb.auth_user.models import AuthUser 

16# Required imports 'create_test_database' 

17from toardb.test_base import ( 

18 client, 

19 get_test_db, 

20 override_dependency, 

21 create_test_database, 

22 url, 

23 get_test_engine, 

24 test_db_session as db, 

25) 

26# for mocking datetime.now(timezone.utc) 

27from datetime import datetime 

28from unittest.mock import patch 

29 

30 

31class TestApps: 

32 def setup(self): 

33 self.application_url = "/timeseries/" 

34 

35 """Set up all the data before each test 

36 If you want the setup only once (per test module), 

37 the scope argument is not working in the expected way, as discussed here: 

38 https://stackoverflow.com/questions/45817153/py-test-fixture-use-function-fixture-in-scope-fixture 

39 """ 

40 @pytest.fixture(autouse=True) 

41 def setup_db_data(self, db): 

42 _db_conn = get_test_engine() 

43 # id_seq will not be reset automatically between tests! 

44 fake_conn = _db_conn.raw_connection() 

45 fake_cur = fake_conn.cursor() 

46 fake_cur.execute("ALTER SEQUENCE auth_user_id_seq RESTART WITH 1") 

47 fake_conn.commit() 

48 fake_cur.execute("ALTER SEQUENCE variables_id_seq RESTART WITH 1") 

49 fake_conn.commit() 

50 fake_cur.execute("ALTER SEQUENCE stationmeta_core_id_seq RESTART WITH 1") 

51 fake_conn.commit() 

52 fake_cur.execute("ALTER SEQUENCE stationmeta_global_id_seq RESTART WITH 1") 

53 fake_conn.commit() 

54 fake_cur.execute("ALTER SEQUENCE stationmeta_roles_id_seq RESTART WITH 1") 

55 fake_conn.commit() 

56 fake_cur.execute("ALTER SEQUENCE stationmeta_annotations_id_seq RESTART WITH 1") 

57 fake_conn.commit() 

58 fake_cur.execute("ALTER SEQUENCE stationmeta_aux_doc_id_seq RESTART WITH 1") 

59 fake_conn.commit() 

60 fake_cur.execute("ALTER SEQUENCE stationmeta_aux_image_id_seq RESTART WITH 1") 

61 fake_conn.commit() 

62 fake_cur.execute("ALTER SEQUENCE stationmeta_aux_url_id_seq RESTART WITH 1") 

63 fake_conn.commit() 

64 fake_cur.execute("ALTER SEQUENCE persons_id_seq RESTART WITH 1") 

65 fake_conn.commit() 

66 fake_cur.execute("ALTER SEQUENCE organisations_id_seq RESTART WITH 1") 

67 fake_conn.commit() 

68 fake_cur.execute("ALTER SEQUENCE contacts_id_seq RESTART WITH 1") 

69 fake_conn.commit() 

70 fake_cur.execute("ALTER SEQUENCE timeseries_annotations_id_seq RESTART WITH 1") 

71 fake_conn.commit() 

72 fake_cur.execute("ALTER SEQUENCE timeseries_roles_id_seq RESTART WITH 4") 

73 fake_conn.commit() 

74 fake_cur.execute("ALTER SEQUENCE timeseries_programmes_id_seq RESTART WITH 1") 

75 fake_conn.commit() 

76 fake_cur.execute("ALTER SEQUENCE timeseries_id_seq RESTART WITH 1") 

77 fake_conn.commit() 

78 fake_cur.execute("ALTER TABLE timeseries_changelog ALTER COLUMN datetime SET DEFAULT '2023-07-28 12:00:00'") 

79 fake_conn.commit() 

80 infilename = "tests/fixtures/auth_user/auth.json" 

81 with open(infilename) as f: 

82 metajson=json.load(f) 

83 for entry in metajson: 

84 new_auth_user = AuthUser(**entry) 

85 db.add(new_auth_user) 

86 db.commit() 

87 db.refresh(new_auth_user) 

88 infilename = "tests/fixtures/contacts/persons.json" 

89 with open(infilename) as f: 

90 metajson=json.load(f) 

91 for entry in metajson: 

92 new_person = Person(**entry) 

93 db.add(new_person) 

94 db.commit() 

95 db.refresh(new_person) 

96 infilename = "tests/fixtures/contacts/organisations.json" 

97 with open(infilename) as f: 

98 metajson=json.load(f) 

99 for entry in metajson: 

100 new_organisation = Organisation(**entry) 

101 db.add(new_organisation) 

102 db.commit() 

103 db.refresh(new_organisation) 

104 infilename = "tests/fixtures/contacts/contacts.json" 

105 with open(infilename) as f: 

106 metajson=json.load(f) 

107 for entry in metajson: 

108 new_contact = Contact(**entry) 

109 db.add(new_contact) 

110 db.commit() 

111 db.refresh(new_contact) 

112 infilename = "tests/fixtures/variables/variables.json" 

113 with open(infilename) as f: 

114 metajson=json.load(f) 

115 for entry in metajson: 

116 new_variable = Variable(**entry) 

117 db.add(new_variable) 

118 db.commit() 

119 db.refresh(new_variable) 

120 infilename = "tests/fixtures/stationmeta/stationmeta_core.json" 

121 with open(infilename) as f: 

122 metajson=json.load(f) 

123 for entry in metajson: 

124 new_stationmeta_core = StationmetaCore(**entry) 

125 # there's a mismatch with coordinates --> how to automatically switch back and forth?! 

126 tmp_coordinates = new_stationmeta_core.coordinates 

127 new_stationmeta_core.coordinates = get_geom_from_coordinates(Coordinates(**new_stationmeta_core.coordinates)) 

128 db.add(new_stationmeta_core) 

129 db.commit() 

130 db.refresh(new_stationmeta_core) 

131 infilename = "tests/fixtures/stationmeta/stationmeta_global.json" 

132 with open(infilename) as f: 

133 metajson=json.load(f) 

134 for entry in metajson: 

135 new_stationmeta_global = StationmetaGlobal(**entry) 

136 db.add(new_stationmeta_global) 

137 db.commit() 

138 db.refresh(new_stationmeta_global) 

139 infilename = "tests/fixtures/timeseries/timeseries_programmes.json" 

140 with open(infilename) as f: 

141 metajson=json.load(f) 

142 for entry in metajson: 

143 new_timeseries_programme = TimeseriesProgramme(**entry) 

144 db.add(new_timeseries_programme) 

145 db.commit() 

146 db.refresh(new_timeseries_programme) 

147 infilename = "tests/fixtures/timeseries/timeseries.json" 

148 with open(infilename) as f: 

149 metajson=json.load(f) 

150 for entry in metajson: 

151 new_timeseries = Timeseries(**entry) 

152 db.add(new_timeseries) 

153 db.commit() 

154 db.refresh(new_timeseries) 

155 infilename = "tests/fixtures/timeseries/timeseries_roles.json" 

156 with open(infilename) as f: 

157 metajson=json.load(f) 

158 for entry in metajson: 

159 new_timeseries_role = TimeseriesRole(**entry) 

160 db.add(new_timeseries_role) 

161 db.commit() 

162 db.refresh(new_timeseries_role) 

163 infilename = "tests/fixtures/timeseries/timeseries_timeseries_roles.json" 

164 with open(infilename) as f: 

165 metajson=json.load(f) 

166 for entry in metajson: 

167 db.execute(insert(timeseries_timeseries_roles_table).values(timeseries_id=entry["timeseries_id"], role_id=entry["role_id"])) 

168 db.execute("COMMIT") 

169 infilename = "tests/fixtures/timeseries/timeseries_contributors.json" 

170 with open(infilename) as f: 

171 metajson=json.load(f) 

172 for entry in metajson: 

173 db.execute(insert(s1_contributors_table).values(request_id=entry["request_id"], timeseries_ids=entry["timeseries_ids"])) 

174 db.execute("COMMIT") 

175 

176 

177 def test_get_timeseries(self, client, db): 

178 response = client.get("/timeseries/") 

179 expected_status_code = 200 

180 assert response.status_code == expected_status_code 

181 expected_resp = [{'id': 1, 

182 'label': 'CMA', 

183 'order': 1, 

184 'sampling_frequency': 'hourly', 

185 'aggregation': 'mean', 

186 'data_start_date': '2003-09-07T15:30:00+00:00', 

187 'data_end_date': '2016-12-31T14:30:00+00:00', 

188 'data_origin': 'instrument', 

189 'data_origin_type': 'measurement', 

190 'provider_version': 'N/A', 

191 'sampling_height': 7.0, 

192 'additional_metadata': {'original_units': 'ppb'}, 

193 'doi': '', 

194 'coverage': -1.0, 

195 'station': {'id': 2, 

196 'codes': ['SDZ54421'], 

197 'name': 'Shangdianzi', 

198 'coordinates': {'lat': 40.65, 'lng': 117.106, 'alt': 293.9}, 

199 'coordinate_validation_status': 'not checked', 

200 'country': 'China', 

201 'state': 'Beijing Shi', 

202 'type': 'unknown', 

203 'type_of_area': 'unknown', 

204 'timezone': 'Asia/Shanghai', 

205 'additional_metadata': {'add_type': 'nature reservation'}, 

206 'aux_images': [], 

207 'aux_docs': [], 

208 'aux_urls': [], 

209 'globalmeta': {'mean_topography_srtm_alt_90m_year1994': -999.0, 

210 'mean_topography_srtm_alt_1km_year1994': -999.0, 

211 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

212 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

213 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

214 'climatic_zone_year2016': '6 (warm temperate dry)', 

215 'htap_region_tier1_year2010': '11 (MDE Middle ' 

216 'East: S. Arabia, ' 

217 'Oman, etc, Iran, ' 

218 'Iraq)', 

219 'dominant_landcover_year2012': '11 (Cropland, ' 

220 'rainfed, herbaceous cover)', 

221 'landcover_description_25km_year2012': '', 

222 'dominant_ecoregion_year2017': '-1 (undefined)', 

223 'ecoregion_description_25km_year2017': '', 

224 'distance_to_major_road_year2020': -999.0, 

225 'mean_stable_nightlights_1km_year2013': -999.0, 

226 'mean_stable_nightlights_5km_year2013': -999.0, 

227 'max_stable_nightlights_25km_year2013': -999.0, 

228 'max_stable_nightlights_25km_year1992': -999.0, 

229 'mean_population_density_250m_year2015': -1.0, 

230 'mean_population_density_5km_year2015': -1.0, 

231 'max_population_density_25km_year2015': -1.0, 

232 'mean_population_density_250m_year1990': -1.0, 

233 'mean_population_density_5km_year1990': -1.0, 

234 'max_population_density_25km_year1990': -1.0, 

235 'mean_nox_emissions_10km_year2015': -999.0, 

236 'mean_nox_emissions_10km_year2000': -999.0, 

237 'toar1_category': 'unclassified', 

238 'toar2_category': 'suburban'}, 

239 'changelog': []}, 

240 'variable': {'name': 'toluene', 

241 'longname': 'toluene', 

242 'displayname': 'Toluene', 

243 'cf_standardname': 'mole_fraction_of_toluene_in_air', 

244 'units': 'nmol mol-1', 

245 'chemical_formula': 'C7H8', 

246 'id': 7}, 

247 'programme': {'id': 0, 

248 'name': '', 

249 'longname': '', 

250 'homepage': '', 

251 'description': ''}, 

252 'roles': [{'id': 2, 

253 'role': 'resource provider', 

254 'status': 'active', 

255 'contact': {'id': 4, 

256 'organisation': {'id': 1, 

257 'name': 'UBA', 

258 'longname': 'Umweltbundesamt', 

259 'kind': 'government', 

260 'city': 'Dessau-Roßlau', 

261 'postcode': '06844', 

262 'street_address': 'Wörlitzer Platz 1', 

263 'country': 'Germany', 

264 'homepage': 'https://www.umweltbundesamt.de', 

265 'contact_url': 'mailto:immission@uba.de'}}}, 

266 {'id': 3, 

267 'role': 'principal investigator', 

268 'status': 'active', 

269 'contact': {'id': 3, 

270 'person': {'email': 's.schroeder@fz-juelich.de', 

271 'id': 3, 

272 'isprivate': False, 

273 'name': 'Sabine Schröder', 

274 'orcid': '0000-0002-0309-8010', 

275 'phone': '+49-2461-61-6397'}}}]}, 

276 {'id': 2, 

277 'label': 'CMA', 

278 'order': 1, 

279 'sampling_frequency': 'hourly', 

280 'aggregation': 'mean', 

281 'data_start_date': '2003-09-07T15:30:00+00:00', 

282 'data_end_date': '2016-12-31T14:30:00+00:00', 

283 'data_origin': 'instrument', 

284 'data_origin_type': 'measurement', 

285 'provider_version': 'N/A', 

286 'sampling_height': 7.0, 

287 'additional_metadata': {'original_units': {'since_19740101000000': 'nmol/mol'}, 

288 'measurement_method': 'uv_abs', 

289 'absorption_cross_section': 'Hearn 1961', 

290 'ebas_metadata_19740101000000_29y': {'Submitter': 'Unknown, ' 

291 'Lady, ' 

292 'lady.unknown@unknown.com, ' 

293 'some ' 

294 'long ' 

295 'division ' 

296 'name, ' 

297 'SHORT, ' 

298 ', ' 

299 '111 ' 

300 'Streetname, ' 

301 ', ' 

302 'zipcode, ' 

303 'Boulder, ' 

304 'CO, ' 

305 'USA', 

306 'Data level': '2', 

307 'Frameworks': 'GAW-WDCRG ' 

308 'NOAA-ESRL', 

309 'Station code': 'XXX', 

310 'Station name': 'Secret'}}, 

311 'doi': '', 

312 'coverage': -1.0, 

313 'station': {'id': 3, 

314 'codes': ['China_test8'], 

315 'name': 'Test_China', 

316 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, 

317 'coordinate_validation_status': 'not checked', 

318 'country': 'China', 

319 'state': 'Shandong Sheng', 

320 'type': 'unknown', 

321 'type_of_area': 'unknown', 

322 'timezone': 'Asia/Shanghai', 

323 'additional_metadata': {}, 

324 'aux_images': [], 

325 'aux_docs': [], 

326 'aux_urls': [], 

327 'globalmeta': {'mean_topography_srtm_alt_90m_year1994': -999.0, 

328 'mean_topography_srtm_alt_1km_year1994': -999.0, 

329 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

330 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

331 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

332 'climatic_zone_year2016': '6 (warm temperate dry)', 

333 'htap_region_tier1_year2010': '10 (SAF Sub ' 

334 'Saharan/sub Sahel ' 

335 'Africa)', 

336 'dominant_landcover_year2012': '10 (Cropland, ' 

337 'rainfed)', 

338 'landcover_description_25km_year2012': '', 

339 'dominant_ecoregion_year2017': '-1 (undefined)', 

340 'ecoregion_description_25km_year2017': '', 

341 'distance_to_major_road_year2020': -999.0, 

342 'mean_stable_nightlights_1km_year2013': -999.0, 

343 'mean_stable_nightlights_5km_year2013': -999.0, 

344 'max_stable_nightlights_25km_year2013': -999.0, 

345 'max_stable_nightlights_25km_year1992': -999.0, 

346 'mean_population_density_250m_year2015': -1.0, 

347 'mean_population_density_5km_year2015': -1.0, 

348 'max_population_density_25km_year2015': -1.0, 

349 'mean_population_density_250m_year1990': -1.0, 

350 'mean_population_density_5km_year1990': -1.0, 

351 'max_population_density_25km_year1990': -1.0, 

352 'mean_nox_emissions_10km_year2015': -999.0, 

353 'mean_nox_emissions_10km_year2000': -999.0, 

354 'toar1_category': 'unclassified', 

355 'toar2_category': 'suburban'}, 

356 'changelog': []}, 

357 'variable': {'name': 'o3', 

358 'longname': 'ozone', 

359 'displayname': 'Ozone', 

360 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

361 'units': 'nmol mol-1', 

362 'chemical_formula': 'O3', 

363 'id': 5}, 

364 'programme': {'id': 0, 

365 'name': '', 

366 'longname': '', 

367 'homepage': '', 

368 'description': ''}, 

369 'roles': [{'id': 1, 

370 'role': 'resource provider', 

371 'status': 'active', 

372 'contact': {'id': 5, 

373 'organisation': {'id': 2, 

374 'name': 'FZJ', 

375 'longname': 'Forschungszentrum ' 

376 'Jülich', 

377 'kind': 'research', 

378 'city': 'Jülich', 

379 'postcode': '52425', 

380 'street_address': 'Wilhelm-Johnen-Straße', 

381 'country': 'Germany', 

382 'homepage': 'https://www.fz-juelich.de', 

383 'contact_url': 'mailto:toar-data@fz-juelich.de'}}}]}, 

384 { 

385 'additional_metadata': { 

386 'original_units': 'ppb', 

387 }, 

388 'aggregation': 'mean', 

389 'coverage': -1.0, 

390 'data_end_date': '2025-02-25T14:00:00+00:00', 

391 'data_origin': 'instrument', 

392 'data_origin_type': 'measurement', 

393 'data_start_date': '1991-01-01T00:00:00+00:00', 

394 'doi': '', 

395 'id': 18763, 

396 'label': '', 

397 'order': 1, 

398 'programme': { 

399 'description': '', 

400 'homepage': '', 

401 'id': 0, 

402 'longname': '', 

403 'name': '', 

404 }, 

405 'provider_version': 'N/A', 

406 'roles': [ 

407 { 

408 'contact': { 

409 'id': 5, 

410 'organisation': { 

411 'city': 'Jülich', 

412 'contact_url': 'mailto:toar-data@fz-juelich.de', 

413 'country': 'Germany', 

414 'homepage': 'https://www.fz-juelich.de', 

415 'id': 2, 

416 'kind': 'research', 

417 'longname': 'Forschungszentrum Jülich', 

418 'name': 'FZJ', 

419 'postcode': '52425', 

420 'street_address': 'Wilhelm-Johnen-Straße', 

421 }, 

422 }, 

423 'id': 1, 

424 'role': 'resource provider', 

425 'status': 'active', 

426 }, 

427 ], 

428 'sampling_frequency': 'hourly', 

429 'sampling_height': 7.0, 

430 'station': { 

431 'additional_metadata': { 

432 'add_type': 'nature reservation', 

433 }, 

434 'aux_docs': [], 

435 'aux_images': [], 

436 'aux_urls': [], 

437 'changelog': [], 

438 'codes': [ 

439 'SDZ54421', 

440 ], 

441 'coordinate_validation_status': 'not checked', 

442 'coordinates': { 

443 'alt': 293.9, 

444 'lat': 40.65, 

445 'lng': 117.106, 

446 }, 

447 'country': 'China', 

448 'globalmeta': { 

449 'climatic_zone_year2016': '6 (warm temperate dry)', 

450 'distance_to_major_road_year2020': -999.0, 

451 'dominant_ecoregion_year2017': '-1 (undefined)', 

452 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

453 'ecoregion_description_25km_year2017': '', 

454 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

455 'landcover_description_25km_year2012': '', 

456 'max_population_density_25km_year1990': -1.0, 

457 'max_population_density_25km_year2015': -1.0, 

458 'max_stable_nightlights_25km_year1992': -999.0, 

459 'max_stable_nightlights_25km_year2013': -999.0, 

460 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

461 'mean_nox_emissions_10km_year2000': -999.0, 

462 'mean_nox_emissions_10km_year2015': -999.0, 

463 'mean_population_density_250m_year1990': -1.0, 

464 'mean_population_density_250m_year2015': -1.0, 

465 'mean_population_density_5km_year1990': -1.0, 

466 'mean_population_density_5km_year2015': -1.0, 

467 'mean_stable_nightlights_1km_year2013': -999.0, 

468 'mean_stable_nightlights_5km_year2013': -999.0, 

469 'mean_topography_srtm_alt_1km_year1994': -999.0, 

470 'mean_topography_srtm_alt_90m_year1994': -999.0, 

471 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

472 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

473 'toar1_category': 'unclassified', 

474 'toar2_category': 'suburban', 

475 }, 

476 'id': 2, 

477 'name': 'Shangdianzi', 

478 'state': 'Beijing Shi', 

479 'timezone': 'Asia/Shanghai', 

480 'type': 'unknown', 

481 'type_of_area': 'unknown', 

482 }, 

483 'variable': { 

484 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

485 'chemical_formula': 'O3', 

486 'displayname': 'Ozone', 

487 'id': 5, 

488 'longname': 'ozone', 

489 'name': 'o3', 

490 'units': 'nmol mol-1', 

491 }, 

492 }, 

493 { 

494 'additional_metadata': { 

495 'original_units': 'ppb', 

496 }, 

497 'aggregation': 'mean', 

498 'coverage': -1.0, 

499 'data_end_date': '2025-02-25T14:00:00+00:00', 

500 'data_origin': 'instrument', 

501 'data_origin_type': 'measurement', 

502 'data_start_date': '1991-01-01T00:00:00+00:00', 

503 'doi': '', 

504 'id': 30890, 

505 'label': '', 

506 'order': 2, 

507 'programme': { 

508 'description': '', 

509 'homepage': '', 

510 'id': 0, 

511 'longname': '', 

512 'name': '', 

513 }, 

514 'provider_version': 'N/A', 

515 'roles': [ 

516 { 

517 'contact': { 

518 'id': 5, 

519 'organisation': { 

520 'city': 'Jülich', 

521 'contact_url': 'mailto:toar-data@fz-juelich.de', 

522 'country': 'Germany', 

523 'homepage': 'https://www.fz-juelich.de', 

524 'id': 2, 

525 'kind': 'research', 

526 'longname': 'Forschungszentrum Jülich', 

527 'name': 'FZJ', 

528 'postcode': '52425', 

529 'street_address': 'Wilhelm-Johnen-Straße', 

530 }, 

531 }, 

532 'id': 1, 

533 'role': 'resource provider', 

534 'status': 'active', 

535 }, 

536 ], 

537 'sampling_frequency': 'hourly', 

538 'sampling_height': 7.0, 

539 'station': { 

540 'additional_metadata': { 

541 'add_type': 'nature reservation', 

542 }, 

543 'aux_docs': [], 

544 'aux_images': [], 

545 'aux_urls': [], 

546 'changelog': [], 

547 'codes': [ 

548 'SDZ54421', 

549 ], 

550 'coordinate_validation_status': 'not checked', 

551 'coordinates': { 

552 'alt': 293.9, 

553 'lat': 40.65, 

554 'lng': 117.106, 

555 }, 

556 'country': 'China', 

557 'globalmeta': { 

558 'climatic_zone_year2016': '6 (warm temperate dry)', 

559 'distance_to_major_road_year2020': -999.0, 

560 'dominant_ecoregion_year2017': '-1 (undefined)', 

561 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

562 'ecoregion_description_25km_year2017': '', 

563 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

564 'landcover_description_25km_year2012': '', 

565 'max_population_density_25km_year1990': -1.0, 

566 'max_population_density_25km_year2015': -1.0, 

567 'max_stable_nightlights_25km_year1992': -999.0, 

568 'max_stable_nightlights_25km_year2013': -999.0, 

569 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

570 'mean_nox_emissions_10km_year2000': -999.0, 

571 'mean_nox_emissions_10km_year2015': -999.0, 

572 'mean_population_density_250m_year1990': -1.0, 

573 'mean_population_density_250m_year2015': -1.0, 

574 'mean_population_density_5km_year1990': -1.0, 

575 'mean_population_density_5km_year2015': -1.0, 

576 'mean_stable_nightlights_1km_year2013': -999.0, 

577 'mean_stable_nightlights_5km_year2013': -999.0, 

578 'mean_topography_srtm_alt_1km_year1994': -999.0, 

579 'mean_topography_srtm_alt_90m_year1994': -999.0, 

580 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

581 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

582 'toar1_category': 'unclassified', 

583 'toar2_category': 'suburban', 

584 }, 

585 'id': 2, 

586 'name': 'Shangdianzi', 

587 'state': 'Beijing Shi', 

588 'timezone': 'Asia/Shanghai', 

589 'type': 'unknown', 

590 'type_of_area': 'unknown', 

591 }, 

592 'variable': { 

593 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

594 'chemical_formula': 'O3', 

595 'displayname': 'Ozone', 

596 'id': 5, 

597 'longname': 'ozone', 

598 'name': 'o3', 

599 'units': 'nmol mol-1', 

600 }, 

601 }, 

602 { 

603 'additional_metadata': { 

604 'original_units': 'ppb', 

605 }, 

606 'aggregation': 'mean', 

607 'coverage': -1.0, 

608 'data_end_date': '2025-02-25T14:00:00+00:00', 

609 'data_origin': 'instrument', 

610 'data_origin_type': 'measurement', 

611 'data_start_date': '1991-01-01T00:00:00+00:00', 

612 'doi': '', 

613 'id': 434870, 

614 'label': '', 

615 'order': 2, 

616 'programme': { 

617 'description': '', 

618 'homepage': '', 

619 'id': 0, 

620 'longname': '', 

621 'name': '', 

622 }, 

623 'provider_version': 'N/A', 

624 'roles': [ 

625 { 

626 'contact': { 

627 'id': 4, 

628 'organisation': { 

629 'city': 'Dessau-Roßlau', 

630 'contact_url': 'mailto:immission@uba.de', 

631 'country': 'Germany', 

632 'homepage': 'https://www.umweltbundesamt.de', 

633 'id': 1, 

634 'kind': 'government', 

635 'longname': 'Umweltbundesamt', 

636 'name': 'UBA', 

637 'postcode': '06844', 

638 'street_address': 'Wörlitzer Platz 1', 

639 }, 

640 }, 

641 'id': 2, 

642 'role': 'resource provider', 

643 'status': 'active', 

644 }, 

645 ], 

646 'sampling_frequency': 'hourly', 

647 'sampling_height': 7.0, 

648 'station': { 

649 'additional_metadata': { 

650 'add_type': 'nature reservation', 

651 }, 

652 'aux_docs': [], 

653 'aux_images': [], 

654 'aux_urls': [], 

655 'changelog': [], 

656 'codes': [ 

657 'SDZ54421', 

658 ], 

659 'coordinate_validation_status': 'not checked', 

660 'coordinates': { 

661 'alt': 293.9, 

662 'lat': 40.65, 

663 'lng': 117.106, 

664 }, 

665 'country': 'China', 

666 'globalmeta': { 

667 'climatic_zone_year2016': '6 (warm temperate dry)', 

668 'distance_to_major_road_year2020': -999.0, 

669 'dominant_ecoregion_year2017': '-1 (undefined)', 

670 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

671 'ecoregion_description_25km_year2017': '', 

672 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

673 'landcover_description_25km_year2012': '', 

674 'max_population_density_25km_year1990': -1.0, 

675 'max_population_density_25km_year2015': -1.0, 

676 'max_stable_nightlights_25km_year1992': -999.0, 

677 'max_stable_nightlights_25km_year2013': -999.0, 

678 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

679 'mean_nox_emissions_10km_year2000': -999.0, 

680 'mean_nox_emissions_10km_year2015': -999.0, 

681 'mean_population_density_250m_year1990': -1.0, 

682 'mean_population_density_250m_year2015': -1.0, 

683 'mean_population_density_5km_year1990': -1.0, 

684 'mean_population_density_5km_year2015': -1.0, 

685 'mean_stable_nightlights_1km_year2013': -999.0, 

686 'mean_stable_nightlights_5km_year2013': -999.0, 

687 'mean_topography_srtm_alt_1km_year1994': -999.0, 

688 'mean_topography_srtm_alt_90m_year1994': -999.0, 

689 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

690 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

691 'toar1_category': 'unclassified', 

692 'toar2_category': 'suburban', 

693 }, 

694 'id': 2, 

695 'name': 'Shangdianzi', 

696 'state': 'Beijing Shi', 

697 'timezone': 'Asia/Shanghai', 

698 'type': 'unknown', 

699 'type_of_area': 'unknown', 

700 }, 

701 'variable': { 

702 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

703 'chemical_formula': 'O3', 

704 'displayname': 'Ozone', 

705 'id': 5, 

706 'longname': 'ozone', 

707 'name': 'o3', 

708 'units': 'nmol mol-1', 

709 }, 

710 }] 

711 assert response.json() == expected_resp 

712 

713 

714 def test_get_special(self, client, db): 

715 response = client.get("/timeseries/1") 

716 expected_status_code = 200 

717 assert response.status_code == expected_status_code 

718 expected_resp = {'id': 1, 'label': 'CMA', 'order': 1, 

719 'sampling_frequency': 'hourly', 'aggregation': 'mean', 'data_origin_type': 'measurement', 

720 'data_start_date': '2003-09-07T15:30:00+00:00', 'data_end_date': '2016-12-31T14:30:00+00:00', 'coverage': -1.0, 

721 'data_origin': 'instrument', 'sampling_height': 7.0, 

722 'provider_version': 'N/A', 'doi': '', 'additional_metadata': {'original_units': 'ppb'}, 

723 'roles': [{'id': 2, 'role': 'resource provider', 'status': 'active', 

724 'contact': {'id': 4, 'organisation': {'id': 1, 'name': 'UBA', 'longname': 'Umweltbundesamt', 

725 'kind': 'government', 'city': 'Dessau-Roßlau', 'postcode': '06844', 'street_address': 'Wörlitzer Platz 1', 

726 'country': 'Germany', 'homepage': 'https://www.umweltbundesamt.de', 'contact_url': 'mailto:immission@uba.de'}}}, 

727 {'id': 3, 'role': 'principal investigator', 'status': 'active', 

728 'contact': {'id': 3, 'person': {'email': 's.schroeder@fz-juelich.de', 'id': 3, 'isprivate': False, 

729 'name': 'Sabine Schröder', 'orcid': '0000-0002-0309-8010', 'phone': '+49-2461-61-6397'}}}], 

730 'variable': {'name': 'toluene', 'longname': 'toluene', 'displayname': 'Toluene', 

731 'cf_standardname': 'mole_fraction_of_toluene_in_air', 'units': 'nmol mol-1', 

732 'chemical_formula': 'C7H8', 'id': 7}, 

733 'station': {'id': 2, 'codes': ['SDZ54421'], 'name': 'Shangdianzi', 

734 'coordinates': {'lat': 40.65, 'lng': 117.106, 'alt': 293.9}, 

735 'coordinate_validation_status': 'not checked', 

736 'country': 'China', 'state': 'Beijing Shi', 

737 'type': 'unknown', 'type_of_area': 'unknown', 

738 'timezone': 'Asia/Shanghai', 

739 'additional_metadata': {'add_type': 'nature reservation'}, 

740 'aux_images': [], 'aux_docs': [], 'aux_urls': [], 

741 'globalmeta': {'climatic_zone_year2016': '6 (warm temperate dry)', 

742 'distance_to_major_road_year2020': -999.0, 

743 'dominant_ecoregion_year2017': '-1 (undefined)', 

744 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

745 'ecoregion_description_25km_year2017': '', 

746 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

747 'landcover_description_25km_year2012': '', 

748 'max_stable_nightlights_25km_year1992': -999.0, 

749 'max_stable_nightlights_25km_year2013': -999.0, 

750 'max_population_density_25km_year1990': -1.0, 

751 'max_population_density_25km_year2015': -1.0, 

752 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

753 'mean_stable_nightlights_1km_year2013': -999.0, 

754 'mean_stable_nightlights_5km_year2013': -999.0, 

755 'mean_nox_emissions_10km_year2000': -999.0, 

756 'mean_nox_emissions_10km_year2015': -999.0, 

757 'mean_population_density_250m_year1990': -1.0, 

758 'mean_population_density_250m_year2015': -1.0, 

759 'mean_population_density_5km_year1990': -1.0, 

760 'mean_population_density_5km_year2015': -1.0, 

761 'mean_topography_srtm_alt_1km_year1994': -999.0, 

762 'mean_topography_srtm_alt_90m_year1994': -999.0, 

763 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

764 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

765 'toar1_category': 'unclassified', 

766 'toar2_category': 'suburban'}, 

767 'changelog': []}, 

768 'programme': {'id': 0, 'name': '', 'longname': '', 'homepage': '', 'description': ''}} 

769 assert response.json() == expected_resp 

770 

771 

772 def test_insert_new_wrong_credentials(self, client, db): 

773 response = client.post("/timeseries/", 

774 json={"timeseries": 

775 {"label": "CMA2", "order": 1, 

776 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

777 "data_start_date": "2003-09-07T15:30:00+02:00", 

778 "data_end_date": "2016-12-31T14:30:00+01:00", 

779 'coverage': -1.0, 

780 "data_origin": "Instrument", "sampling_height": 7.0, 

781 "station_id": 2, "variable_id": 7, 

782 "additional_metadata":"{}" 

783 } 

784 }, 

785 headers={"email": "j.doe@fz-juelich.de"} 

786 ) 

787 expected_status_code=401 

788 assert response.status_code == expected_status_code 

789 expected_resp = {'detail': 'Unauthorized.'} 

790 assert response.json() == expected_resp 

791 

792 

793 def test_insert_new_without_credentials(self, client, db): 

794 response = client.post("/timeseries/", 

795 json={"timeseries": 

796 {"label": "CMA2", "order": 1, 

797 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

798 "data_start_date": "2003-09-07T15:30:00+02:00", 

799 "data_end_date": "2016-12-31T14:30:00+01:00", 

800 'coverage': -1.0, 

801 "data_origin": "Instrument", "sampling_height": 7.0, 

802 "station_id": 2, "variable_id": 7, 

803 "additional_metadata":"{}" 

804 } 

805 }) 

806 expected_status_code=401 

807 assert response.status_code == expected_status_code 

808 expected_resp = {'detail': 'Unauthorized.'} 

809 assert response.json() == expected_resp 

810 

811 

812 def test_insert_new_with_roles(self, client, db): 

813 add_meta_dict = {"absorption_cross_section": "Hearn1961", "measurement_method": "uv_abs"} 

814 response = client.post("/timeseries/", 

815 json={"timeseries": 

816 {"label": "CMA2", "order": 1, 

817 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

818 "data_start_date": "2003-09-07T15:30:00+02:00", 

819 "data_end_date": "2016-12-31T14:30:00+01:00", 

820 'coverage': -1.0, 

821 "data_origin": "Instrument", "sampling_height": 7.0, 

822 "station_id": 2, "variable_id": 5, 

823 "additional_metadata": json.dumps(add_meta_dict), 

824 "roles": [{"role": "PointOfContact", "contact_id": 3, "status": "Active"}, 

825 {"role": "Originator", "contact_id": 1, "status": "Active"}] 

826 } 

827 }, 

828 headers={"email": "s.schroeder@fz-juelich.de"} 

829 ) 

830 expected_status_code = 200 

831 assert response.status_code == expected_status_code 

832 expected_resp = {'detail': {'message': 'New timeseries created.', 'timeseries_id': 3}} 

833 assert response.json() == expected_resp 

834 

835 new_response = client.get("/timeseries/3") 

836 new_expected_status_code = 200 

837 assert new_response.status_code == new_expected_status_code 

838 new_response_changelog = new_response.json()["changelog"][-1] 

839 expected_changelog = {'datetime': '2023-07-28T12:00:00+00:00', 'description': 'time series created', 'old_value': '', 'new_value': '', 

840 'timeseries_id': 3, 'author_id': 1, 

841 'type_of_change': 'created'} 

842 assert new_response_changelog == expected_changelog 

843 

844 

845 def test_insert_new_without_existing_station(self, client, db): 

846 response = client.post("/timeseries/", 

847 json={"timeseries": 

848 {"label": "CMA2", "order": 1, 

849 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

850 "data_start_date": "2003-09-07T15:30:00+02:00", 

851 "data_end_date": "2016-12-31T14:30:00+01:00", 

852 'coverage': -1.0, 

853 "data_origin": "Instrument", "sampling_height": 7.0, 

854 "station_id": 97, "variable_id": 7, 

855 "additional_metadata":"{}", 

856 "roles": [{"role": "PointOfContact", "contact_id": 3, "status": "Active"}, 

857 {"role": "Originator", "contact_id": 1, "status": "Active"}] 

858 } 

859 }, 

860 headers={"email": "s.schroeder@fz-juelich.de"} 

861 ) 

862 expected_status_code = 440 

863 assert response.status_code == expected_status_code 

864 expected_resp = 'Station (station_id: 97) not found in database.' 

865 assert response.json() == expected_resp 

866 

867 

868 def test_insert_duplicate_no_resource_provider(self, client, db): 

869 # one timeseries can be inserted without having a resource provider 

870 response = client.post("/timeseries/", 

871 json={"timeseries": 

872 {"label": "CMA", "order": 1, 

873 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

874 "data_start_date": "2003-09-07T15:30:00+02:00", 

875 "data_end_date": "2016-12-31T14:30:00+01:00", 

876 'coverage': -1.0, 

877 "data_origin": "Instrument", "sampling_height": 7.0, 

878 "station_id": 2, "variable_id": 7, 

879 "additional_metadata":"{}", 

880 "roles": [{"role": "PointOfContact", "contact_id": 3, "status": "Active"}, 

881 {"role": "Originator", "contact_id": 1, "status": "Active"}] 

882 } 

883 }, 

884 headers={"email": "s.schroeder@fz-juelich.de"} 

885 ) 

886 expected_status_code = 200 

887 assert response.status_code == expected_status_code 

888 expected_resp = {'detail': {'message': 'New timeseries created.', 'timeseries_id': 3}} 

889 assert response.json() == expected_resp 

890 

891 

892 def test_insert_duplicate_wrong_resource_provider(self, client, db): 

893 # one timeseries can be inserted without having a resource provider 

894 response = client.post("/timeseries/", 

895 json={"timeseries": 

896 {"label": "CMA", "order": 1, 

897 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

898 "data_start_date": "2003-09-07T15:30:00+02:00", 

899 "data_end_date": "2016-12-31T14:30:00+01:00", 

900 'coverage': -1.0, 

901 "data_origin": "Instrument", "sampling_height": 7.0, 

902 "station_id": 2, "variable_id": 7, 

903 "additional_metadata":"{}", 

904 "roles": [{"role": "PointOfContact", "contact_id": 3, "status": "Active"}, 

905 {"role": "Originator", "contact_id": 1, "status": "Active"}, 

906 {"role": "ResourceProvider", "contact_id": 1, "status": "Active"}] 

907 } 

908 }, 

909 headers={"email": "s.schroeder@fz-juelich.de"} 

910 ) 

911 expected_status_code = 442 

912 assert response.status_code == expected_status_code 

913 expected_resp = 'Resource provider (contact_id: 1) not found in database.' 

914 assert response.json() == expected_resp 

915 

916 

917 def test_insert_duplicate(self, client, db): 

918 # one timeseries can be inserted without having a resource provider 

919 response = client.post("/timeseries/", 

920 json={"timeseries": 

921 {"label": "CMA", "order": 1, 

922 "sampling_frequency": "Hourly", "aggregation": "Mean", "data_origin_type": "Measurement", 

923 "data_start_date": "2003-09-07T15:30:00+02:00", 

924 "data_end_date": "2016-12-31T14:30:00+01:00", 

925 'coverage': -1.0, 

926 "data_origin": "Instrument", "sampling_height": 7.0, 

927 "station_id": 2, "variable_id": 7, 

928 "additional_metadata":"{}", 

929 "roles": [{"role": "PointOfContact", "contact_id": 3, "status": "Active"}, 

930 {"role": "Originator", "contact_id": 1, "status": "Active"}, 

931 {"role": "ResourceProvider", "contact_id": 4, "status": "Active"}] 

932 } 

933 }, 

934 headers={"email": "s.schroeder@fz-juelich.de"} 

935 ) 

936 expected_status_code = 443 

937 assert response.status_code == expected_status_code 

938 expected_resp = {'detail': {'message': 'Timeseries already registered.', 'timeseries_id': 1}} 

939 assert response.json() == expected_resp 

940 

941 def test_get_all_timeseries(self, client, db): 

942 response = client.get("/timeseries/?limit=1") 

943 expected_status_code = 200 

944 assert response.status_code == expected_status_code 

945 expected_response = [{ 

946 'id': 1, 'label': 'CMA', 'order': 1, 'sampling_frequency': 'hourly', 

947 'aggregation': 'mean', 'data_start_date': '2003-09-07T15:30:00+00:00', 

948 'data_end_date': '2016-12-31T14:30:00+00:00', 'coverage': -1.0, 'data_origin': 'instrument', 'data_origin_type': 'measurement', 

949 'provider_version': 'N/A', 'doi': '', 'sampling_height': 7.0, 'additional_metadata': {'original_units': 'ppb'}, 

950 'roles': [{'id': 2, 'role': 'resource provider', 'status': 'active', 

951 'contact': {'id': 4, 'organisation': {'id': 1, 'name': 'UBA', 'longname': 'Umweltbundesamt', 

952 'kind': 'government', 'city': 'Dessau-Roßlau', 'postcode': '06844', 'street_address': 'Wörlitzer Platz 1', 

953 'country': 'Germany', 'homepage': 'https://www.umweltbundesamt.de', 'contact_url': 'mailto:immission@uba.de'}}}, 

954 {'id': 3, 'role': 'principal investigator', 'status': 'active', 

955 'contact': {'id': 3, 'person': {'email': 's.schroeder@fz-juelich.de','id': 3, 'isprivate': False, 

956 'name': 'Sabine Schröder', 'orcid': '0000-0002-0309-8010', 'phone': '+49-2461-61-6397'}}} 

957 ], 

958 'station': {'id': 2, 'codes': ['SDZ54421'], 'name': 'Shangdianzi', 

959 'coordinates': {'lat': 40.65, 'lng': 117.106, 'alt': 293.9}, 

960 'coordinate_validation_status': 'not checked', 'country': 'China', 

961 'state': 'Beijing Shi', 'type': 'unknown', 'type_of_area': 'unknown', 

962 'timezone': 'Asia/Shanghai', 

963 'additional_metadata': {'add_type': 'nature reservation'}, 

964 'aux_images': [], 'aux_docs': [], 'aux_urls': [], 

965 'globalmeta': {'climatic_zone_year2016': '6 (warm temperate dry)', 

966 'distance_to_major_road_year2020': -999.0, 

967 'dominant_ecoregion_year2017': '-1 (undefined)', 

968 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

969 'ecoregion_description_25km_year2017': '', 

970 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

971 'landcover_description_25km_year2012': '', 

972 'max_stable_nightlights_25km_year1992': -999.0, 

973 'max_stable_nightlights_25km_year2013': -999.0, 

974 'max_population_density_25km_year1990': -1.0, 

975 'max_population_density_25km_year2015': -1.0, 

976 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

977 'mean_stable_nightlights_1km_year2013': -999.0, 

978 'mean_stable_nightlights_5km_year2013': -999.0, 

979 'mean_nox_emissions_10km_year2000': -999.0, 

980 'mean_nox_emissions_10km_year2015': -999.0, 

981 'mean_population_density_250m_year1990': -1.0, 

982 'mean_population_density_250m_year2015': -1.0, 

983 'mean_population_density_5km_year1990': -1.0, 

984 'mean_population_density_5km_year2015': -1.0, 

985 'mean_topography_srtm_alt_1km_year1994': -999.0, 

986 'mean_topography_srtm_alt_90m_year1994': -999.0, 

987 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

988 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

989 'toar1_category': 'unclassified', 

990 'toar2_category': 'suburban'}, 

991 'changelog': []}, 

992 'variable': {'name': 'toluene', 'longname': 'toluene', 'displayname': 'Toluene', 

993 'cf_standardname': 'mole_fraction_of_toluene_in_air', 'units': 'nmol mol-1', 

994 'chemical_formula': 'C7H8', 'id': 7}, 

995 'programme': {'id': 0, 'name': '', 'longname': '', 'homepage': '', 'description': ''} 

996 }] 

997 assert response.json() == expected_response 

998 

999 

1000 def test_get_one_timeseries_with_fields(self, client, db): 

1001 response = client.get("/timeseries/1?fields=additional_metadata") 

1002 expected_status_code = 200 

1003 assert response.status_code == expected_status_code 

1004 expected_response = {'additional_metadata': {'original_units': 'ppb'} } 

1005 assert response.json() == expected_response 

1006 

1007 

1008 def test_get_all_timeseries_with_fields(self, client, db): 

1009 response = client.get("/timeseries/?fields=additional_metadata") 

1010 expected_status_code = 200 

1011 assert response.status_code == expected_status_code 

1012 expected_response = [ 

1013 {'additional_metadata': {'original_units': 'ppb'}}, 

1014 {'additional_metadata': 

1015 {'absorption_cross_section': 'Hearn 1961', 

1016 'ebas_metadata_19740101000000_29y': 

1017 {'Data level': '2', 

1018 'Frameworks': 'GAW-WDCRG ' 

1019 'NOAA-ESRL', 

1020 'Station code': 'XXX', 

1021 'Station name': 'Secret', 

1022 'Submitter': 'Unknown, Lady, lady.unknown@unknown.com, some long division name, SHORT, , 111 Streetname, , zipcode, Boulder, CO, USA' 

1023 }, 

1024 'measurement_method': 'uv_abs', 

1025 'original_units': {'since_19740101000000': 'nmol/mol'} 

1026 } 

1027 } 

1028 ] 

1029 set_expected_response = {json.dumps(item, sort_keys=True) for item in expected_response} 

1030 set_response = {json.dumps(item, sort_keys=True) for item in response.json()} 

1031 assert set_response == set_expected_response 

1032 

1033 

1034 def test_get_all_timeseries_filter_roles(self, client, db): 

1035 response = client.get("/timeseries/?has_role=UBA") 

1036 expected_status_code = 200 

1037 assert response.status_code == expected_status_code 

1038 expected_response = [{'id': 1, 

1039 'label': 'CMA', 

1040 'order': 1, 

1041 'sampling_frequency': 'hourly', 

1042 'aggregation': 'mean', 

1043 'data_start_date': '2003-09-07T15:30:00+00:00', 

1044 'data_end_date': '2016-12-31T14:30:00+00:00', 

1045 'data_origin': 'instrument', 

1046 'data_origin_type': 'measurement', 

1047 'provider_version': 'N/A', 

1048 'sampling_height': 7.0, 

1049 'additional_metadata': {'original_units': 'ppb'}, 

1050 'doi': '', 

1051 'coverage': -1.0, 

1052 'station': {'id': 2, 

1053 'codes': ['SDZ54421'], 

1054 'name': 'Shangdianzi', 

1055 'coordinates': {'lat': 40.65, 'lng': 117.106, 'alt': 293.9}, 

1056 'coordinate_validation_status': 'not checked', 

1057 'country': 'China', 

1058 'state': 'Beijing Shi', 

1059 'type': 'unknown', 

1060 'type_of_area': 'unknown', 

1061 'timezone': 'Asia/Shanghai', 

1062 'additional_metadata': {'add_type': 'nature reservation'}, 

1063 'aux_images': [], 

1064 'aux_docs': [], 

1065 'aux_urls': [], 

1066 'globalmeta': {'mean_topography_srtm_alt_90m_year1994': -999.0, 

1067 'mean_topography_srtm_alt_1km_year1994': -999.0, 

1068 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

1069 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

1070 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

1071 'climatic_zone_year2016': '6 (warm temperate dry)', 

1072 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

1073 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

1074 'landcover_description_25km_year2012': '', 

1075 'dominant_ecoregion_year2017': '-1 (undefined)', 

1076 'ecoregion_description_25km_year2017': '', 

1077 'distance_to_major_road_year2020': -999.0, 

1078 'mean_stable_nightlights_1km_year2013': -999.0, 

1079 'mean_stable_nightlights_5km_year2013': -999.0, 

1080 'max_stable_nightlights_25km_year2013': -999.0, 

1081 'max_stable_nightlights_25km_year1992': -999.0, 

1082 'mean_population_density_250m_year2015': -1.0, 

1083 'mean_population_density_5km_year2015': -1.0, 

1084 'max_population_density_25km_year2015': -1.0, 

1085 'mean_population_density_250m_year1990': -1.0, 

1086 'mean_population_density_5km_year1990': -1.0, 

1087 'max_population_density_25km_year1990': -1.0, 

1088 'mean_nox_emissions_10km_year2015': -999.0, 

1089 'mean_nox_emissions_10km_year2000': -999.0, 

1090 'toar1_category': 'unclassified', 

1091 'toar2_category': 'suburban' 

1092 }, 

1093 'changelog': [] 

1094 }, 

1095 'variable': {'name': 'toluene', 

1096 'longname': 'toluene', 

1097 'displayname': 'Toluene', 

1098 'cf_standardname': 'mole_fraction_of_toluene_in_air', 

1099 'units': 'nmol mol-1', 

1100 'chemical_formula': 'C7H8', 

1101 'id': 7 

1102 }, 

1103 'programme': {'id': 0, 

1104 'name': '', 

1105 'longname': '', 

1106 'homepage': '', 

1107 'description': '' 

1108 }, 

1109 'roles': [{'id': 2, 

1110 'role': 'resource provider', 

1111 'status': 'active', 

1112 'contact': {'id': 4, 

1113 'organisation': {'id': 1, 

1114 'name': 'UBA', 

1115 'longname': 'Umweltbundesamt', 

1116 'kind': 'government', 

1117 'city': 'Dessau-Roßlau', 

1118 'postcode': '06844', 

1119 'street_address': 'Wörlitzer Platz 1', 

1120 'country': 'Germany', 

1121 'homepage': 'https://www.umweltbundesamt.de', 

1122 'contact_url': 'mailto:immission@uba.de' 

1123 } 

1124 } 

1125 }, 

1126 {'id': 3, 

1127 'role': 'principal investigator', 

1128 'status': 'active', 

1129 'contact': {'id': 3, 

1130 'person': {'email': 's.schroeder@fz-juelich.de', 

1131 'id': 3, 

1132 'isprivate': False, 

1133 'name': 'Sabine Schröder', 

1134 'orcid': '0000-0002-0309-8010', 

1135 'phone': '+49-2461-61-6397'}}} 

1136 ] 

1137 }, 

1138 {'additional_metadata': { 

1139 'original_units': 'ppb', 

1140 }, 

1141 'aggregation': 'mean', 

1142 'coverage': -1.0, 

1143 'data_end_date': '2025-02-25T14:00:00+00:00', 

1144 'data_origin': 'instrument', 

1145 'data_origin_type': 'measurement', 

1146 'data_start_date': '1991-01-01T00:00:00+00:00', 

1147 'doi': '', 

1148 'id': 434870, 

1149 'label': '', 

1150 'order': 2, 

1151 'programme': { 

1152 'description': '', 

1153 'homepage': '', 

1154 'id': 0, 

1155 'longname': '', 

1156 'name': '', 

1157 }, 

1158 'provider_version': 'N/A', 

1159 'roles': [ 

1160 { 

1161 'contact': { 

1162 'id': 4, 

1163 'organisation': { 

1164 'city': 'Dessau-Roßlau', 

1165 'contact_url': 'mailto:immission@uba.de', 

1166 'country': 'Germany', 

1167 'homepage': 'https://www.umweltbundesamt.de', 

1168 'id': 1, 

1169 'kind': 'government', 

1170 'longname': 'Umweltbundesamt', 

1171 'name': 'UBA', 

1172 'postcode': '06844', 

1173 'street_address': 'Wörlitzer Platz 1', 

1174 }, 

1175 }, 

1176 'id': 2, 

1177 'role': 'resource provider', 

1178 'status': 'active', 

1179 }, 

1180 ], 

1181 'sampling_frequency': 'hourly', 

1182 'sampling_height': 7.0, 

1183 'station': { 

1184 'additional_metadata': { 

1185 'add_type': 'nature reservation', 

1186 }, 

1187 'aux_docs': [], 

1188 'aux_images': [], 

1189 'aux_urls': [], 

1190 'changelog': [], 

1191 'codes': [ 

1192 'SDZ54421', 

1193 ], 

1194 'coordinate_validation_status': 'not checked', 

1195 'coordinates': { 

1196 'alt': 293.9, 

1197 'lat': 40.65, 

1198 'lng': 117.106, 

1199 }, 

1200 'country': 'China', 

1201 'globalmeta': { 

1202 'climatic_zone_year2016': '6 (warm temperate dry)', 

1203 'distance_to_major_road_year2020': -999.0, 

1204 'dominant_ecoregion_year2017': '-1 (undefined)', 

1205 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

1206 'ecoregion_description_25km_year2017': '', 

1207 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

1208 'landcover_description_25km_year2012': '', 

1209 'max_population_density_25km_year1990': -1.0, 

1210 'max_population_density_25km_year2015': -1.0, 

1211 'max_stable_nightlights_25km_year1992': -999.0, 

1212 'max_stable_nightlights_25km_year2013': -999.0, 

1213 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

1214 'mean_nox_emissions_10km_year2000': -999.0, 

1215 'mean_nox_emissions_10km_year2015': -999.0, 

1216 'mean_population_density_250m_year1990': -1.0, 

1217 'mean_population_density_250m_year2015': -1.0, 

1218 'mean_population_density_5km_year1990': -1.0, 

1219 'mean_population_density_5km_year2015': -1.0, 

1220 'mean_stable_nightlights_1km_year2013': -999.0, 

1221 'mean_stable_nightlights_5km_year2013': -999.0, 

1222 'mean_topography_srtm_alt_1km_year1994': -999.0, 

1223 'mean_topography_srtm_alt_90m_year1994': -999.0, 

1224 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

1225 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

1226 'toar1_category': 'unclassified', 

1227 'toar2_category': 'suburban', 

1228 }, 

1229 'id': 2, 

1230 'name': 'Shangdianzi', 

1231 'state': 'Beijing Shi', 

1232 'timezone': 'Asia/Shanghai', 

1233 'type': 'unknown', 

1234 'type_of_area': 'unknown', 

1235 }, 

1236 'variable': { 

1237 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

1238 'chemical_formula': 'O3', 

1239 'displayname': 'Ozone', 

1240 'id': 5, 

1241 'longname': 'ozone', 

1242 'name': 'o3', 

1243 'units': 'nmol mol-1', 

1244 } 

1245 }] 

1246 assert response.json() == expected_response 

1247 

1248 

1249 def test_get_all_timeseries_filter_not_roles(self, client, db): 

1250 response = client.get("/timeseries/?has_role=~UBA") 

1251 expected_status_code = 200 

1252 assert response.status_code == expected_status_code 

1253 expected_response = [{'id': 2, 

1254 'label': 'CMA', 

1255 'order': 1, 

1256 'sampling_frequency': 'hourly', 

1257 'aggregation': 'mean', 

1258 'data_start_date': '2003-09-07T15:30:00+00:00', 

1259 'data_end_date': '2016-12-31T14:30:00+00:00', 

1260 'data_origin': 'instrument', 

1261 'data_origin_type': 'measurement', 

1262 'provider_version': 'N/A', 

1263 'sampling_height': 7.0, 

1264 'additional_metadata': {'original_units': {'since_19740101000000': 'nmol/mol'}, 'measurement_method': 'uv_abs', 'absorption_cross_section': 'Hearn 1961', 'ebas_metadata_19740101000000_29y': {'Submitter': 'Unknown, Lady, lady.unknown@unknown.com, some long division name, SHORT, , 111 Streetname, , zipcode, Boulder, CO, USA', 'Data level': '2', 'Frameworks': 'GAW-WDCRG NOAA-ESRL', 'Station code': 'XXX', 'Station name': 'Secret'}}, 

1265 'doi': '', 

1266 'coverage': -1.0, 

1267 'station': {'id': 3, 

1268 'codes': ['China_test8'], 

1269 'name': 'Test_China', 

1270 'coordinates': {'lat': 36.256, 'lng': 117.106, 'alt': 1534.0}, 

1271 'coordinate_validation_status': 'not checked', 

1272 'country': 'China', 

1273 'state': 'Shandong Sheng', 

1274 'type': 'unknown', 

1275 'type_of_area': 'unknown', 

1276 'timezone': 'Asia/Shanghai', 

1277 'additional_metadata': {}, 

1278 'aux_images': [], 

1279 'aux_docs': [], 

1280 'aux_urls': [], 

1281 'globalmeta': {'mean_topography_srtm_alt_90m_year1994': -999.0, 

1282 'mean_topography_srtm_alt_1km_year1994': -999.0, 

1283 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

1284 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

1285 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

1286 'climatic_zone_year2016': '6 (warm temperate dry)', 

1287 'htap_region_tier1_year2010': '10 (SAF Sub Saharan/sub Sahel Africa)', 

1288 'dominant_landcover_year2012': '10 (Cropland, rainfed)', 

1289 'landcover_description_25km_year2012': '', 

1290 'dominant_ecoregion_year2017': '-1 (undefined)', 

1291 'ecoregion_description_25km_year2017': '', 

1292 'distance_to_major_road_year2020': -999.0, 

1293 'mean_stable_nightlights_1km_year2013': -999.0, 

1294 'mean_stable_nightlights_5km_year2013': -999.0, 

1295 'max_stable_nightlights_25km_year2013': -999.0, 

1296 'max_stable_nightlights_25km_year1992': -999.0, 

1297 'mean_population_density_250m_year2015': -1.0, 

1298 'mean_population_density_5km_year2015': -1.0, 

1299 'max_population_density_25km_year2015': -1.0, 

1300 'mean_population_density_250m_year1990': -1.0, 

1301 'mean_population_density_5km_year1990': -1.0, 

1302 'max_population_density_25km_year1990': -1.0, 

1303 'mean_nox_emissions_10km_year2015': -999.0, 

1304 'mean_nox_emissions_10km_year2000': -999.0, 

1305 'toar1_category': 'unclassified', 

1306 'toar2_category': 'suburban'}, 

1307 'changelog': []}, 

1308 'variable': {'name': 'o3', 

1309 'longname': 'ozone', 

1310 'displayname': 'Ozone', 

1311 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

1312 'units': 'nmol mol-1', 

1313 'chemical_formula': 'O3', 

1314 'id': 5}, 

1315 'programme': {'id': 0, 

1316 'name': '', 

1317 'longname': '', 

1318 'homepage': '', 

1319 'description': ''}, 

1320 'roles': [{'id': 1, 

1321 'role': 'resource provider', 

1322 'status': 'active', 

1323 'contact': {'id': 5, 

1324 'organisation': {'id': 2, 

1325 'name': 'FZJ', 

1326 'longname': 'Forschungszentrum Jülich', 

1327 'kind': 'research', 

1328 'city': 'Jülich', 

1329 'postcode': '52425', 

1330 'street_address': 'Wilhelm-Johnen-Straße', 

1331 'country': 'Germany', 

1332 'homepage': 'https://www.fz-juelich.de', 

1333 'contact_url': 'mailto:toar-data@fz-juelich.de' 

1334 } 

1335 } 

1336 } 

1337 ] 

1338 }, 

1339 { 

1340 'additional_metadata': { 

1341 'original_units': 'ppb', 

1342 }, 

1343 'aggregation': 'mean', 

1344 'coverage': -1.0, 

1345 'data_end_date': '2025-02-25T14:00:00+00:00', 

1346 'data_origin': 'instrument', 

1347 'data_origin_type': 'measurement', 

1348 'data_start_date': '1991-01-01T00:00:00+00:00', 

1349 'doi': '', 

1350 'id': 18763, 

1351 'label': '', 

1352 'order': 1, 

1353 'programme': { 

1354 'description': '', 

1355 'homepage': '', 

1356 'id': 0, 

1357 'longname': '', 

1358 'name': '', 

1359 }, 

1360 'provider_version': 'N/A', 

1361 'roles': [ 

1362 { 

1363 'contact': { 

1364 'id': 5, 

1365 'organisation': { 

1366 'city': 'Jülich', 

1367 'contact_url': 'mailto:toar-data@fz-juelich.de', 

1368 'country': 'Germany', 

1369 'homepage': 'https://www.fz-juelich.de', 

1370 'id': 2, 

1371 'kind': 'research', 

1372 'longname': 'Forschungszentrum Jülich', 

1373 'name': 'FZJ', 

1374 'postcode': '52425', 

1375 'street_address': 'Wilhelm-Johnen-Straße', 

1376 }, 

1377 }, 

1378 'id': 1, 

1379 'role': 'resource provider', 

1380 'status': 'active', 

1381 }, 

1382 ], 

1383 'sampling_frequency': 'hourly', 

1384 'sampling_height': 7.0, 

1385 'station': { 

1386 'additional_metadata': { 

1387 'add_type': 'nature reservation', 

1388 }, 

1389 'aux_docs': [], 

1390 'aux_images': [], 

1391 'aux_urls': [], 

1392 'changelog': [], 

1393 'codes': [ 

1394 'SDZ54421', 

1395 ], 

1396 'coordinate_validation_status': 'not checked', 

1397 'coordinates': { 

1398 'alt': 293.9, 

1399 'lat': 40.65, 

1400 'lng': 117.106, 

1401 }, 

1402 'country': 'China', 

1403 'globalmeta': { 

1404 'climatic_zone_year2016': '6 (warm temperate dry)', 

1405 'distance_to_major_road_year2020': -999.0, 

1406 'dominant_ecoregion_year2017': '-1 (undefined)', 

1407 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

1408 'ecoregion_description_25km_year2017': '', 

1409 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

1410 'landcover_description_25km_year2012': '', 

1411 'max_population_density_25km_year1990': -1.0, 

1412 'max_population_density_25km_year2015': -1.0, 

1413 'max_stable_nightlights_25km_year1992': -999.0, 

1414 'max_stable_nightlights_25km_year2013': -999.0, 

1415 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

1416 'mean_nox_emissions_10km_year2000': -999.0, 

1417 'mean_nox_emissions_10km_year2015': -999.0, 

1418 'mean_population_density_250m_year1990': -1.0, 

1419 'mean_population_density_250m_year2015': -1.0, 

1420 'mean_population_density_5km_year1990': -1.0, 

1421 'mean_population_density_5km_year2015': -1.0, 

1422 'mean_stable_nightlights_1km_year2013': -999.0, 

1423 'mean_stable_nightlights_5km_year2013': -999.0, 

1424 'mean_topography_srtm_alt_1km_year1994': -999.0, 

1425 'mean_topography_srtm_alt_90m_year1994': -999.0, 

1426 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

1427 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

1428 'toar1_category': 'unclassified', 

1429 'toar2_category': 'suburban', 

1430 }, 

1431 'id': 2, 

1432 'name': 'Shangdianzi', 

1433 'state': 'Beijing Shi', 

1434 'timezone': 'Asia/Shanghai', 

1435 'type': 'unknown', 

1436 'type_of_area': 'unknown', 

1437 }, 

1438 'variable': { 

1439 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

1440 'chemical_formula': 'O3', 

1441 'displayname': 'Ozone', 

1442 'id': 5, 

1443 'longname': 'ozone', 

1444 'name': 'o3', 

1445 'units': 'nmol mol-1', 

1446 }, 

1447 }, 

1448 { 

1449 'additional_metadata': { 

1450 'original_units': 'ppb', 

1451 }, 

1452 'aggregation': 'mean', 

1453 'coverage': -1.0, 

1454 'data_end_date': '2025-02-25T14:00:00+00:00', 

1455 'data_origin': 'instrument', 

1456 'data_origin_type': 'measurement', 

1457 'data_start_date': '1991-01-01T00:00:00+00:00', 

1458 'doi': '', 

1459 'id': 30890, 

1460 'label': '', 

1461 'order': 2, 

1462 'programme': { 

1463 'description': '', 

1464 'homepage': '', 

1465 'id': 0, 

1466 'longname': '', 

1467 'name': '', 

1468 }, 

1469 'provider_version': 'N/A', 

1470 'roles': [ 

1471 { 

1472 'contact': { 

1473 'id': 5, 

1474 'organisation': { 

1475 'city': 'Jülich', 

1476 'contact_url': 'mailto:toar-data@fz-juelich.de', 

1477 'country': 'Germany', 

1478 'homepage': 'https://www.fz-juelich.de', 

1479 'id': 2, 

1480 'kind': 'research', 

1481 'longname': 'Forschungszentrum Jülich', 

1482 'name': 'FZJ', 

1483 'postcode': '52425', 

1484 'street_address': 'Wilhelm-Johnen-Straße', 

1485 }, 

1486 }, 

1487 'id': 1, 

1488 'role': 'resource provider', 

1489 'status': 'active', 

1490 }, 

1491 ], 

1492 'sampling_frequency': 'hourly', 

1493 'sampling_height': 7.0, 

1494 'station': { 

1495 'additional_metadata': { 

1496 'add_type': 'nature reservation', 

1497 }, 

1498 'aux_docs': [], 

1499 'aux_images': [], 

1500 'aux_urls': [], 

1501 'changelog': [], 

1502 'codes': [ 

1503 'SDZ54421', 

1504 ], 

1505 'coordinate_validation_status': 'not checked', 

1506 'coordinates': { 

1507 'alt': 293.9, 

1508 'lat': 40.65, 

1509 'lng': 117.106, 

1510 }, 

1511 'country': 'China', 

1512 'globalmeta': { 

1513 'climatic_zone_year2016': '6 (warm temperate dry)', 

1514 'distance_to_major_road_year2020': -999.0, 

1515 'dominant_ecoregion_year2017': '-1 (undefined)', 

1516 'dominant_landcover_year2012': '11 (Cropland, rainfed, herbaceous cover)', 

1517 'ecoregion_description_25km_year2017': '', 

1518 'htap_region_tier1_year2010': '11 (MDE Middle East: S. Arabia, Oman, etc, Iran, Iraq)', 

1519 'landcover_description_25km_year2012': '', 

1520 'max_population_density_25km_year1990': -1.0, 

1521 'max_population_density_25km_year2015': -1.0, 

1522 'max_stable_nightlights_25km_year1992': -999.0, 

1523 'max_stable_nightlights_25km_year2013': -999.0, 

1524 'max_topography_srtm_relative_alt_5km_year1994': -999.0, 

1525 'mean_nox_emissions_10km_year2000': -999.0, 

1526 'mean_nox_emissions_10km_year2015': -999.0, 

1527 'mean_population_density_250m_year1990': -1.0, 

1528 'mean_population_density_250m_year2015': -1.0, 

1529 'mean_population_density_5km_year1990': -1.0, 

1530 'mean_population_density_5km_year2015': -1.0, 

1531 'mean_stable_nightlights_1km_year2013': -999.0, 

1532 'mean_stable_nightlights_5km_year2013': -999.0, 

1533 'mean_topography_srtm_alt_1km_year1994': -999.0, 

1534 'mean_topography_srtm_alt_90m_year1994': -999.0, 

1535 'min_topography_srtm_relative_alt_5km_year1994': -999.0, 

1536 'stddev_topography_srtm_relative_alt_5km_year1994': -999.0, 

1537 'toar1_category': 'unclassified', 

1538 'toar2_category': 'suburban', 

1539 }, 

1540 'id': 2, 

1541 'name': 'Shangdianzi', 

1542 'state': 'Beijing Shi', 

1543 'timezone': 'Asia/Shanghai', 

1544 'type': 'unknown', 

1545 'type_of_area': 'unknown', 

1546 }, 

1547 'variable': { 

1548 'cf_standardname': 'mole_fraction_of_ozone_in_air', 

1549 'chemical_formula': 'O3', 

1550 'displayname': 'Ozone', 

1551 'id': 5, 

1552 'longname': 'ozone', 

1553 'name': 'o3', 

1554 'units': 'nmol mol-1', 

1555 } 

1556 }] 

1557 assert response.json() == expected_response 

1558 

1559 

1560 def test_get_timeseries_not_found(self, client, db): 

1561 response = client.get("/timeseries/-1") 

1562 expected_status_code = 404 

1563 assert response.status_code == expected_status_code 

1564 expected_response = {"detail": "Timeseries not found."} 

1565 assert response.json() == expected_response 

1566 

1567 

1568 def test_get_timeseries_invalid_type(self, client, db): 

1569 response = client.get("/timeseries/A") 

1570 expected_status_code = 422 

1571 assert response.status_code == expected_status_code 

1572 """expected_response = {"detail": "Timeseries not found."} 

1573 assert response.json() == expected_response""" 

1574 

1575 def test_get_timeseries2_not_found(self, client, db): 

1576 response = client.get("/timeseries/id/-1") 

1577 expected_status_code = 404 

1578 assert response.status_code == expected_status_code 

1579 expected_response = {"detail": "Timeseries not found."} 

1580 assert response.json() == expected_response 

1581 

1582 def test_get_timeseries2_invalid_type(self, client, db): 

1583 response = client.get("/timeseries/id/A") 

1584 expected_status_code = 422 

1585 assert response.status_code == expected_status_code 

1586 """expected_response = {"detail": "Timeseries not found."} 

1587 assert response.json() == expected_response""" 

1588 

1589 

1590# def test_get_unique_timeseries(self, client, db): 

1591# response = client.get("/timeseries/unique/?station_id=2&variable_id=7") 

1592# expected_status_code = 200 

1593# assert response.status_code == expected_status_code 

1594 

1595# assert response.json() == client.get("/timeseries/1").json() 

1596 

1597 

1598# def test_get_unique_timeseries_not_found(self, client, db): 

1599# response = client.get("/timeseries/unique/?station_id=-1&variable_id=-1") 

1600# expected_status_code = 404 

1601# assert response.status_code == expected_status_code 

1602# expected_response = {"detail": "Timeseries not found."} 

1603# assert response.json() == expected_response 

1604 

1605 

1606 def test_get_unique_timeseries_invalid_type(self, client, db): 

1607 response = client.get("/timeseries/unique/?station_id=A&variable_id=A") 

1608 expected_status_code = 422 

1609 assert response.status_code == expected_status_code 

1610 """expected_response = {"detail": "Timeseries not found."} 

1611 assert response.json() == expected_response""" 

1612 

1613 

1614 def test_get_citation(self, client, db): 

1615 fixed_time = datetime(2023, 7, 28, 12, 0, 0) 

1616 

1617 with patch('toardb.timeseries.crud.dt.datetime') as mock_datetime: 

1618 mock_datetime.now.return_value = fixed_time 

1619 response = client.get("/timeseries/citation/2") 

1620 expected_status_code = 200 

1621 assert response.status_code == expected_status_code 

1622 expected_response = {'attribution': 'Test-Attributions to be announced', 

1623 'citation': 'Forschungszentrum Jülich: time series of o3 at Test_China, accessed from the TOAR database on 2023-07-28 12:00:00', 

1624 'license': 'This data is published under a Creative Commons Attribution 4.0 International (CC BY 4.0). https://creativecommons.org/licenses/by/4.0/' 

1625 } 

1626 assert response.json() == expected_response 

1627 

1628 

1629 # test the endpoint needed for TOARgridding 

1630 def test_get_contributors_json(self, client, db): 

1631 response = client.post("/timeseries/request_contributors?format=json", files={"file": open("tests/fixtures/timeseries/timeseries_id.txt", "rb")}) 

1632 expected_status_code = 200 

1633 assert response.status_code == expected_status_code 

1634 expected_response = [ 

1635 {'contact': {'id': 5, 

1636 'organisation': {'city': 'Jülich', 

1637 'contact_url': 'mailto:toar-data@fz-juelich.de', 

1638 'country': 'Germany', 

1639 'homepage': 'https://www.fz-juelich.de', 

1640 'id': 2, 

1641 'kind': 'research', 

1642 'longname': 'Forschungszentrum Jülich', 

1643 'name': 'FZJ', 

1644 'postcode': '52425', 

1645 'street_address': 'Wilhelm-Johnen-Straße' 

1646 }, 

1647 'person': None 

1648 }, 

1649 'id': 1, 

1650 'role': 'resource provider', 

1651 'status': 'active' 

1652 }, 

1653 {'contact': {'id': 4, 

1654 'organisation': {'city': 'Dessau-Roßlau', 

1655 'contact_url': 'mailto:immission@uba.de', 

1656 'country': 'Germany', 

1657 'homepage': 'https://www.umweltbundesamt.de', 

1658 'id': 1, 

1659 'kind': 'government', 

1660 'longname': 'Umweltbundesamt', 

1661 'name': 'UBA', 

1662 'postcode': '06844', 

1663 'street_address': 'Wörlitzer Platz 1' 

1664 }, 

1665 'person': None 

1666 }, 

1667 'id': 2, 

1668 'role': 'resource provider', 

1669 'status': 'active' 

1670 }, 

1671 {'id': 3, 

1672 'role': 'principal investigator', 

1673 'status': 'active', 

1674 'contact': {'id': 3, 

1675 'organisation': None, 

1676 'person': {'email': 's.schroeder@fz-juelich.de', 

1677 'id': 3, 

1678 'isprivate': False, 

1679 'name': 'Sabine Schröder', 

1680 'orcid': '0000-0002-0309-8010', 

1681 'phone': '+49-2461-61-6397'}}} 

1682 ] 

1683 assert response.json() == expected_response 

1684 

1685 

1686 def test_get_contributors_text(self, client, db): 

1687 response = client.post("/timeseries/request_contributors?format=text", files={"file": open("tests/fixtures/timeseries/timeseries_id.txt", "rb")}) 

1688 expected_status_code = 200 

1689 assert response.status_code == expected_status_code 

1690 expected_response = 'organisations: Forschungszentrum Jülich;Umweltbundesamt; persons:Sabine Schröder' 

1691 assert response.json() == expected_response 

1692 

1693 

1694 def test_get_contributors_default(self, client, db): 

1695 response = client.post("/timeseries/request_contributors", files={"file": open("tests/fixtures/timeseries/timeseries_id.txt", "rb")}) 

1696 expected_status_code = 200 

1697 assert response.status_code == expected_status_code 

1698 expected_response = 'organisations: Forschungszentrum Jülich;Umweltbundesamt; persons:Sabine Schröder' 

1699 assert response.json() == expected_response 

1700 

1701 

1702 def test_get_contributors_unknown_format(self, client, db): 

1703 response = client.post("/timeseries/request_contributors?format=CMOR", files={"file": open("tests/fixtures/timeseries/timeseries_id.txt", "rb")}) 

1704 expected_status_code = 400 

1705 assert response.status_code == expected_status_code 

1706 expected_response = 'not a valid format: CMOR' 

1707 assert response.json() == expected_response 

1708 

1709 

1710 def test_register_contributors_list(self, client, db): 

1711 response = client.post("/timeseries/register_timeseries_list_of_contributors/5f0df73a-bd0f-48b9-bb17-d5cd36f89598", 

1712 data='''[1,2]''', 

1713 headers={"email": "s.schroeder@fz-juelich.de"} ) 

1714 expected_status_code = 200 

1715 assert response.status_code == expected_status_code 

1716 expected_response = '5f0df73a-bd0f-48b9-bb17-d5cd36f89598 successfully registered.' 

1717 assert response.json() == expected_response 

1718 

1719 

1720 def test_register_duplicate_contributors_list(self, client, db): 

1721 response = client.post("/timeseries/register_timeseries_list_of_contributors/7f0df73a-bd0f-48b9-bb17-d5cd36f89598", 

1722 data='''[1,2]''', 

1723 headers={"email": "s.schroeder@fz-juelich.de"} ) 

1724 expected_status_code = 443 

1725 assert response.status_code == expected_status_code 

1726 expected_response = '7f0df73a-bd0f-48b9-bb17-d5cd36f89598 already registered.' 

1727 assert response.json() == expected_response 

1728 

1729 

1730 def test_request_registered_contributors_list_json(self, client, db): 

1731 response = client.get("/timeseries/request_timeseries_list_of_contributors/7f0df73a-bd0f-48b9-bb17-d5cd36f89598?format=json") 

1732 expected_status_code = 200 

1733 assert response.status_code == expected_status_code 

1734 expected_response = [ 

1735 {'contact': {'id': 5, 

1736 'organisation': {'city': 'Jülich', 

1737 'contact_url': 'mailto:toar-data@fz-juelich.de', 

1738 'country': 'Germany', 

1739 'homepage': 'https://www.fz-juelich.de', 

1740 'id': 2, 

1741 'kind': 'research', 

1742 'longname': 'Forschungszentrum Jülich', 

1743 'name': 'FZJ', 

1744 'postcode': '52425', 

1745 'street_address': 'Wilhelm-Johnen-Straße' 

1746 }, 

1747 'person': None 

1748 }, 

1749 'id': 1, 

1750 'role': 'resource provider', 

1751 'status': 'active' 

1752 }, 

1753 {'contact': {'id': 4, 

1754 'organisation': {'city': 'Dessau-Roßlau', 

1755 'contact_url': 'mailto:immission@uba.de', 

1756 'country': 'Germany', 

1757 'homepage': 'https://www.umweltbundesamt.de', 

1758 'id': 1, 

1759 'kind': 'government', 

1760 'longname': 'Umweltbundesamt', 

1761 'name': 'UBA', 

1762 'postcode': '06844', 

1763 'street_address': 'Wörlitzer Platz 1' 

1764 }, 

1765 'person': None 

1766 }, 

1767 'id': 2, 

1768 'role': 'resource provider', 

1769 'status': 'active' 

1770 }, 

1771 {'id': 3, 

1772 'role': 'principal investigator', 

1773 'status': 'active', 

1774 'contact': {'id': 3, 

1775 'organisation': None, 

1776 'person': {'email': 's.schroeder@fz-juelich.de', 

1777 'id': 3, 

1778 'isprivate': False, 

1779 'name': 'Sabine Schröder', 

1780 'orcid': '0000-0002-0309-8010', 

1781 'phone': '+49-2461-61-6397'}}} 

1782 ] 

1783 assert response.json() == expected_response 

1784 

1785 

1786 def test_request_registered_contributors_list_text(self, client, db): 

1787 response = client.get("/timeseries/request_timeseries_list_of_contributors/7f0df73a-bd0f-48b9-bb17-d5cd36f89598?format=text") 

1788 expected_status_code = 200 

1789 assert response.status_code == expected_status_code 

1790 expected_response = 'organisations: Forschungszentrum Jülich;Umweltbundesamt; persons:Sabine Schröder' 

1791 assert response.json() == expected_response 

1792 

1793 

1794 def test_request_registered_contributors_list_unknown_rid(self, client, db): 

1795 response = client.get("/timeseries/request_timeseries_list_of_contributors/7f0df73a-bd0f-58b9-bb17-d5cd36f89598?format=text") 

1796 expected_status_code = 400 

1797 assert response.status_code == expected_status_code 

1798 expected_response = 'not a registered request id: 7f0df73a-bd0f-58b9-bb17-d5cd36f89598' 

1799 assert response.json() == expected_response 

1800 

1801 

1802 # 3. tests updating timeseries metadata 

1803 

1804 def test_patch_timeseries_no_description(self, client, db): 

1805 response = client.patch("/timeseries/id/1", 

1806 json={"timeseries": 

1807 {"sampling_frequency":"Daily"} 

1808 }, 

1809 headers={"email": "s.schroeder@fz-juelich.de"} 

1810 ) 

1811 expected_status_code = 404 

1812 assert response.status_code == expected_status_code 

1813 expected_resp = {"detail": "description text ist missing."} 

1814 assert response.json() == expected_resp 

1815 

1816 

1817 def test_patch_timeseries_not_found(self, client, db): 

1818 response = client.patch("/timeseries/id/-1?description=changed sampling_frequency", 

1819 json={"timeseries": 

1820 {"sampling_frequency":"Daily"} 

1821 }, 

1822 headers={"email": "s.schroeder@fz-juelich.de"} 

1823 ) 

1824 expected_status_code = 404 

1825 assert response.status_code == expected_status_code 

1826 expected_resp = {"detail": "Time series for patching not found."} 

1827 assert response.json() == expected_resp 

1828 

1829 

1830 def test_patch_timeseries_single_item(self, client, db): 

1831 response = client.patch("/timeseries/id/1?description=changed sampling_frequency", 

1832 json={"timeseries": 

1833 {"sampling_frequency": "Daily"} 

1834 }, 

1835 headers={"email": "s.schroeder@fz-juelich.de"} 

1836 ) 

1837 expected_status_code = 200 

1838 assert response.status_code == expected_status_code 

1839 expected_resp = {'detail': { 'message': 'timeseries patched.', 

1840 'timeseries_id': 1 } 

1841 } 

1842 response_json = response.json() 

1843 assert response_json == expected_resp 

1844 response = client.get(f"/timeseries/id/{response_json['detail']['timeseries_id']}") 

1845 response_json = response.json() 

1846 # just check special changes 

1847 assert response_json['sampling_frequency'] == "daily" 

1848 assert response_json['changelog'][0]['old_value'] == "{'sampling_frequency': 'Hourly'}" 

1849 assert response_json['changelog'][0]['new_value'] == "{'sampling_frequency': 'Daily'}" 

1850 assert response_json['changelog'][0]['author_id'] == 1 

1851 assert response_json['changelog'][0]['type_of_change'] == 'single value correction in metadata' 

1852 

1853 

1854 def test_patch_timeseries_multiple_items(self, client, db): 

1855 response = client.patch("/timeseries/id/1?description=changed some metadata", 

1856 json={"timeseries": 

1857 {"sampling_frequency": "Daily", 

1858 "aggregation": "MeanOf4Samples", 

1859 "data_origin": "COSMOREA6", 

1860 "data_origin_type": "Model"} 

1861 }, 

1862 headers={"email": "s.schroeder@fz-juelich.de"} 

1863 ) 

1864 expected_status_code = 200 

1865 assert response.status_code == expected_status_code 

1866 expected_resp = {'detail': { 'message': 'timeseries patched.', 

1867 'timeseries_id': 1 } 

1868 } 

1869 response_json = response.json() 

1870 assert response_json == expected_resp 

1871 response = client.get(f"/timeseries/id/{response_json['detail']['timeseries_id']}") 

1872 response_json = response.json() 

1873 # just check special changes 

1874 assert response_json['sampling_frequency'] == "daily" 

1875 assert response_json['changelog'][0]['old_value'] == "{'sampling_frequency': 'Hourly', 'aggregation': 'Mean', 'data_origin': 'Instrument', 'data_origin_type': 'Measurement'}" 

1876 assert response_json['changelog'][0]['new_value'] == "{'sampling_frequency': 'Daily', 'aggregation': 'MeanOf4Samples', 'data_origin': 'COSMOREA6', 'data_origin_type': 'Model'}" 

1877 assert response_json['changelog'][0]['author_id'] == 1 

1878 assert response_json['changelog'][0]['type_of_change'] == 'comprehensive metadata revision' 

1879 

1880 

1881 def test_patch_timeseries_roles(self, client, db): 

1882 response = client.patch("/timeseries/id/1?description=changed roles", 

1883 json={"timeseries": 

1884 {"roles": [{"role": "ResourceProvider", "contact_id": 5, "status": "Active"}] 

1885 } 

1886 }, 

1887 headers={"email": "s.schroeder@fz-juelich.de"} 

1888 ) 

1889 expected_status_code = 200 

1890 assert response.status_code == expected_status_code 

1891 expected_resp = {'detail': { 'message': 'timeseries patched.', 

1892 'timeseries_id': 1 } 

1893 } 

1894 response_json = response.json() 

1895 assert response_json == expected_resp 

1896 response = client.get(f"/timeseries/id/{response_json['detail']['timeseries_id']}") 

1897 response_json = response.json() 

1898 # just check special changes 

1899 response_roles = [{'contact': {'id': 5, 

1900 'organisation': {'city': 'Jülich', 

1901 'contact_url': 'mailto:toar-data@fz-juelich.de', 

1902 'country': 'Germany', 

1903 'homepage': 'https://www.fz-juelich.de', 

1904 'id': 2, 

1905 'kind': 'research', 

1906 'longname': 'Forschungszentrum Jülich', 

1907 'name': 'FZJ', 

1908 'postcode': '52425', 

1909 'street_address': 'Wilhelm-Johnen-Straße', 

1910 }, 

1911 }, 

1912 'id': 1, 

1913 'role': 'resource provider', 

1914 'status': 'active'}, 

1915 {'contact': {'id': 4, 

1916 'organisation': {'city': 'Dessau-Roßlau', 

1917 'contact_url': 'mailto:immission@uba.de', 

1918 'country': 'Germany', 

1919 'homepage': 'https://www.umweltbundesamt.de', 

1920 'id': 1, 

1921 'kind': 'government', 

1922 'longname': 'Umweltbundesamt', 

1923 'name': 'UBA', 

1924 'postcode': '06844', 

1925 'street_address': 'Wörlitzer Platz 1', 

1926 'contact_url': 'mailto:immission@uba.de'}}, 

1927 'id': 2, 

1928 'role': 'resource provider', 

1929 'status': 'active'}, 

1930 {'id': 3, 

1931 'role': 'principal investigator', 

1932 'status': 'active', 

1933 'contact': {'id': 3, 

1934 'person': {'email': 's.schroeder@fz-juelich.de', 

1935 'id': 3, 

1936 'isprivate': False, 

1937 'name': 'Sabine Schröder', 

1938 'orcid': '0000-0002-0309-8010', 

1939 'phone': '+49-2461-61-6397'}}}] 

1940 set_expected_response_roles = {json.dumps(item, sort_keys=True) for item in response_roles} 

1941 set_response_roles = {json.dumps(item, sort_keys=True) for item in response_json['roles']} 

1942 assert set_response_roles == set_expected_response_roles 

1943 assert response_json['changelog'][0]['old_value'] == "{'roles': [{'role': 'ResourceProvider', 'status': 'Active', 'contact_id': 4}, {'role': 'PrincipalInvestigator', 'status': 'Active', 'contact_id': 3}]}" 

1944 assert response_json['changelog'][0]['new_value'] == "{'roles': [{'role': 'ResourceProvider', 'status': 'Active', 'contact_id': 4}, {'role': 'PrincipalInvestigator', 'status': 'Active', 'contact_id': 3}, {'role': 'ResourceProvider', 'contact_id': 5, 'status': 'Active'}]}" 

1945 assert response_json['changelog'][0]['author_id'] == 1 

1946 assert response_json['changelog'][0]['type_of_change'] == 'single value correction in metadata' 

1947 

1948 

1949 def test_patch_timeseries_annotations(self, client, db): 

1950 response = client.patch("/timeseries/id/1?description=changed annotations", 

1951 json={"timeseries": 

1952 {"annotations": [{"kind": "User", 

1953 "text": "some foo", 

1954 "date_added": "2021-07-27 00:00", 

1955 "approved": True, 

1956 "contributor_id":1}] 

1957 } 

1958 }, 

1959 headers={"email": "s.schroeder@fz-juelich.de"} 

1960 ) 

1961 expected_status_code = 200 

1962 assert response.status_code == expected_status_code 

1963 expected_resp = {'detail': { 'message': 'timeseries patched.', 

1964 'timeseries_id': 1 } 

1965 } 

1966 response_json = response.json() 

1967 assert response_json == expected_resp 

1968 response = client.get(f"/timeseries/id/{response_json['detail']['timeseries_id']}") 

1969 response_json = response.json() 

1970 # just check special changes 

1971 response_annotations = [{"kind": "user comment", 

1972 "text": "some foo", 

1973 "date_added": "2021-07-27 00:00", 

1974 "approved": True, 

1975 "contributor_id":1} 

1976 ] 

1977 assert response_json['changelog'][0]['old_value'] == "{'annotations': []}" 

1978 assert response_json['changelog'][0]['new_value'] == "{'annotations': [{'kind': 'User', 'text': 'some foo', 'date_added': '2021-07-27 00:00', 'approved': True, 'contributor_id': 1}]}" 

1979 assert response_json['changelog'][0]['author_id'] == 1 

1980 assert response_json['changelog'][0]['type_of_change'] == 'single value correction in metadata' 

1981 

1982 

1983 """def test_get_timeseries_changelog(self, client, db): 

1984 response = client.get("/timeseries_changelog/{id}") 

1985 expected_status_code = 200 

1986 assert response.status_code == expected_status_code 

1987 new_response = response.json()["datetime"] = "" 

1988 expected_response = [{"datetime":"","description":"fake creation of timeseries","old_value":"","new_value":"","timeseries_id":8,"author_id":1,"type_of_change":0,"period_start":None,"period_end":None,"version":None}] 

1989 assert response.json() == expected_response""" 

1990