4
4
# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
5
5
6
6
import json
7
+ from io import StringIO
7
8
8
9
import dash
9
10
import logging_config
10
11
import pandas as pd
11
- from dash import dcc , html
12
+ from dash import callback_context , dcc , html
12
13
from dash .dependencies import Input , Output , State
13
14
from dash .exceptions import PreventUpdate
14
15
from main import app
15
- from pyroclient import Client
16
16
17
17
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
25
20
26
21
logger = logging_config .configure_logging (cfg .DEBUG , cfg .SENTRY_DSN )
27
22
28
23
29
24
@app .callback (
30
25
[
31
- Output ("user_credentials" , "data" ),
32
- Output ("user_headers" , "data" ),
26
+ Output ("user_token" , "data" ),
33
27
Output ("form_feedback_area" , "children" ),
34
28
Output ("username_input" , "style" ),
35
29
Output ("password_input" , "style" ),
41
35
[
42
36
State ("username_input" , "value" ),
43
37
State ("password_input" , "value" ),
44
- State ("user_headers " , "data" ),
38
+ State ("user_token " , "data" ),
45
39
State ("language" , "data" ),
46
40
],
47
41
)
48
- def login_callback (n_clicks , username , password , user_headers , lang ):
42
+ def login_callback (n_clicks , username , password , user_token , lang ):
49
43
"""
50
44
Callback to handle user login.
51
45
52
46
Parameters:
53
47
n_clicks (int): Number of times the login button has been clicked.
54
48
username (str or None): The value entered in the username input field.
55
49
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.
57
51
58
52
This function is triggered when the login button is clicked. It verifies the provided username and password,
59
53
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):
80
74
},
81
75
}
82
76
83
- if user_headers is not None :
77
+ if user_token is not None :
84
78
return (
85
- dash .no_update ,
86
79
dash .no_update ,
87
80
dash .no_update ,
88
81
input_style_unchanged ,
@@ -104,7 +97,6 @@ def login_callback(n_clicks, username, password, user_headers, lang):
104
97
105
98
# The login modal remains open; other outputs are updated with arbitrary values
106
99
return (
107
- dash .no_update ,
108
100
dash .no_update ,
109
101
form_feedback ,
110
102
input_style_unchanged ,
@@ -116,11 +108,10 @@ def login_callback(n_clicks, username, password, user_headers, lang):
116
108
else :
117
109
# This is the route of the API that we are going to use for the credential check
118
110
try :
119
- client = Client ( cfg . API_URL , username , password )
111
+ user_token = get_token ( username , password )
120
112
121
113
return (
122
- {"username" : username , "password" : password },
123
- client .headers ,
114
+ user_token ,
124
115
dash .no_update ,
125
116
hide_element_style ,
126
117
hide_element_style ,
@@ -133,7 +124,6 @@ def login_callback(n_clicks, username, password, user_headers, lang):
133
124
form_feedback .append (html .P (translate [lang ]["wrong_credentials" ]))
134
125
135
126
return (
136
- dash .no_update ,
137
127
dash .no_update ,
138
128
form_feedback ,
139
129
input_style_unchanged ,
@@ -147,25 +137,36 @@ def login_callback(n_clicks, username, password, user_headers, lang):
147
137
148
138
149
139
@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" )],
150
156
[
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" ),
157
159
],
158
160
prevent_initial_call = True ,
159
161
)
160
- def api_watcher (n_intervals , user_credentials , local_alerts , user_headers ):
162
+ def api_watcher (n_intervals , api_cameras , local_sequences , user_token ):
161
163
"""
162
164
Callback to periodically fetch alerts data from the API.
163
165
164
166
Parameters:
165
167
n_intervals (int): Number of times the interval has been triggered.
166
- user_credentials (dict or None): Current user credentials for API authentication.
167
168
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.
169
170
170
171
This function is triggered at specified intervals and when user credentials are updated.
171
172
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):
174
175
Returns:
175
176
dash.dependencies.Output: Serialized JSON data of alerts and a flag indicating if data is loaded.
176
177
"""
177
- if user_headers is None :
178
+ if user_token is None :
178
179
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 )
200
237
201
238
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 )
207
257
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