Coverage for tests / test_toardb.py: 100%

211 statements  

« 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 

3 

4import pytest 

5import json 

6# Required imports 'create_test_database' 

7import csv 

8from sqlalchemy import insert, MetaData 

9from toardb.test_base import ( 

10 client, 

11 get_test_db, 

12 create_test_database, 

13 url, 

14 get_test_engine, 

15 test_db_session as db, 

16) 

17from toardb.stationmeta.models import StationmetaCore, StationmetaGlobal, StationmetaGlobalService 

18from toardb.auth_user.models import AuthUser 

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

20from toardb.variables.models import Variable 

21from toardb.stationmeta.schemas import get_geom_from_coordinates, Coordinates 

22from toardb.timeseries.models import Timeseries, timeseries_timeseries_roles_table 

23from toardb.timeseries.models_programme import TimeseriesProgramme 

24from toardb.timeseries.models_role import TimeseriesRole 

25from toardb.data.models import Data 

26 

27 

28class TestApps: 

29 def setup(self): 

30 self.application_url = "/controlled_vocabulary" 

31 

32 

33 @pytest.fixture(autouse=True) 

34 def setup_db_data(self, db): 

35 _db_conn = get_test_engine() 

36 fake_conn = _db_conn.raw_connection() 

37 fake_cur = fake_conn.cursor() 

38 # id_seq will not be reset automatically between tests! 

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

40 fake_conn.commit() 

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

42 fake_conn.commit() 

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

44 fake_conn.commit() 

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

46 fake_conn.commit() 

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

48 fake_conn.commit() 

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

50 fake_conn.commit() 

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

52 fake_conn.commit() 

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

54 fake_conn.commit() 

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

56 fake_conn.commit() 

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

58 fake_conn.commit() 

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

60 fake_conn.commit() 

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

62 fake_conn.commit() 

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

64 fake_conn.commit() 

65 fake_cur.execute("ALTER SEQUENCE timeseries_roles_id_seq RESTART WITH 3") 

66 fake_conn.commit() 

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

68 fake_conn.commit() 

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

70 fake_conn.commit() 

71 infilename = "tests/fixtures/stationmeta/stationmeta_global_services.json" 

72 with open(infilename) as f: 

73 metajson=json.load(f) 

74 for entry in metajson: 

75 new_stationmeta_global_service = StationmetaGlobalService(**entry) 

76 db.add(new_stationmeta_global_service) 

77 db.commit() 

78 db.refresh(new_stationmeta_global_service) 

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

80 with open(infilename) as f: 

81 metajson=json.load(f) 

82 for entry in metajson: 

83 new_auth_user = AuthUser(**entry) 

84 db.add(new_auth_user) 

85 db.commit() 

86 db.refresh(new_auth_user) 

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

88 with open(infilename) as f: 

89 metajson=json.load(f) 

90 for entry in metajson: 

91 new_person = Person(**entry) 

92 db.add(new_person) 

93 db.commit() 

94 db.refresh(new_person) 

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

96 with open(infilename) as f: 

97 metajson=json.load(f) 

98 for entry in metajson: 

99 new_organisation = Organisation(**entry) 

100 db.add(new_organisation) 

101 db.commit() 

102 db.refresh(new_organisation) 

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

104 with open(infilename) as f: 

105 metajson=json.load(f) 

106 for entry in metajson: 

107 new_contact = Contact(**entry) 

108 db.add(new_contact) 

109 db.commit() 

110 db.refresh(new_contact) 

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

112 with open(infilename) as f: 

113 metajson=json.load(f) 

114 for entry in metajson: 

115 new_variable = Variable(**entry) 

116 db.add(new_variable) 

117 db.commit() 

118 db.refresh(new_variable) 

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

120 with open(infilename) as f: 

121 metajson=json.load(f) 

122 for entry in metajson: 

123 new_stationmeta_core = StationmetaCore(**entry) 

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

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

126 db.add(new_stationmeta_core) 

127 db.commit() 

