Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 

678 righe
20 KiB

  1. import asyncio
  2. import json
  3. from urllib.parse import urlencode
  4. from fastapi import APIRouter, Depends, HTTPException, Query, Request
  5. import httpx
  6. import config_env
  7. from typing import List, Optional
  8. from schemas.reslevis import (
  9. BuildingItem,
  10. FloorItem,
  11. ZoneItem,
  12. ZoneAreaDefinitionItem,
  13. GatewayItem,
  14. TrackerItem,
  15. OperatorItem,
  16. SubjectItem,
  17. AlarmItem,
  18. AlarmCoreItem,
  19. AlarmStatusUpdateItem,
  20. TrackItem,
  21. TrackHistoryItem,
  22. TrackerZoneItem,
  23. SettingItem,
  24. GuiConfigItem,
  25. CoreSettingsItem,
  26. CoreSettingsUpdateItem,
  27. )
  28. from logica_reslevis.gateway import GatewayJsonRepository
  29. from logica_reslevis.building import BuildingJsonRepository
  30. from logica_reslevis.floor import FloorJsonRepository
  31. from logica_reslevis.zone import ZoneJsonRepository
  32. from logica_reslevis.zone_area_definition import ZoneAreaDefinitionJsonRepository
  33. from logica_reslevis.tracker import TrackerJsonRepository
  34. from logica_reslevis.operator import OperatorJsonRepository
  35. from logica_reslevis.setting import SettingJsonRepository
  36. from logica_reslevis.gui_config import GuiConfigJsonRepository
  37. from logica_reslevis.subject import SubjectJsonRepository
  38. from logica_reslevis.alarm import AlarmJsonRepository
  39. from logica_reslevis.track import TrackJsonRepository
  40. from logica_reslevis.tracker_zone import TrackerZoneJsonRepository
  41. from security import get_current_user
  42. #CORE SYNC
  43. CORE_BASE_URL = config_env.CORE_API_URL.rstrip("/")
  44. ALERTS_CORE_BASE_URL = "http://localhost:1902"
  45. TRACKS_CORE_BASE_URL = "http://localhost:1902"
  46. SETTINGS_CORE_BASE_URL = "http://localhost:1902"
  47. CORE_TIMEOUT = 2.0 # secondi
  48. async def sync_core_get(request: Request) -> None:
  49. if request.method != "GET":
  50. return
  51. sync = CORE_GET_SYNC.get(request.url.path)
  52. if sync is None:
  53. return
  54. repo, normalizer = sync
  55. try:
  56. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  57. resp = await client.get(
  58. f"{CORE_BASE_URL}{request.url.path}",
  59. params=request.query_params,
  60. )
  61. if 200 <= resp.status_code < 300:
  62. data = resp.json()
  63. if isinstance(data, list):
  64. if normalizer:
  65. data = [normalizer(r) for r in data if isinstance(r, dict)]
  66. repo._write_all(data) # aggiorna i file locali
  67. except (httpx.RequestError, ValueError):
  68. # CORE giù o risposta non valida -> uso il file locale
  69. pass
  70. router = APIRouter(dependencies=[Depends(sync_core_get)])
  71. gateway_repo = GatewayJsonRepository()
  72. building_repo = BuildingJsonRepository()
  73. floor_repo = FloorJsonRepository()
  74. zone_repo = ZoneJsonRepository()
  75. zone_area_definition_repo = ZoneAreaDefinitionJsonRepository()
  76. tracker_repo = TrackerJsonRepository()
  77. operator_repo = OperatorJsonRepository()
  78. subject_repo = SubjectJsonRepository()
  79. alarm_repo = AlarmJsonRepository()
  80. track_repo = TrackJsonRepository()
  81. tracker_zone_repo = TrackerZoneJsonRepository()
  82. setting_repo = SettingJsonRepository()
  83. gui_config_repo = GuiConfigJsonRepository()
  84. def _none_if_empty(v):
  85. return None if v in ("", None, 0, "0") else v
  86. def _str_or_none(v):
  87. if v in ("", None):
  88. return None
  89. if isinstance(v, (int, float, bool)):
  90. return str(v)
  91. return v
  92. def _uuid_list(values):
  93. if values in ("", None):
  94. return []
  95. if isinstance(values, str):
  96. values = [v for v in values.split(",") if v]
  97. if isinstance(values, (list, tuple, set)):
  98. cleaned = []
  99. for v in values:
  100. if isinstance(v, dict):
  101. v = v.get("id") or v.get("uuid")
  102. if v in ("", None, 0, "0"):
  103. continue
  104. cleaned.append(v)
  105. return cleaned
  106. return [values] if values not in ("", None, 0, "0") else []
  107. def _normalize_gateway(row: dict) -> dict:
  108. row = dict(row)
  109. row["floor"] = _none_if_empty(row.get("floor"))
  110. row["building"] = _none_if_empty(row.get("building"))
  111. return row
  112. def _normalize_tracker(row: dict) -> dict:
  113. row = dict(row)
  114. row["floor"] = _none_if_empty(row.get("floor"))
  115. row["building"] = _none_if_empty(row.get("building"))
  116. row["battery"] = _str_or_none(row.get("battery"))
  117. row["temperature"] = _str_or_none(row.get("temperature"))
  118. row["acceleration"] = _str_or_none(row.get("acceleration"))
  119. row["heartRate"] = _str_or_none(row.get("heartRate"))
  120. return row
  121. def _normalize_track(row: dict) -> dict:
  122. row = dict(row)
  123. row["ID"] = row.get("ID")
  124. row["gateway"] = _none_if_empty(row.get("gateway"))
  125. row["tracker"] = _none_if_empty(row.get("tracker"))
  126. row["subject"] = _none_if_empty(row.get("subject"))
  127. row["floor"] = _none_if_empty(row.get("floor"))
  128. row["building"] = _none_if_empty(row.get("building"))
  129. row["timestamp"] = _str_or_none(row.get("timestamp"))
  130. row["type"] = _str_or_none(row.get("type"))
  131. row["status"] = _str_or_none(row.get("status"))
  132. row["gatewayMac"] = _str_or_none(row.get("gatewayMac"))
  133. row["trackerMac"] = _str_or_none(row.get("trackerMac"))
  134. row["subjectName"] = _str_or_none(row.get("subjectName"))
  135. row["x"] = None if row.get("x") in ("", None) else row.get("x")
  136. row["y"] = None if row.get("y") in ("", None) else row.get("y")
  137. row["z"] = None if row.get("z") in ("", None) else row.get("z")
  138. # signal resta float o None
  139. row["signal"] = None if row.get("signal") in ("", None) else row.get("signal")
  140. return row
  141. def _normalize_zone(row: dict) -> dict:
  142. row = dict(row)
  143. row["floor"] = _none_if_empty(row.get("floor"))
  144. row["building"] = _none_if_empty(row.get("building"))
  145. row["groups"] = _uuid_list(row.get("groups"))
  146. return row
  147. CORE_GET_SYNC = {
  148. "/reslevis/getGateways": (gateway_repo, _normalize_gateway),
  149. "/reslevis/getZones": (zone_repo, _normalize_zone),
  150. "/reslevis/getTrackers": (tracker_repo, _normalize_tracker),
  151. }
  152. async def _fetch_tracks_for_tracker(
  153. tracker_id: str,
  154. params: Optional[dict] = None,
  155. ) -> List[dict]:
  156. query_string = urlencode(params or {})
  157. url = f"{TRACKS_CORE_BASE_URL}/reslevis/getTracks/{tracker_id}"
  158. if query_string:
  159. url = f"{url}?{query_string}"
  160. process = await asyncio.create_subprocess_exec(
  161. "curl",
  162. "-sS",
  163. "-X",
  164. "GET",
  165. url,
  166. stdout=asyncio.subprocess.PIPE,
  167. stderr=asyncio.subprocess.PIPE,
  168. )
  169. stdout, stderr = await process.communicate()
  170. if process.returncode != 0:
  171. detail = (stderr or stdout).decode("utf-8", errors="replace").strip() or "CORE curl request failed"
  172. raise HTTPException(status_code=502, detail=detail)
  173. try:
  174. payload = json.loads(stdout.decode("utf-8"))
  175. except ValueError as exc:
  176. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  177. if not isinstance(payload, list):
  178. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  179. return [_normalize_track(row) for row in payload if isinstance(row, dict)]
  180. @router.get(
  181. "/getGateways",
  182. response_model=List[GatewayItem],
  183. tags=["Reslevis"],
  184. dependencies=[Depends(get_current_user)],
  185. )
  186. def getGateways():
  187. return gateway_repo.list()
  188. @router.post("/postGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  189. def postGateway(item: GatewayItem):
  190. gateway_repo.add(item)
  191. return {"message": "OK"}
  192. @router.put("/updateGateway", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  193. def updateGateway(item: GatewayItem):
  194. gateway_repo.update(item)
  195. return {"message": "OK"}
  196. @router.delete("/removeGateway/{gateway_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  197. def removeGateway(gateway_id: str):
  198. gateway_repo.remove(gateway_id)
  199. return {"message": "OK"}
  200. @router.get(
  201. "/getBuildings",
  202. response_model=List[BuildingItem],
  203. tags=["Reslevis"],
  204. dependencies=[Depends(get_current_user)],
  205. )
  206. def getBuildings():
  207. return building_repo.list()
  208. @router.post("/postBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  209. def postBuilding(item: BuildingItem):
  210. building_repo.add(item)
  211. return {"message": "OK"}
  212. @router.put("/updateBuilding", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  213. def updateBuilding(item: BuildingItem):
  214. building_repo.update(item)
  215. return {"message": "OK"}
  216. @router.delete("/removeBuilding/{building_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  217. def removeBuilding(building_id: str):
  218. building_repo.remove(building_id)
  219. return {"message": "OK"}
  220. @router.get(
  221. "/getFloors",
  222. response_model=List[FloorItem],
  223. tags=["Reslevis"],
  224. dependencies=[Depends(get_current_user)],
  225. )
  226. def getFloors():
  227. return floor_repo.list()
  228. @router.post("/postFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  229. def postFloor(item: FloorItem):
  230. floor_repo.add(item)
  231. return {"message": "OK"}
  232. @router.put("/updateFloor", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  233. def updateFloor(item: FloorItem):
  234. floor_repo.update(item)
  235. return {"message": "OK"}
  236. @router.delete("/removeFloor/{floor_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  237. def removeFloor(floor_id: str):
  238. floor_repo.remove(floor_id)
  239. return {"message": "OK"}
  240. @router.get(
  241. "/getZones",
  242. response_model=List[ZoneItem],
  243. tags=["Reslevis"],
  244. dependencies=[Depends(get_current_user)],
  245. )
  246. def getZones():
  247. return zone_repo.list()
  248. @router.post("/postZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  249. def postZone(item: ZoneItem):
  250. zone_repo.add(item)
  251. return {"message": "OK"}
  252. @router.put("/updateZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  253. def updateZone(item: ZoneItem):
  254. zone_repo.update(item)
  255. return {"message": "OK"}
  256. @router.delete("/removeZone/{zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  257. def removeZone(zone_id: str):
  258. zone_repo.remove(zone_id)
  259. return {"message": "OK"}
  260. @router.get(
  261. "/getZoneAreaDefinitions",
  262. response_model=List[ZoneAreaDefinitionItem],
  263. tags=["Reslevis"],
  264. dependencies=[Depends(get_current_user)],
  265. )
  266. def getZoneAreaDefinitions(UUID: str | None = None):
  267. return zone_area_definition_repo.list(UUID)
  268. @router.post("/postZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  269. def postZoneAreaDefinition(item: ZoneAreaDefinitionItem):
  270. zone_area_definition_repo.add(item)
  271. return {"message": "OK"}
  272. @router.put("/updateZoneAreaDefinition", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  273. def updateZoneAreaDefinition(item: ZoneAreaDefinitionItem):
  274. zone_area_definition_repo.update(item)
  275. return {"message": "OK"}
  276. @router.delete("/removeZoneAreaDefinition/{zone_area_definition_uuid}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  277. def removeZoneAreaDefinition(zone_area_definition_uuid: str):
  278. zone_area_definition_repo.remove(zone_area_definition_uuid)
  279. return {"message": "OK"}
  280. @router.get(
  281. "/getTrackers",
  282. response_model=List[TrackerItem],
  283. tags=["Reslevis"],
  284. dependencies=[Depends(get_current_user)],
  285. )
  286. def getTrackers():
  287. return tracker_repo.list()
  288. @router.post("/postTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  289. def postTracker(item: TrackerItem):
  290. tracker_repo.add(item)
  291. return {"message": "OK"}
  292. @router.put("/updateTracker", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  293. def updateTracker(item: TrackerItem):
  294. tracker_repo.update(item)
  295. return {"message": "OK"}
  296. @router.delete("/removeTracker/{tracker_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  297. def removeTracker(tracker_id: str):
  298. tracker_repo.remove(tracker_id)
  299. return {"message": "OK"}
  300. @router.get(
  301. "/getTrackerZones",
  302. response_model=List[TrackerZoneItem],
  303. tags=["Reslevis"],
  304. dependencies=[Depends(get_current_user)],
  305. )
  306. def getTrackerZones():
  307. return tracker_zone_repo.list()
  308. @router.post("/postTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  309. def postTrackerZone(item: TrackerZoneItem):
  310. tracker_zone_repo.add(item)
  311. return {"message": "OK"}
  312. @router.put("/updateTrackerZone", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  313. def updateTrackerZone(item: TrackerZoneItem):
  314. tracker_zone_repo.update(item)
  315. return {"message": "OK"}
  316. @router.delete("/removeTrackerZone/{tracker_zone_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  317. def removeTrackerZone(tracker_zone_id: str):
  318. tracker_zone_repo.remove(tracker_zone_id)
  319. return {"message": "OK"}
  320. @router.get(
  321. "/getTracks",
  322. response_model=List[TrackHistoryItem],
  323. tags=["Reslevis"],
  324. dependencies=[Depends(get_current_user)],
  325. )
  326. async def getTracks(
  327. tracker_id: str = Query(..., alias="id"),
  328. limit: Optional[int] = Query(None, ge=1),
  329. from_: Optional[str] = Query(None, alias="from"),
  330. to: Optional[str] = Query(None),
  331. ):
  332. params = {}
  333. if limit is not None:
  334. params["limit"] = limit
  335. if from_:
  336. params["from"] = from_
  337. if to:
  338. params["to"] = to
  339. return await _fetch_tracks_for_tracker(tracker_id, params)
  340. @router.get(
  341. "/getTracks/{tracker_id}",
  342. response_model=List[TrackHistoryItem],
  343. tags=["Reslevis"],
  344. dependencies=[Depends(get_current_user)],
  345. )
  346. async def getTrack(
  347. tracker_id: str,
  348. limit: Optional[int] = Query(None, ge=1),
  349. from_: Optional[str] = Query(None, alias="from"),
  350. to: Optional[str] = Query(None),
  351. ):
  352. params = {}
  353. if limit is not None:
  354. params["limit"] = limit
  355. if from_:
  356. params["from"] = from_
  357. if to:
  358. params["to"] = to
  359. return await _fetch_tracks_for_tracker(tracker_id, params)
  360. @router.post("/postTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  361. def postTrack(item: TrackItem):
  362. track_repo.add(item)
  363. return {"message": "OK"}
  364. @router.put("/updateTrack", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  365. def updateTrack(item: TrackItem):
  366. track_repo.update(item)
  367. return {"message": "OK"}
  368. @router.delete("/removeTrack/{track_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  369. def removeTrack(track_id: str):
  370. track_repo.remove(track_id)
  371. return {"message": "OK"}
  372. @router.get(
  373. "/getAlarms",
  374. response_model=List[AlarmCoreItem],
  375. tags=["Reslevis"],
  376. dependencies=[Depends(get_current_user)],
  377. )
  378. async def getAlarms():
  379. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  380. resp = await client.get(f"{ALERTS_CORE_BASE_URL}/reslevis/alerts")
  381. if resp.status_code >= 400:
  382. detail = resp.text.strip() or "CORE alerts request failed"
  383. raise HTTPException(status_code=resp.status_code, detail=detail)
  384. try:
  385. payload = resp.json()
  386. except ValueError as exc:
  387. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  388. if not isinstance(payload, list):
  389. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  390. return payload
  391. @router.put(
  392. "/updateAlarm",
  393. tags=["Reslevis"],
  394. dependencies=[Depends(get_current_user)],
  395. )
  396. async def updateAlarm(item: AlarmStatusUpdateItem):
  397. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  398. resp = await client.patch(
  399. f"{ALERTS_CORE_BASE_URL}/reslevis/alerts/{item.id}",
  400. json={"status": item.status},
  401. )
  402. if resp.status_code >= 400:
  403. detail = resp.text.strip() or "CORE alert update failed"
  404. raise HTTPException(status_code=resp.status_code, detail=detail)
  405. if not resp.content:
  406. return {"message": "OK"}
  407. try:
  408. return resp.json()
  409. except ValueError:
  410. return {"message": "OK"}
  411. @router.get(
  412. "/getOperators",
  413. response_model=List[OperatorItem],
  414. tags=["Reslevis"],
  415. dependencies=[Depends(get_current_user)],
  416. )
  417. def getOperators():
  418. return operator_repo.list()
  419. @router.post("/postOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  420. def postOperator(item: OperatorItem):
  421. operator_repo.add(item)
  422. return {"message": "OK"}
  423. @router.put("/updateOperator", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  424. def updateOperator(item: OperatorItem):
  425. operator_repo.update(item)
  426. return {"message": "OK"}
  427. @router.delete("/removeOperator/{operator_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  428. def removeOperator(operator_id: str):
  429. operator_repo.remove(operator_id)
  430. return {"message": "OK"}
  431. @router.get(
  432. "/getSubjects",
  433. response_model=List[SubjectItem],
  434. tags=["Reslevis"],
  435. dependencies=[Depends(get_current_user)],
  436. )
  437. def getSubjects():
  438. return subject_repo.list()
  439. @router.post("/postSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  440. def postSubject(item: SubjectItem):
  441. subject_repo.add(item)
  442. return {"message": "OK"}
  443. @router.put("/updateSubject", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  444. def updateSubject(item: SubjectItem):
  445. subject_repo.update(item)
  446. return {"message": "OK"}
  447. @router.delete("/removeSubject/{subject_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  448. def removeSubject(subject_id: str):
  449. subject_repo.remove(subject_id)
  450. return {"message": "OK"}
  451. @router.get(
  452. "/getSettings",
  453. response_model=List[SettingItem],
  454. tags=["Reslevis"],
  455. dependencies=[Depends(get_current_user)],
  456. )
  457. def getSettings():
  458. return setting_repo.list()
  459. @router.post("/postSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  460. def postSetting(item: SettingItem):
  461. setting_repo.add(item)
  462. return {"message": "OK"}
  463. @router.put("/updateSetting", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  464. def updateSetting(item: SettingItem):
  465. setting_repo.update(item)
  466. return {"message": "OK"}
  467. @router.delete("/removeSetting/{setting_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  468. def removeSetting(setting_id: str):
  469. setting_repo.remove(setting_id)
  470. return {"message": "OK"}
  471. @router.get(
  472. "/getGuiConfigs",
  473. response_model=List[GuiConfigItem],
  474. tags=["Reslevis"],
  475. dependencies=[Depends(get_current_user)],
  476. )
  477. def getGuiConfigs():
  478. return gui_config_repo.list()
  479. @router.post("/postGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  480. def postGuiConfig(item: GuiConfigItem):
  481. gui_config_repo.add(item)
  482. return {"message": "OK"}
  483. @router.put("/updateGuiConfig", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  484. def updateGuiConfig(item: GuiConfigItem):
  485. gui_config_repo.update(item)
  486. return {"message": "OK"}
  487. @router.delete("/removeGuiConfig/{gui_config_id}", tags=["Reslevis"], dependencies=[Depends(get_current_user)])
  488. def removeGuiConfig(gui_config_id: str):
  489. gui_config_repo.remove(gui_config_id)
  490. return {"message": "OK"}
  491. @router.get(
  492. "/getCoreSettings",
  493. response_model=List[CoreSettingsItem],
  494. tags=["Reslevis"],
  495. dependencies=[Depends(get_current_user)],
  496. )
  497. async def getCoreSettings():
  498. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  499. resp = await client.get(f"{SETTINGS_CORE_BASE_URL}/reslevis/settings")
  500. if resp.status_code >= 400:
  501. detail = resp.text.strip() or "CORE settings request failed"
  502. raise HTTPException(status_code=resp.status_code, detail=detail)
  503. try:
  504. payload = resp.json()
  505. except ValueError as exc:
  506. raise HTTPException(status_code=502, detail="Invalid CORE response") from exc
  507. if not isinstance(payload, list):
  508. raise HTTPException(status_code=502, detail="Unexpected CORE response type")
  509. return payload
  510. @router.put(
  511. "/updateCoreSettings",
  512. tags=["Reslevis"],
  513. dependencies=[Depends(get_current_user)],
  514. )
  515. async def updateCoreSettings(item: CoreSettingsUpdateItem):
  516. async with httpx.AsyncClient(timeout=CORE_TIMEOUT) as client:
  517. resp = await client.patch(
  518. f"{SETTINGS_CORE_BASE_URL}/reslevis/settings",
  519. json=item.model_dump(exclude_none=True),
  520. )
  521. if resp.status_code >= 400:
  522. detail = resp.text.strip() or "CORE settings update failed"
  523. raise HTTPException(status_code=resp.status_code, detail=detail)
  524. if not resp.content:
  525. return {"message": "OK"}
  526. try:
  527. return resp.json()
  528. except ValueError:
  529. return {"message": "OK"}