Skip to content

Commit d1f7c1f

Browse files
Adapt the platform for the new api data model (#178)
* fix data callbacks * fix display callbacks * make the rest work * update api version * remove unused * headers * fix style * adapt tp new api routes * add acknowledge * update token * drop conf
1 parent 25b1b86 commit d1f7c1f

File tree

12 files changed

+427
-356
lines changed

12 files changed

+427
-356
lines changed

app/callbacks/data_callbacks.py

Lines changed: 109 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,26 @@
44
# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
55

66
import json
7+
from io import StringIO
78

89
import dash
910
import logging_config
1011
import pandas as pd
11-
from dash import dcc, html
12+
from dash import callback_context, dcc, html
1213
from dash.dependencies import Input, Output, State
1314
from dash.exceptions import PreventUpdate
1415
from main import app
15-
from pyroclient import Client
1616

1717
import config as cfg
18-
from services import api_client, call_api
19-
from utils.data import (
20-
convert_time,
21-
past_ndays_api_events,
22-
process_bbox,
23-
read_stored_DataFrame,
24-
)
18+
from services import api_client, get_token
19+
from utils.data import process_bbox
2520

2621
logger = logging_config.configure_logging(cfg.DEBUG, cfg.SENTRY_DSN)
2722

2823

2924
@app.callback(
3025
[
31-
Output("user_credentials", "data"),
32-
Output("user_headers", "data"),
26+
Output("user_token", "data"),
3327
Output("form_feedback_area", "children"),
3428
Output("username_input", "style"),
3529
Output("password_input", "style"),
@@ -41,19 +35,19 @@
4135
[
4236
State("username_input", "value"),
4337
State("password_input", "value"),
44-
State("user_headers", "data"),
38+
State("user_token", "data"),
4539
State("language", "data"),
4640
],
4741
)
48-
def login_callback(n_clicks, username, password, user_headers, lang):
42+
def login_callback(n_clicks, username, password, user_token, lang):
4943
"""
5044
Callback to handle user login.
5145
5246
Parameters:
5347
n_clicks (int): Number of times the login button has been clicked.
5448
username (str or None): The value entered in the username input field.
5549
password (str or None): The value entered in the password input field.
56-
user_headers (dict or None): Existing user headers, if any, containing authentication details.
50+
user_token (dict or None): Existing user headers, if any, containing authentication details.
5751
5852
This function is triggered when the login button is clicked. It verifies the provided username and password,
5953
attempts to authenticate the user via the API, and updates the user credentials and headers.
@@ -80,9 +74,8 @@ def login_callback(n_clicks, username, password, user_headers, lang):
8074
},
8175
}
8276

83-
if user_headers is not None:
77+
if user_token is not None:
8478
return (
85-
dash.no_update,
8679
dash.no_update,
8780
dash.no_update,
8881
input_style_unchanged,
@@ -104,7 +97,6 @@ def login_callback(n_clicks, username, password, user_headers, lang):
10497

10598
# The login modal remains open; other outputs are updated with arbitrary values
10699
return (
107-
dash.no_update,
108100
dash.no_update,
109101
form_feedback,
110102
input_style_unchanged,
@@ -116,11 +108,10 @@ def login_callback(n_clicks, username, password, user_headers, lang):
116108
else:
117109
# This is the route of the API that we are going to use for the credential check
118110
try:
119-
client = Client(cfg.API_URL, username, password)
111+
user_token = get_token(username, password)
120112

121113
return (
122-
{"username": username, "password": password},
123-
client.headers,
114+
user_token,
124115
dash.no_update,
125116
hide_element_style,
126117
hide_element_style,
@@ -133,7 +124,6 @@ def login_callback(n_clicks, username, password, user_headers, lang):
133124
form_feedback.append(html.P(translate[lang]["wrong_credentials"]))
134125

135126
return (
136-
dash.no_update,
137127
dash.no_update,
138128
form_feedback,
139129
input_style_unchanged,
@@ -147,25 +137,36 @@ def login_callback(n_clicks, username, password, user_headers, lang):
147137

148138

149139
@app.callback(
140+
Output("api_cameras", "data"),
141+
Input("user_token", "data"),
142+
prevent_initial_call=True,
143+
)
144+
def get_cameras(user_token):
145+
logger.info("Get cameras data")
146+
if user_token is not None:
147+
api_client.token = user_token
148+
cameras = pd.DataFrame(api_client.fetch_cameras().json())
149+
150+
return cameras.to_json(orient="split")
151+
152+
153+
@app.callback(
154+
Output("api_sequences", "data"),
155+
[Input("main_api_fetch_interval", "n_intervals"), Input("api_cameras", "data")],
150156
[
151-
Output("store_api_alerts_data", "data"),
152-
],
153-
[Input("main_api_fetch_interval", "n_intervals"), Input("user_credentials", "data")],
154-
[
155-
State("store_api_alerts_data", "data"),
156-
State("user_headers", "data"),
157+
State("api_sequences", "data"),
158+
State("user_token", "data"),
157159
],
158160
prevent_initial_call=True,
159161
)
160-
def api_watcher(n_intervals, user_credentials, local_alerts, user_headers):
162+
def api_watcher(n_intervals, api_cameras, local_sequences, user_token):
161163
"""
162164
Callback to periodically fetch alerts data from the API.
163165
164166
Parameters:
165167
n_intervals (int): Number of times the interval has been triggered.
166-
user_credentials (dict or None): Current user credentials for API authentication.
167168
local_alerts (dict or None): Locally stored alerts data, serialized as JSON.
168-
user_headers (dict or None): Current user headers containing authentication details.
169+
user_token (dict or None): Current user headers containing authentication details.
169170
170171
This function is triggered at specified intervals and when user credentials are updated.
171172
It retrieves unacknowledged events from the API, processes the data, and stores it locally.
@@ -174,35 +175,85 @@ def api_watcher(n_intervals, user_credentials, local_alerts, user_headers):
174175
Returns:
175176
dash.dependencies.Output: Serialized JSON data of alerts and a flag indicating if data is loaded.
176177
"""
177-
if user_headers is None:
178+
if user_token is None:
178179
raise PreventUpdate
179-
user_token = user_headers["Authorization"].split(" ")[1]
180-
api_client.token = user_token
181-
182-
# Read local data
183-
local_alerts, alerts_data_loaded = read_stored_DataFrame(local_alerts)
184-
logger.info("Start Fetching the events")
185-
186-
# Fetch events
187-
api_alerts = pd.DataFrame(call_api(api_client.get_unacknowledged_events, user_credentials)())
188-
api_alerts["created_at"] = convert_time(api_alerts)
189-
api_alerts = past_ndays_api_events(api_alerts, n_days=0)
190-
191-
if len(api_alerts) == 0:
192-
return [
193-
json.dumps(
194-
{
195-
"data": pd.DataFrame().to_json(orient="split"),
196-
"data_loaded": True,
197-
}
198-
)
199-
]
180+
181+
logger.info("Start Fetching Sequences")
182+
# Fetch Sequences
183+
response = api_client.fetch_latest_sequences()
184+
api_sequences = pd.DataFrame(response.json())
185+
186+
local_sequences = pd.read_json(StringIO(local_sequences), orient="split")
187+
if len(api_sequences) == 0:
188+
return pd.DataFrame().to_json(orient="split")
189+
190+
else:
191+
if not local_sequences.empty:
192+
aligned_api_sequences, aligned_local_sequences = api_sequences["id"].align(local_sequences["id"])
193+
if all(aligned_api_sequences == aligned_local_sequences):
194+
return dash.no_update
195+
196+
return api_sequences.to_json(orient="split")
197+
198+
199+
@app.callback(
200+
[Output("are_detections_loaded", "data"), Output("sequence_on_display", "data"), Output("api_detections", "data")],
201+
[Input("api_sequences", "data"), Input("sequence_id_on_display", "data"), Input("api_detections", "data")],
202+
State("are_detections_loaded", "data"),
203+
prevent_initial_call=True,
204+
)
205+
def load_detections(api_sequences, sequence_id_on_display, api_detections, are_detections_loaded):
206+
# Deserialize data
207+
api_sequences = pd.read_json(StringIO(api_sequences), orient="split")
208+
sequence_id_on_display = str(sequence_id_on_display)
209+
are_detections_loaded = json.loads(are_detections_loaded)
210+
api_detections = json.loads(api_detections)
211+
212+
# Initialize sequence_on_display
213+
sequence_on_display = pd.DataFrame().to_json(orient="split")
214+
215+
# Identify which input triggered the callback
216+
ctx = callback_context
217+
if not ctx.triggered:
218+
raise PreventUpdate
219+
220+
triggered_input = ctx.triggered[0]["prop_id"].split(".")[0]
221+
222+
if triggered_input == "sequence_id_on_display":
223+
# If the displayed sequence changes, load its detections if not already loaded
224+
if sequence_id_on_display not in api_detections:
225+
response = api_client.fetch_sequences_detections(sequence_id_on_display)
226+
detections = pd.DataFrame(response.json())
227+
detections["processed_bboxes"] = detections["bboxes"].apply(process_bbox)
228+
api_detections[sequence_id_on_display] = detections.to_json(orient="split")
229+
230+
sequence_on_display = api_detections[sequence_id_on_display]
231+
last_seen_at = api_sequences.loc[
232+
api_sequences["id"].astype("str") == sequence_id_on_display, "last_seen_at"
233+
].iloc[0]
234+
235+
# Ensure last_seen_at is stored as a string
236+
are_detections_loaded[sequence_id_on_display] = str(last_seen_at)
200237

201238
else:
202-
api_alerts["processed_loc"] = api_alerts["localization"].apply(process_bbox)
203-
if alerts_data_loaded and not local_alerts.empty:
204-
aligned_api_alerts, aligned_local_alerts = api_alerts["alert_id"].align(local_alerts["alert_id"])
205-
if all(aligned_api_alerts == aligned_local_alerts):
206-
return [dash.no_update]
239+
# If no specific sequence is triggered, load detections for the first missing sequence
240+
for _, row in api_sequences.iterrows():
241+
sequence_id = str(row["id"])
242+
last_seen_at = row["last_seen_at"]
243+
244+
if sequence_id not in are_detections_loaded or are_detections_loaded[sequence_id] != str(last_seen_at):
245+
response = api_client.fetch_sequences_detections(sequence_id)
246+
detections = pd.DataFrame(response.json())
247+
detections["processed_bboxes"] = detections["bboxes"].apply(process_bbox)
248+
api_detections[sequence_id] = detections.to_json(orient="split")
249+
are_detections_loaded[sequence_id] = str(last_seen_at)
250+
break
251+
252+
# Clean up old sequences that are no longer in api_sequences
253+
sequences_in_api = api_sequences["id"].astype("str").values
254+
to_drop = [key for key in are_detections_loaded if key not in sequences_in_api]
255+
for key in to_drop:
256+
are_detections_loaded.pop(key, None)
207257

208-
return [json.dumps({"data": api_alerts.to_json(orient="split"), "data_loaded": True})]
258+
# Serialize and return data
259+
return json.dumps(are_detections_loaded), sequence_on_display, json.dumps(api_detections)

0 commit comments

Comments
 (0)