128 db.refresh(new_stationmeta_core) 

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

130 with open(infilename) as f: 

131 metajson=json.load(f) 

132 for entry in metajson: 

133 new_stationmeta_global = StationmetaGlobal(**entry) 

134 db.add(new_stationmeta_global) 

135 db.commit() 

136 db.refresh(new_stationmeta_global) 

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

138 with open(infilename) as f: 

139 metajson=json.load(f) 

140 for entry in metajson: 

141 new_timeseries_programme = TimeseriesProgramme(**entry) 

142 db.add(new_timeseries_programme) 

143 db.commit() 

144 db.refresh(new_timeseries_programme) 

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

146 with open(infilename) as f: 

147 metajson=json.load(f) 

148 for entry in metajson: 

149 new_timeseries = Timeseries(**entry) 

150 db.add(new_timeseries) 

151 db.commit() 

152 db.refresh(new_timeseries) 

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

154 with open(infilename) as f: 

155 metajson=json.load(f) 

156 for entry in metajson: 

157 new_timeseries_role = TimeseriesRole(**entry) 

158 db.add(new_timeseries_role) 

159 db.commit() 

160 db.refresh(new_timeseries_role) 

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

162 with open(infilename) as f: 

163 metajson=json.load(f) 

164 for entry in metajson: 

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

166 db.execute("COMMIT") 

167 infilename = "tests/fixtures/data/data.json" 

168 with open(infilename) as f: 

169 metajson=json.load(f) 

170 for entry in metajson: 

171 new_data = Data (**entry) 

172 db.add(new_data) 

173 db.commit() 

174 db.refresh(new_data) 

175 infilename = "tests/fixtures/data/staging_data.json" 

176 with open(infilename) as f: 

177 metajson=json.load(f) 

178 for entry in metajson: 

179 new_data = Data (**entry) 

180 fake_cur.execute(("INSERT INTO staging.data(datetime, value, flags, timeseries_id, version) " 

181 f" VALUES('{new_data.datetime}', {new_data.value}, {new_data.flags}, " 

182 f"{new_data.timeseries_id}, '{new_data.version}');")) 

183 fake_conn.commit() 

184 with open("tests/fixtures/data/yearly_coverage.csv", newline="", encoding="utf-8") as f: 

185 metadata = MetaData() 

186 metadata.reflect(bind=_db_conn) 

187 table = metadata.tables["yearly_coverage"] 

188 reader = csv.DictReader(f) 

189 rows = list(reader) 

190 db.execute(insert(table), rows) 

191 db.commit() 

192 

193 

194 def test_get_controlled_vocabulary(self, client, db): 

195 response = client.get("/controlled_vocabulary") 

196 expected_status_code = 200 

197 assert response.status_code == expected_status_code 

