Skip to content

Commit 520c262

Browse files
committed
feat: add location query
1 parent d8499f7 commit 520c262

3 files changed

Lines changed: 112 additions & 17 deletions

File tree

rest_api/main.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,20 @@
22
from contextlib import asynccontextmanager
33
from functools import partial
44

5-
from fastapi import Depends, FastAPI
5+
from fastapi import Body, Depends, FastAPI
66
from fastapi.middleware.cors import CORSMiddleware
77
from fastapi.responses import ORJSONResponse, PlainTextResponse
88
from models.return_models import (
99
ApplicationListResponse,
10+
LocationListResponse,
1011
MeasurementListResponse,
1112
SensorListResponse,
1213
)
1314
from prometheus_fastapi_instrumentator import Instrumentator
1415
from psycopg2 import pool
1516
from src.handlers import (
1617
fetch_applications,
18+
fetch_locations_for_device_list,
1719
fetch_measurements_for_sensor,
1820
fetch_sensors,
1921
fetch_sensors_for_application,
@@ -140,6 +142,26 @@ def get_sensors_for_application(
140142
return {"sensors": sensors}
141143

142144

145+
@app.post(
146+
"/api/locations",
147+
response_model=LocationListResponse,
148+
tags=["measurements"],
149+
responses={
150+
401: {"description": "Forbidden - Invalid API Key"},
151+
403: {"description": "Forbidden - Not authenticated"},
152+
404: {"description": "Not Found - Sensor not found"},
153+
500: {"description": "Internal Server Error"},
154+
},
155+
)
156+
def get_locations(
157+
conn=Depends(partial(get_db_connection, connection_pool)),
158+
sensor_list=Body(...),
159+
):
160+
"""Fetch locations for a given list of sensors."""
161+
ret = fetch_locations_for_device_list(conn, sensor_list)
162+
return {"locations": ret}
163+
164+
143165
@app.get(
144166
"/api/measurements",
145167
response_model=MeasurementListResponse,

rest_api/models/return_models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ class MeasurementListResponse(BaseModel):
3131
page_info: PageInfo # Forward reference to PageInfo
3232

3333

34+
class LocationListResponse(BaseModel):
35+
"""Data model for a list of locations."""
36+
37+
locations: list[Measurement]
38+
39+
3440
class Sensor(BaseModel):
3541
"""Data model for sensor information."""
3642

rest_api/src/handlers.py

Lines changed: 83 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -155,28 +155,95 @@ def fetch_measurements_for_sensor(
155155
)
156156
except Exception as e:
157157
print(f"Error executing query on table {table_name}: {e}")
158-
# query = sql.SQL("SELECT * FROM {} WHERE id = ANY(%s)").format(
159-
# sql.Identifier(table_name)
160-
# )
161-
# cur.execute(query, (ids,))
162-
# columns = [desc[0] for desc in cur.description]
163-
# for row in cur.fetchall():
164-
# data = {col: val for col, val in zip(columns[3:], row[3:])}
165-
# filtered_data = {k: v for k, v in data.items() if v is not None}
166-
# measurements.append(
167-
# Measurement(
168-
# # sensor_id=row[1],
169-
# table_name=table_name,
170-
# timestamp=str(row[2]),
171-
# data=filtered_data,
172-
# )
173-
# )
174158

175159
# Now sort all measurements by timestamp descending
176160
measurements.sort(key=lambda x: x.timestamp, reverse=True)
177161
return measurements
178162

179163

164+
def fetch_locations_for_device_list(conn, device_list: list[str]) -> list:
165+
"""Fetch the latest location for each device in the device_list."""
166+
with conn.cursor() as cur:
167+
query = """
168+
SELECT
169+
m.measurement_id,
170+
r.measurement_table_name,
171+
s.sensor_name
172+
FROM measurements m
173+
JOIN application_measurements_registry r
174+
ON m.registry_id = r.id
175+
JOIN sensors s
176+
ON m.sensor_id = s.id
177+
ORDER BY m.timestamp DESC
178+
"""
179+
cur.execute(
180+
query,
181+
)
182+
183+
rows = cur.fetchall()
184+
185+
# Step 2: Group measurement_ids by table name
186+
table_to_ids = defaultdict(list)
187+
for measurement_id, table_name, sensor_name in rows:
188+
print(
189+
f"Sensor name in DB: {sensor_name}, measurement_id: {measurement_id}, table_name: {table_name}"
190+
)
191+
table_to_ids[table_name].append((measurement_id, sensor_name))
192+
193+
locations = []
194+
195+
# Step 3: Query each table for its measurement_ids
196+
for table_name, info in table_to_ids.items():
197+
measurement_ids, sensor_names = zip(*info)
198+
ids = list(measurement_ids)
199+
print(f"IDs: {ids}")
200+
# Check if latitude and longitude columns exist in the table
201+
cur.execute(
202+
sql.SQL(
203+
"SELECT column_name FROM information_schema.columns WHERE table_name = %s AND column_name IN ('latitude', 'longitude')"
204+
),
205+
(table_name,),
206+
)
207+
columns_present = {row[0] for row in cur.fetchall()}
208+
if not {"latitude", "longitude"}.issubset(columns_present):
209+
continue
210+
select_fields = sql.SQL("id, timestamp, latitude, longitude")
211+
query = sql.SQL(
212+
"SELECT {} FROM {} WHERE id = ANY(%s) ORDER BY timestamp DESC"
213+
).format(select_fields, sql.Identifier(table_name))
214+
# print(f"Executing query on table {table_name} for IDs: {ids}")
215+
# print(f"Query: {query.as_string(conn)}")
216+
217+
try:
218+
cur.execute(query, (ids,))
219+
columns = [desc[0] for desc in cur.description]
220+
rows = cur.fetchall()
221+
for row in rows:
222+
data = {col: val for col, val in zip(columns[1:], row[1:])}
223+
filtered_data = {k: v for k, v in data.items() if v is not None}
224+
225+
# sensor_name = sensor_names[ids.index(row[0])]
226+
filtered_data["sensor_name"] = sensor_name
227+
print(f"Sensor name: {sensor_name}")
228+
if filtered_data:
229+
if (
230+
filtered_data.get("latitude") is not None
231+
and filtered_data.get("longitude") is not None
232+
):
233+
locations.append(
234+
Measurement(
235+
table_name=table_name,
236+
timestamp=str(row[1]),
237+
data=filtered_data,
238+
)
239+
)
240+
break
241+
except Exception as e:
242+
print(f"Error executing query on table {table_name}: {e}")
243+
244+
return locations
245+
246+
180247
def fetch_sensors_for_application(conn, app_name: str) -> list | None:
181248
"""Fetch all sensors for a given application name."""
182249
with conn.cursor() as cur:

0 commit comments

Comments
 (0)