44# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
55
66import json
7+ from io import StringIO
78
89import dash
910import logging_config
1011import pandas as pd
11- from dash import dcc , html
12+ from dash import callback_context , dcc , html
1213from dash .dependencies import Input , Output , State
1314from dash .exceptions import PreventUpdate
1415from main import app
15- from pyroclient import Client
1616
1717import 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
2621logger = 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" ),
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