198 expected_resp = {"Station Landcover Type": 

199 [[-1, 'Undefined', '-1 (undefined)'], 

200 [0, 'NoData', '0 (No Data)'], 

201 [10, 'CroplandRainfed', '10 (Cropland, rainfed)'], 

202 [11, 'CroplandRainfedHerbaceousCover', '11 (Cropland, rainfed, herbaceous cover)'], 

203 [12, 'CroplandRainfedTreeOrShrubCover', '12 (Cropland, rainfed, tree or shrub cover)'], 

204 [20, 'CroplandIrrigated', '20 (Cropland, irrigated or post-flooding)'], 

205 [30, 'MosaicCropland', '30 (Mosaic cropland (>50%) / natural vegetation (tree, shrub, herbaceous cover) (<50%))'], 

206 [40, 'MosaicNaturalVegetation', '40 (Mosaic natural vegetation (tree, shrub, herbaceous cover) (>50%) / cropland (<50%))'], 

207 [50, 'TreeBroadleavedEvergreenClosedToOpen', '50 (Tree cover, broadleaved, evergreen, closed to open (>15%))'], 

208 [60, 'TreeBroadleavedDeciduousClosedToOpen', '60 (Tree cover, broadleaved, deciduous, closed to open (>15%))'], 

209 [61, 'TreeBroadleavedDeciduousClosed', '61 (Tree cover, broadleaved, deciduous, closed (>40%))'], 

210 [62, 'TreeBroadleavedDeciduousOpen', '62 (Tree cover, broadleaved, deciduous, open (15-40%))'], 

211 [70, 'TreeNeedleleavedEvergreenClosedToOpen', '70 (Tree cover, needleleaved, evergreen, closed to open (>15%))'], 

212 [71, 'TreeNeedleleavedEvergreenClosed', '71 (Tree cover, needleleaved, evergreen, closed (>40%))'], 

213 [72, 'TreeNeedleleavedEvergreenOpen', '72 (Tree cover, needleleaved, evergreen, open (15-40%))'], 

214 [80, 'TreeNeedleleavedDeciduousClosedToOpen', '80 (Tree cover, needleleaved, deciduous, closed to open (>15%))'], 

215 [81, 'TreeNedleleavedDeciduousClosed', '81 (Tree cover, needleleaved, deciduous, closed (>40%))'], 

216 [82, 'TreeNeedleleavedDeciduousOpen', '82 (Tree cover, needleleaved, deciduous, open (15-40%))'], 

217 [90, 'TreeMixed', '90 (Tree cover, mixed leaf type (broadleaved and needleleaved))'], 

218 [100, 'MosaicTreeAndShrub', '100 (Mosaic tree and shrub (>50%) / herbaceous cover (<50%))'], 

219 [110, 'MosaicHerbaceous', '110 (Mosaic herbaceous cover (>50%) / tree and shrub (<50%))'], 

220 [120, 'Shrubland', '120 (Shrubland)'], 

221 [121, 'ShrublandEvergreen', '121 (Evergreen shrubland)'], 

222 [122, 'ShrublandDeciduous', '122 (Deciduous shrubland)'], 

223 [130, 'Grassland', '130 (Grassland)'], 

224 [140, 'LichensAndMosses', '140 (Lichens and mosses)'], 

225 [150, 'SparseVegetation', '150 (Sparse vegetation (tree, shrub, herbaceous cover) (<15%))'], 

226 [151, 'SparseTree', '151 (Sparse tree (<15%))'], 

227 [152, 'SparseShrub', '152 (Sparse shrub (<15%))'], 

228 [153, 'SparseHerbaceous', '153 (Sparse herbaceous cover (<15%))'], 

229 [160, 'TreeCoverFloodedFreshOrBrakishWater', '160 (Tree cover, flooded, fresh or brakish water)'], 

230 [170, 'TreeCoverFloodedSalineWater', '170 (Tree cover, flooded, saline water)'], 

231 [180, 'ShrubOrHerbaceousCoverFlooded', '180 (Shrub or herbaceous cover, flooded, fresh/saline/brakish water)'], 

232 [190, 'Urban', '190 (Urban areas)'], 

233 [200, 'BareAreas', '200 (Bare areas)'], 

234 [201, 'BareAreasConsolidated', '201 (Consolidated bare areas)'], 

235 [202, 'BareAreasUnconsolidated', '202 (Unconsolidated bare areas)'], 

236 [210, 'Water', '210 (Water bodies)'], 

237 [220, 'SnowAndIce', '220 (Permanent snow and ice)']], 

238 "Climatic Zone 2019": 

239 [[-1,"Undefined","-1 (undefined)"], 

240 [0,"Unclassified","0 (unclassified)"], 

241 [1, 'TropicalMontane', '1 (tropical montane)'], 

242 [2, 'TropicalWet', '2 (tropical wet)'], 

243 [3, 'TropicalMoist', '3 (tropical moist)'], 

244 [4, 'TropicalDry', '4 (tropical dry)'], 

245 [5, 'WarmTemperateMoist', '5 (warm temperate moist)'], 

246 [6, 'WarmTemperateDry', '6 (warm temperate dry)'], 

247 [7, 'CoolTemperateMoist', '7 (cool temperate moist)'], 

248 [8, 'CoolTemperateDry', '8 (cool temperate dry)'], 

249 [9, 'BorealMoist', '9 (boreal moist)'], 

250 [10, 'BorealDry', '10 (boreal dry)'], 

251 [11, 'PolarMoist', '11 (polar moist)'], 

252 [12, 'PolarDry', '12 (polar dry)']] } 

253 assert response.json()["Station Landcover Type"] == expected_resp["Station Landcover Type"] 

254 assert response.json()["Climatic Zone 2019"] == expected_resp["Climatic Zone 2019"] 

255 

256 

257 

258 def test_get_controlled_vocabulary_field(self, client, db): 

259 response = client.get("/controlled_vocabulary/Station Landcover Type") 

260 expected_status_code = 200 

261 assert response.status_code == expected_status_code 

262 expected_resp = [[-1, 'Undefined', '-1 (undefined)'], 

263 [0, 'NoData', '0 (No Data)'], 

264 [10, 'CroplandRainfed', '10 (Cropland, rainfed)'], 

265 [11, 'CroplandRainfedHerbaceousCover', '11 (Cropland, rainfed, herbaceous cover)'], 

266 [12, 'CroplandRainfedTreeOrShrubCover', '12 (Cropland, rainfed, tree or shrub cover)'], 

267 [20, 'CroplandIrrigated', '20 (Cropland, irrigated or post-flooding)'], 

268 [30, 'MosaicCropland', '30 (Mosaic cropland (>50%) / natural vegetation (tree, shrub, herbaceous cover) (<50%))'], 

269 [40, 'MosaicNaturalVegetation', '40 (Mosaic natural vegetation (tree, shrub, herbaceous cover) (>50%) / cropland (<50%))'], 

270 [50, 'TreeBroadleavedEvergreenClosedToOpen', '50 (Tree cover, broadleaved, evergreen, closed to open (>15%))'], 

271 [60, 'TreeBroadleavedDeciduousClosedToOpen', '60 (Tree cover, broadleaved, deciduous, closed to open (>15%))'], 

272 [61, 'TreeBroadleavedDeciduousClosed', '61 (Tree cover, broadleaved, deciduous, closed (>40%))'], 

273 [62, 'TreeBroadleavedDeciduousOpen', '62 (Tree cover, broadleaved, deciduous, open (15-40%))'], 

274 [70, 'TreeNeedleleavedEvergreenClosedToOpen', '70 (Tree cover, needleleaved, evergreen, closed to open (>15%))'], 

275 [71, 'TreeNeedleleavedEvergreenClosed', '71 (Tree cover, needleleaved, evergreen, closed (>40%))'], 

276 [72, 'TreeNeedleleavedEvergreenOpen', '72 (Tree cover, needleleaved, evergreen, open (15-40%))'], 

277 [80, 'TreeNeedleleavedDeciduousClosedToOpen', '80 (Tree cover, needleleaved, deciduous, closed to open (>15%))'], 

278 [81, 'TreeNedleleavedDeciduousClosed', '81 (Tree cover, needleleaved, deciduous, closed (>40%))'], 

279 [82, 'TreeNeedleleavedDeciduousOpen', '82 (Tree cover, needleleaved, deciduous, open (15-40%))'], 

280 [90, 'TreeMixed', '90 (Tree cover, mixed leaf type (broadleaved and needleleaved))'], 

281 [100, 'MosaicTreeAndShrub', '100 (Mosaic tree and shrub (>50%) / herbaceous cover (<50%))'], 

282 [110, 'MosaicHerbaceous', '110 (Mosaic herbaceous cover (>50%) / tree and shrub (<50%))'], 

283 [120, 'Shrubland', '120 (Shrubland)'], 

284 [121, 'ShrublandEvergreen', '121 (Evergreen shrubland)'], 

285 [122, 'ShrublandDeciduous', '122 (Deciduous shrubland)'], 

286 [130, 'Grassland', '130 (Grassland)'], 

287 [140, 'LichensAndMosses', '140 (Lichens and mosses)'], 

288 [150, 'SparseVegetation', '150 (Sparse vegetation (tree, shrub, herbaceous cover) (<15%))'], 

289 [151, 'SparseTree', '151 (Sparse tree (<15%))'], 

290 [152, 'SparseShrub', '152 (Sparse shrub (<15%))'], 

291 [153, 'SparseHerbaceous', '153 (Sparse herbaceous cover (<15%))'], 

292 [160, 'TreeCoverFloodedFreshOrBrakishWater', '160 (Tree cover, flooded, fresh or brakish water)'], 

293 [170, 'TreeCoverFloodedSalineWater', '170 (Tree cover, flooded, saline water)'], 

294 [180, 'ShrubOrHerbaceousCoverFlooded', '180 (Shrub or herbaceous cover, flooded, fresh/saline/brakish water)'], 

295 [190, 'Urban', '190 (Urban areas)'], 

296 [200, 'BareAreas', '200 (Bare areas)'], 

297 [201, 'BareAreasConsolidated', '201 (Consolidated bare areas)'], 

298 [202, 'BareAreasUnconsolidated', '202 (Unconsolidated bare areas)'], 

299 [210, 'Water', '210 (Water bodies)'], 

300 [220, 'SnowAndIce', '220 (Permanent snow and ice)']] 

301 assert response.json() == expected_resp 

302 

303 

304 def test_get_controlled_vocabulary_unknown_field(self, client, db): 

305 response = client.get("/controlled_vocabulary/Station Landuse Type") 

306 expected_status_code = 200 

307 assert response.status_code == expected_status_code 

308 expected_resp = "No controlled vocabulary found for 'Station Landuse Type'" 

309 assert response.json() == expected_resp 

310 

311 

312 def test_get_database_statistics(self, client, db): 

313 response = client.get("/database_statistics") 

314 expected_status_code = 200 

315 assert response.status_code == expected_status_code 

316 # since returned data records are just estimated, this means that they are treated as 0 

317 expected_resp = {'data records': 0, 'stations': 3, 'time-series': 5, 'users': 1} 

318 assert response.json() == expected_resp 

319 

320 

321 def test_get_database_statistics_field(self, client, db): 

322 response = client.get("/database_statistics/stations") 

323 expected_status_code = 200 

324 assert response.status_code == expected_status_code 

325 expected_resp = 3 

326 assert response.json() == expected_resp 

327 

328 

329 def test_get_geopeas_urls(self, client, db): 

330 response = client.get("/geopeas_urls") 

331 expected_status_code = 200 

332 assert response.status_code == expected_status_code 

333 expected_resp = [ 

334 { 

335 "service_url":"{base_url}/climatic_zone/?lat={lat}&lon={lon}", 

336 "variable_name":"climatic_zone_year2016" 

337 }, 

338 { 

339 "service_url":"{base_url}/major_road/?lat={lat}&lon={lon}", 

340 "variable_name":"distance_to_major_road_year2020" 

341 }, 

342 { 

343 "service_url":"{base_url}/population_density/?year=2015&lat={lat}&lon={lon}", 

344 "variable_name":"mean_population_density_250m_year2015" 

345 }, 

346 { 

347 "service_url":"{base_url}/population_density/?year=2015&lat={lat}&lon={lon}", 

348 "variable_name":"mean_population_density_5km_year2015" 

349 }, 

350 { 

351 "service_url":"{base_url}/population_density/?agg=max&radius=25000&year=2015&lat={lat}&lon={lon}", 

352 "variable_name":"max_population_density_25km_year2015" 

353 }, 

354 { 

355 "service_url":"{base_url}/population_density/?agg=mean&radius=250&year=1990&lat={lat}&lon={lon}", 

356 "variable_name":"mean_population_density_250m_year1990" 

357 }, 

358 { 

359 "service_url":"{base_url}/population_density/?agg=mean&radius=5000&year=1990&lat={lat}&lon={lon}", 

360 "variable_name":"mean_population_density_5km_year1990" 

361 }, 

362 { 

363 "service_url":"{base_url}/population_density/?agg=max&radius=25000&year=1990&lat={lat}&lon={lon}", 

364 "variable_name":"max_population_density_25km_year1990" 

365 }, 

366 { 

367 "service_url":"{base_url}/nox_emissions/?year=2015&lat={lat}&lon={lon}", 

368 "variable_name":"mean_nox_emissions_10km_year2015" 

369 }, 

370 { 

371 "service_url":"{base_url}/nox_emissions/?year=2000&lat={lat}&lon={lon}", 

372 "variable_name":"mean_nox_emissions_10km_year2000" 

373 }, 

374 { 

375 "service_url":"{base_url}/htap_region_tier1/?lat={lat}&country={country}", 

376 "variable_name":"htap_region_tier1_year2010" 

377 }, 

378 { 

379 "service_url":"{base_url}/ecoregion/?description=false&lat={lat}&lon={lon}", 

380 "variable_name":"dominant_ecoregion_year2017" 

381 }, 

382 { 

383 "service_url":"{base_url}/landcover/?year=2012&description=false&lat={lat}&lon={lon}", 

384 "variable_name":"dominant_landcover_year2012" 

385 }, 

386 { 

387 "service_url":"{base_url}/ecoregion/?description=true&radius=25000&lat={lat}&lon={lon}", 

388 "variable_name":"ecoregion_description_25km_year2017" 

389 }, 

390 { 

391 "service_url":"{base_url}/landcover/?year=2012&radius=25000&description=true&lat={lat}&lon={lon}", 

392 "variable_name":"landcover_description_25km_year2012" 

393 }, 

394 { 

395 "service_url":"{base_url}/topography_srtm/?relative=false&lat={lat}&lon={lon}", 

396 "variable_name":"mean_topography_srtm_alt_90m_year1994" 

397 }, 

398 { 

399 "service_url":"{base_url}/topography_srtm/?relative=false&agg=mean&radius=1000&lat={lat}&lon={lon}", 

400 "variable_name":"mean_topography_srtm_alt_1km_year1994" 

401 }, 

402 { 

403 "service_url":"{base_url}/topography_srtm/?relative=true&agg=max&radius=5000&lat={lat}&lon={lon}", 

404 "variable_name":"max_topography_srtm_relative_alt_5km_year1994" 

405 }, 

406 { 

407 "service_url":"{base_url}/topography_srtm/?relative=true&agg=min&radius=5000&lat={lat}&lon={lon}", 

408 "variable_name":"min_topography_srtm_relative_alt_5km_year1994" 

409 }, 

410 { 

411 "service_url":"{base_url}/topography_srtm/?relative=true&agg=stddev&radius=5000&lat={lat}&lon={lon}", 

412 "variable_name":"stddev_topography_srtm_relative_alt_5km_year1994" 

413 }, 

414 { 

415 "service_url":"{base_url}/stable_nightlights/?year=2013&lat={lat}&lon={lon}", 

416 "variable_name":"mean_stable_nightlights_1km_year2013" 

417 }, 

418 { 

419 "service_url":"{base_url}/stable_nightlights/?agg=mean&radius=5000&year=2013&lat={lat}&lon={lon}", 

420 "variable_name":"mean_stable_nightlights_5km_year2013" 

421 }, 

422 { 

423 "service_url":"{base_url}/stable_nightlights/?agg=max&radius=25000&year=2013&lat={lat}&lon={lon}", 

424 "variable_name":"max_stable_nightlights_25km_year2013" 

425 }, 

426 { 

427 "service_url":"{base_url}/stable_nightlights/?agg=max&radius=25000&year=1992&lat={lat}&lon={lon}", 

428 "variable_name":"max_stable_nightlights_25km_year1992" 

429 } 

430 ] 

431 set_expected_resp = {json.dumps(item, sort_keys=True) for item in expected_resp} 

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

433 assert set_response == set_expected_resp