From 571eb9920d87d923a2f333a707a79342906cdade Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:00:39 +0100 Subject: [PATCH 01/11] fix data callbacks --- app/callbacks/data_callbacks.py | 106 +++++++++++++++----------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/app/callbacks/data_callbacks.py b/app/callbacks/data_callbacks.py index 8a209446..97dd73db 100644 --- a/app/callbacks/data_callbacks.py +++ b/app/callbacks/data_callbacks.py @@ -4,6 +4,7 @@ # See LICENSE or go to for full license details. import json +from datetime import datetime, timedelta import dash import logging_config @@ -12,24 +13,17 @@ from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate from main import app -from pyroclient import Client import config as cfg -from services import api_client, call_api -from utils.data import ( - convert_time, - past_ndays_api_events, - process_bbox, - read_stored_DataFrame, -) +from services import api_client, get_token +from utils.data import assign_event_ids, process_bbox, read_stored_DataFrame logger = logging_config.configure_logging(cfg.DEBUG, cfg.SENTRY_DSN) @app.callback( [ - Output("user_credentials", "data"), - Output("user_headers", "data"), + Output("user_token", "data"), Output("form_feedback_area", "children"), Output("username_input", "style"), Output("password_input", "style"), @@ -41,11 +35,11 @@ [ State("username_input", "value"), State("password_input", "value"), - State("user_headers", "data"), + State("user_token", "data"), State("language", "data"), ], ) -def login_callback(n_clicks, username, password, user_headers, lang): +def login_callback(n_clicks, username, password, user_token, lang): """ Callback to handle user login. @@ -53,7 +47,7 @@ def login_callback(n_clicks, username, password, user_headers, lang): n_clicks (int): Number of times the login button has been clicked. username (str or None): The value entered in the username input field. password (str or None): The value entered in the password input field. - user_headers (dict or None): Existing user headers, if any, containing authentication details. + user_token (dict or None): Existing user headers, if any, containing authentication details. This function is triggered when the login button is clicked. It verifies the provided username and password, 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): }, } - if user_headers is not None: + if user_token is not None: return ( - dash.no_update, dash.no_update, dash.no_update, input_style_unchanged, @@ -104,7 +97,6 @@ def login_callback(n_clicks, username, password, user_headers, lang): # The login modal remains open; other outputs are updated with arbitrary values return ( - dash.no_update, dash.no_update, form_feedback, input_style_unchanged, @@ -116,11 +108,10 @@ def login_callback(n_clicks, username, password, user_headers, lang): else: # This is the route of the API that we are going to use for the credential check try: - client = Client(cfg.API_URL, username, password) + user_token = get_token(username, password) return ( - {"username": username, "password": password}, - client.headers, + user_token, dash.no_update, hide_element_style, hide_element_style, @@ -133,7 +124,6 @@ def login_callback(n_clicks, username, password, user_headers, lang): form_feedback.append(html.P(translate[lang]["wrong_credentials"])) return ( - dash.no_update, dash.no_update, form_feedback, input_style_unchanged, @@ -147,25 +137,34 @@ def login_callback(n_clicks, username, password, user_headers, lang): @app.callback( + Output("api_cameras", "data"), + Input("user_token", "data"), + prevent_initial_call=True, +) +def get_cameras(user_token): + logger.info("Get cameras data") + cameras = pd.DataFrame(api_client.fetch_cameras().json()) + + return json.dumps({"data": cameras.to_json(orient="split"), "data_loaded": True}) + + +@app.callback( + Output("api_detections", "data"), + [Input("main_api_fetch_interval", "n_intervals"), Input("api_cameras", "data")], [ - Output("store_api_alerts_data", "data"), - ], - [Input("main_api_fetch_interval", "n_intervals"), Input("user_credentials", "data")], - [ - State("store_api_alerts_data", "data"), - State("user_headers", "data"), + State("api_detections", "data"), + State("user_token", "data"), ], prevent_initial_call=True, ) -def api_watcher(n_intervals, user_credentials, local_alerts, user_headers): +def api_watcher(n_intervals, api_cameras, local_detections, user_token): """ Callback to periodically fetch alerts data from the API. Parameters: n_intervals (int): Number of times the interval has been triggered. - user_credentials (dict or None): Current user credentials for API authentication. local_alerts (dict or None): Locally stored alerts data, serialized as JSON. - user_headers (dict or None): Current user headers containing authentication details. + user_token (dict or None): Current user headers containing authentication details. This function is triggered at specified intervals and when user credentials are updated. It retrieves unacknowledged events from the API, processes the data, and stores it locally. @@ -174,35 +173,30 @@ def api_watcher(n_intervals, user_credentials, local_alerts, user_headers): Returns: dash.dependencies.Output: Serialized JSON data of alerts and a flag indicating if data is loaded. """ - if user_headers is None: + if user_token is None: raise PreventUpdate - user_token = user_headers["Authorization"].split(" ")[1] - api_client.token = user_token - # Read local data - local_alerts, alerts_data_loaded = read_stored_DataFrame(local_alerts) logger.info("Start Fetching the events") - - # Fetch events - api_alerts = pd.DataFrame(call_api(api_client.get_unacknowledged_events, user_credentials)()) - api_alerts["created_at"] = convert_time(api_alerts) - api_alerts = past_ndays_api_events(api_alerts, n_days=0) - - if len(api_alerts) == 0: - return [ - json.dumps( - { - "data": pd.DataFrame().to_json(orient="split"), - "data_loaded": True, - } - ) - ] + # Fetch Detections + previous_time_event = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d_%H:%M:%S") + response = api_client.fetch_unlabeled_detections(from_date=previous_time_event, limit=50) + api_detections = pd.DataFrame(response.json()) + + local_detections, _ = read_stored_DataFrame(local_detections) + if len(api_detections) == 0: + return json.dumps( + { + "data": pd.DataFrame().to_json(orient="split"), + "data_loaded": True, + } + ) else: - api_alerts["processed_loc"] = api_alerts["localization"].apply(process_bbox) - if alerts_data_loaded and not local_alerts.empty: - aligned_api_alerts, aligned_local_alerts = api_alerts["alert_id"].align(local_alerts["alert_id"]) - if all(aligned_api_alerts == aligned_local_alerts): - return [dash.no_update] - - return [json.dumps({"data": api_alerts.to_json(orient="split"), "data_loaded": True})] + api_detections["processed_bboxes"] = api_detections["bboxes"].apply(process_bbox) + api_detections = assign_event_ids(api_detections, time_threshold=30 * 60) + if not local_detections.empty: + aligned_api_detections, aligned_local_detections = api_detections["id"].align(local_detections["id"]) + if all(aligned_api_detections == aligned_local_detections): + return dash.no_update + + return json.dumps({"data": api_detections.to_json(orient="split"), "data_loaded": True}) From 4724e3dbbe536cc52249ed8ef104ee7e28204fd6 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:00:57 +0100 Subject: [PATCH 02/11] fix display callbacks --- app/callbacks/display_callbacks.py | 98 +++++++++++++++++------------- 1 file changed, 56 insertions(+), 42 deletions(-) diff --git a/app/callbacks/display_callbacks.py b/app/callbacks/display_callbacks.py index 29de70ef..472037c1 100644 --- a/app/callbacks/display_callbacks.py +++ b/app/callbacks/display_callbacks.py @@ -15,7 +15,7 @@ from main import app import config as cfg -from services import api_client, call_api +from services import api_client from utils.data import read_stored_DataFrame from utils.display import build_vision_polygon, create_event_list_from_alerts @@ -26,39 +26,45 @@ @app.callback( Output("alert-list-container", "children"), [ - Input("store_api_alerts_data", "data"), + Input("api_detections", "data"), Input("to_acknowledge", "data"), ], + State("api_cameras", "data"), ) -def update_event_list(api_alerts, to_acknowledge): +def update_event_list(api_detections, to_acknowledge, cameras): """ Updates the event list based on changes in the events data or acknowledgement actions. Parameters: - - api_alerts (json): JSON formatted data containing current alerts information. + - api_detections (json): JSON formatted data containing current alerts information. - to_acknowledge (int): Event ID that is being acknowledged. Returns: - html.Div: A Div containing the updated list of alerts. """ - api_alerts, event_data_loaded = read_stored_DataFrame(api_alerts) + logger.info("update_event_list") + + api_detections, event_data_loaded = read_stored_DataFrame(api_detections) + cameras, _ = read_stored_DataFrame(cameras) + + print("api") if not event_data_loaded: raise PreventUpdate - if len(api_alerts): + if len(api_detections): # Drop acknowledge event for faster update - api_alerts = api_alerts[~api_alerts["id"].isin([to_acknowledge])] + api_detections = api_detections[~api_detections["event_id"].isin([to_acknowledge])] # Drop event with less than 5 alerts or less then 2 bbox drop_event = [] - for event_id in np.unique(api_alerts["id"].values): - event_alerts = api_alerts[api_alerts["id"] == event_id] - if np.sum([len(box) > 2 for box in event_alerts["localization"]]) < 2 or len(event_alerts) < 5: + for event_id in np.unique(api_detections["event_id"].values): + event_alerts = api_detections[api_detections["event_id"] == event_id] + if np.sum([len(box) > 2 for box in event_alerts["bboxes"]]) < 2 or len(event_alerts) < 5: drop_event.append(event_id) - api_alerts = api_alerts[~api_alerts["id"].isin([drop_event])] + api_detections = api_detections[~api_detections["event_id"].isin([drop_event])] - return create_event_list_from_alerts(api_alerts) + return create_event_list_from_alerts(api_detections, cameras) # Select the event id @@ -74,7 +80,7 @@ def update_event_list(api_alerts, to_acknowledge): ], [ State({"type": "event-button", "index": ALL}, "id"), - State("store_api_alerts_data", "data"), + State("api_detections", "data"), State("event_id_on_display", "data"), ], prevent_initial_call=True, @@ -94,6 +100,7 @@ def select_event_with_button(n_clicks, button_ids, local_alerts, event_id_on_dis - int: ID of the event to display. - int: Number of clicks for the auto-move button reset. """ + logger.info("select_event_with_button") ctx = dash.callback_context local_alerts, alerts_data_loaded = read_stored_DataFrame(local_alerts) @@ -145,7 +152,7 @@ def select_event_with_button(n_clicks, button_ids, local_alerts, event_id_on_dis @app.callback( Output("alert_on_display", "data"), Input("event_id_on_display", "data"), - State("store_api_alerts_data", "data"), + State("api_detections", "data"), prevent_initial_call=True, ) def update_display_data(event_id_on_display, local_alerts): @@ -159,6 +166,7 @@ def update_display_data(event_id_on_display, local_alerts): Returns: - json: JSON formatted data for the selected event. """ + logger.info("update_display_data") local_alerts, data_loaded = read_stored_DataFrame(local_alerts) if not data_loaded: @@ -173,9 +181,9 @@ def update_display_data(event_id_on_display, local_alerts): ) else: if event_id_on_display == 0: - event_id_on_display = local_alerts["id"].values[0] + event_id_on_display = local_alerts["event_id"].values[0] - alert_on_display = local_alerts[local_alerts["id"] == event_id_on_display] + alert_on_display = local_alerts[local_alerts["event_id"] == event_id_on_display] return json.dumps({"data": alert_on_display.to_json(orient="split"), "data_loaded": True}) @@ -214,6 +222,7 @@ def update_image_and_bbox(slider_value, alert_data, alert_list, lang): bbox_style = {"display": "none"} # Default style for the bounding box alert_data, data_loaded = read_stored_DataFrame(alert_data) + if not data_loaded: raise PreventUpdate @@ -223,9 +232,9 @@ def update_image_and_bbox(slider_value, alert_data, alert_list, lang): # Filter images with non-empty URLs images, boxes = zip( *( - (alert["media_url"], alert["processed_loc"]) + (alert["url"], alert["processed_bboxes"]) for _, alert in alert_data.iterrows() - if alert["media_url"] # Only include if media_url is not empty + if alert["url"] # Only include if url is not empty ) ) @@ -278,6 +287,7 @@ def toggle_bbox_visibility(n_clicks, button_style): - dict: Updated style for the bounding box. - dict: Updated style for the hide/show button. """ + logger.info("toggle_bbox_visibility") if n_clicks % 2 == 0: bbox_style = {"display": "block"} # Show the bounding box button_style["backgroundColor"] = "#FEBA6A" # Original button color @@ -363,7 +373,7 @@ def update_download_link(slider_value, alert_data): alert_data, data_loaded = read_stored_DataFrame(alert_data) if data_loaded and len(alert_data): try: - return alert_data["media_url"].values[slider_value] + return alert_data["url"].values[slider_value] except Exception as e: logger.info(e) logger.info(f"Size of the alert_data dataframe: {alert_data.size}") @@ -386,9 +396,10 @@ def update_download_link(slider_value, alert_data): Output("slider-container", "style"), ], Input("alert_on_display", "data"), + State("api_cameras", "data"), prevent_initial_call=True, ) -def update_map_and_alert_info(alert_data): +def update_map_and_alert_info(alert_data, cameras): """ Updates the map's vision polygons, center, and alert information based on the current alert data. @@ -407,47 +418,52 @@ def update_map_and_alert_info(alert_data): - dict: Style settings for alert information. - dict: Style settings for the slider container. """ + logger.info("update_map_and_alert_info") alert_data, data_loaded = read_stored_DataFrame(alert_data) + cameras, _ = read_stored_DataFrame(cameras) if not data_loaded: raise PreventUpdate if not alert_data.empty: - # Convert the 'localization' column to a list (empty lists if the original value was '[]'). - alert_data["localization"] = alert_data["localization"].apply( + # Convert the 'bboxes' column to a list (empty lists if the original value was '[]'). + alert_data["bboxes"] = alert_data["bboxes"].apply( lambda x: ast.literal_eval(x) if isinstance(x, str) and x.strip() != "[]" else [] ) - # Filter out rows where 'localization' is not empty and get the last one. + # Filter out rows where 'bboxes' is not empty and get the last one. # If all are empty, then simply get the last row of the DataFrame. - row_with_localization = ( - alert_data[alert_data["localization"].astype(bool)].iloc[-1] - if not alert_data[alert_data["localization"].astype(bool)].empty + row_with_bboxes = ( + alert_data[alert_data["bboxes"].astype(bool)].iloc[-1] + if not alert_data[alert_data["bboxes"].astype(bool)].empty else alert_data.iloc[-1] ) + row_cam = cameras[cameras["id"] == row_with_bboxes["camera_id"]] + lat, lon = row_cam[["lat"]].values.item(), row_cam[["lon"]].values.item() + polygon, detection_azimuth = build_vision_polygon( - site_lat=row_with_localization["lat"], - site_lon=row_with_localization["lon"], - azimuth=row_with_localization["device_azimuth"], + site_lat=lat, + site_lon=lon, + azimuth=row_with_bboxes["azimuth"], opening_angle=cfg.CAM_OPENING_ANGLE, dist_km=cfg.CAM_RANGE_KM, - localization=row_with_localization["processed_loc"], + bboxes=row_with_bboxes["processed_bboxes"], ) - date_val = row_with_localization["created_at"] - cam_name = f"{row_with_localization['device_login'][:-2].replace('_', ' ')} - {int(row_with_localization['device_azimuth'])}°" + date_val = row_with_bboxes["created_at"].strftime("%Y-%m-%d %H:%M") + cam_name = f"{row_cam['name'].values.item()[:-3].replace('_', ' ')} : {int(row_with_bboxes['azimuth'])}°" camera_info = f"{cam_name}" - location_info = f"{row_with_localization['lat']:.4f}, {row_with_localization['lon']:.4f}" + location_info = f"{lat:.4f}, {lon:.4f}" angle_info = f"{detection_azimuth}°" date_info = f"{date_val}" return ( polygon, - [row_with_localization["lat"], row_with_localization["lon"]], + [lat, lon], polygon, - [row_with_localization["lat"], row_with_localization["lon"]], + [lat, lon], camera_info, location_info, angle_info, @@ -475,20 +491,18 @@ def update_map_and_alert_info(alert_data): [Input("acknowledge-button", "n_clicks")], [ State("event_id_on_display", "data"), - State("user_headers", "data"), - State("user_credentials", "data"), + State("user_token", "data"), ], prevent_initial_call=True, ) -def acknowledge_event(n_clicks, event_id_on_display, user_headers, user_credentials): +def acknowledge_event(n_clicks, event_id_on_display, user_token): """ Acknowledges the selected event and updates the state to reflect this. Parameters: - n_clicks (int): Number of clicks on the acknowledge button. - event_id_on_display (int): Currently displayed event ID. - - user_headers (dict): User authorization headers for API requests. - - user_credentials (tuple): User credentials (username, password). + - user_token (dict): User authorization headers for API requests. Returns: - int: The ID of the event that has been acknowledged. @@ -499,9 +513,8 @@ def acknowledge_event(n_clicks, event_id_on_display, user_headers, user_credenti if cfg.SAFE_DEV_MODE == "True": raise PreventUpdate - user_token = user_headers["Authorization"].split(" ")[1] api_client.token = user_token - call_api(api_client.acknowledge_event, user_credentials)(event_id=int(event_id_on_display)) + # call_api(api_client.acknowledge_event, user_credentials)(event_id=int(event_id_on_display)) return event_id_on_display @@ -524,6 +537,7 @@ def toggle_fullscreen_map(n_clicks_open, is_open): Returns: - bool: New state of the map modal (open/close). """ + logger.info("toggle_fullscreen_map") if n_clicks_open: return not is_open # Toggle the modal return is_open # Keep the current state From a56e3cfc181b7252f63993ec9ebd1851838fddc3 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:01:20 +0100 Subject: [PATCH 03/11] make the rest work --- app/index.py | 17 +++++++-------- app/layouts/main_layout.py | 29 ++++++++++++++----------- app/pages/homepage.py | 7 ++++--- app/services/__init__.py | 4 ++-- app/services/api.py | 43 +++++++++++--------------------------- app/utils/data.py | 43 ++++++++++++++++++++++++++++++++++++++ app/utils/display.py | 38 ++++++++++++++++----------------- app/utils/sites.py | 16 -------------- 8 files changed, 104 insertions(+), 93 deletions(-) diff --git a/app/index.py b/app/index.py index 496fc848..8ff33d36 100644 --- a/app/index.py +++ b/app/index.py @@ -26,17 +26,16 @@ # Manage Pages @app.callback( Output("page-content", "children"), - [Input("url", "pathname"), Input("user_headers", "data")], - State("user_credentials", "data"), + [Input("url", "pathname"), Input("api_cameras", "data")], + State("user_token", "data"), ) -def display_page(pathname, user_headers, user_credentials): +def display_page(pathname, api_cameras, user_token): logger.debug( - "display_page called with pathname: %s, user_headers: %s, user_credentials: %s", + "display_page called with pathname: %s, user_token: %s", pathname, - user_headers, - user_credentials, + user_token, ) - if user_headers is None: + if user_token is None: if pathname == "/" or pathname == "/fr" or pathname is None: logger.info("No user headers found, showing login layout (language: French).") return login_layout(lang="fr") @@ -45,10 +44,10 @@ def display_page(pathname, user_headers, user_credentials): return login_layout(lang="es") if pathname == "/" or pathname == "/fr" or pathname is None: logger.info("Showing homepage layout (default language: French).") - return homepage_layout(user_headers, user_credentials, lang="fr") + return homepage_layout(user_token, api_cameras, lang="fr") if pathname == "/es": logger.info("Showing homepage layout (language: Spanish).") - return homepage_layout(user_headers, user_credentials, lang="es") + return homepage_layout(user_token, api_cameras, lang="es") else: logger.warning("Unable to find page for pathname: %s", pathname) return html.Div([html.P("Unable to find this page.", className="alert alert-warning")]) diff --git a/app/layouts/main_layout.py b/app/layouts/main_layout.py index d583cb15..624d4f5a 100644 --- a/app/layouts/main_layout.py +++ b/app/layouts/main_layout.py @@ -7,22 +7,18 @@ import pandas as pd from dash import dcc, html -from pyroclient import Client import config as cfg from components.navbar import Navbar from services import api_client if not cfg.LOGIN: - client = Client(cfg.API_URL, cfg.API_LOGIN, cfg.API_PWD) - user_headers = client.headers - user_token = user_headers["Authorization"].split(" ")[1] - api_client.token = user_token - user_credentials = {"username": cfg.API_LOGIN, "password": cfg.API_PWD} + user_token = api_client.token else: - user_credentials = {} - user_headers = None + user_token = None + +print("user token AA", user_token) def get_main_layout(): @@ -43,7 +39,17 @@ def get_main_layout(): ), dcc.Interval(id="main_api_fetch_interval", interval=30 * 1000), dcc.Store( - id="store_api_alerts_data", + id="api_detections", + storage_type="session", + data=json.dumps( + { + "data": pd.DataFrame().to_json(orient="split"), + "data_loaded": False, + } + ), + ), + dcc.Store( + id="api_cameras", storage_type="session", data=json.dumps( { @@ -67,10 +73,9 @@ def get_main_layout(): # Add this to your app.layout dcc.Store(id="bbox_visibility", data={"visible": True}), # Storage components saving the user's headers and credentials - dcc.Store(id="user_headers", storage_type="session", data=user_headers), + dcc.Store(id="user_token", storage_type="session", data=user_token), # [TEMPORARY FIX] Storing the user's credentials to refresh the token when needed - dcc.Store(id="user_credentials", storage_type="session", data=user_credentials), dcc.Store(id="to_acknowledge", data=0), - dcc.Store(id="trigger_no_events", data=False), + dcc.Store(id="trigger_no_detections", data=False), ] ) diff --git a/app/pages/homepage.py b/app/pages/homepage.py index c122024d..6608a77a 100644 --- a/app/pages/homepage.py +++ b/app/pages/homepage.py @@ -14,7 +14,8 @@ app.css.append_css({"external_url": "/assets/style.css"}) -def homepage_layout(user_headers, user_credentials, lang="fr"): +def homepage_layout(user_token, api_cameras, lang="fr"): + print("user token 00", user_token) translate = { "fr": { "animate_on_off": "Activer / Désactiver l'animation", @@ -172,7 +173,7 @@ def homepage_layout(user_headers, user_credentials, lang="fr"): ), dbc.Row( dbc.Col( - build_alerts_map(user_headers, user_credentials), + build_alerts_map(api_cameras), className="common-style", style={ "position": "relative", @@ -251,7 +252,7 @@ def homepage_layout(user_headers, user_credentials, lang="fr"): [ dbc.ModalHeader(translate[lang]["map"]), dbc.ModalBody( - build_alerts_map(user_headers, user_credentials, id_suffix="-md"), + build_alerts_map(api_cameras, id_suffix="-md"), ), ], id="map-modal", diff --git a/app/services/__init__.py b/app/services/__init__.py index 061b12ec..1ebda3c1 100644 --- a/app/services/__init__.py +++ b/app/services/__init__.py @@ -1,3 +1,3 @@ -from .api import api_client, call_api +from .api import api_client, get_token -__all__ = ["api_client", "call_api"] +__all__ = ["api_client", "get_token"] diff --git a/app/services/api.py b/app/services/api.py index 204fc50f..b3cbf705 100644 --- a/app/services/api.py +++ b/app/services/api.py @@ -3,49 +3,30 @@ # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. -from functools import wraps -from typing import Callable, Dict +from urllib.parse import urljoin +import requests from pyroclient import Client import config as cfg -__all__ = ["api_client", "call_api"] +__all__ = ["api_client", "get_token"] if any(not isinstance(val, str) for val in [cfg.API_URL, cfg.API_LOGIN, cfg.API_PWD]): raise ValueError("The following environment variables need to be set: 'API_URL', 'API_LOGIN', 'API_PWD'") -api_client = Client(cfg.API_URL, cfg.API_LOGIN, cfg.API_PWD) +def get_token(login: str, passwrd: str): + access_token = requests.post( + urljoin(cfg.API_URL, "/api/v1/login/creds"), + data={"username": login, "password": passwrd}, + timeout=5, + ).json()["access_token"] -def call_api(func: Callable, user_credentials: Dict[str, str]) -> Callable: - """Decorator to call API method and renew the token if needed. Usage: + return access_token - result = call_api(my_func, user_credentials)(1, 2, verify=False) - Instead of: +access_token = get_token(cfg.API_LOGIN, cfg.API_PWD) - response = my_func(1, verify=False) - if response.status_code == 401: - api_client.refresh_token(user_credentials["username"], user_credentials["password"]) - response = my_func(1, verify=False) - result = response.json() - - Args: - func: function that calls API method - user_credentials: a dictionary with two keys, the username and password for authentication - - Returns: decorated function, to be called with positional and keyword arguments - """ - - @wraps(func) - def wrapper(*args, **kwargs): - response = func(*args, **kwargs) - if response.status_code == 401: - api_client.refresh_token(user_credentials["username"], user_credentials["password"]) - response = func(*args, **kwargs) - assert response.status_code // 100 == 2, response.text - return response.json() - - return wrapper +api_client = Client(access_token, cfg.API_URL) diff --git a/app/utils/data.py b/app/utils/data.py index dd60cbcf..26158fec 100644 --- a/app/utils/data.py +++ b/app/utils/data.py @@ -123,3 +123,46 @@ def past_ndays_api_events(api_events, n_days=0): api_events = api_events[(api_events["created_at"] > start_date) | (api_events["created_at"] == start_date)] return api_events + + +def assign_event_ids(df, time_threshold=30 * 60): + """ + Assigns event IDs to detections in a DataFrame based on the same camera_id + and a time threshold between consecutive detections. + + Args: + df (pd.DataFrame): The input DataFrame containing 'camera_id' and 'created_at' columns. + time_threshold (int): The time difference in seconds to group detections into the same event. + + Returns: + pd.DataFrame: A DataFrame with an additional 'event_id' column. + """ + # Ensure 'created_at' is in datetime format + df["created_at"] = pd.to_datetime(df["created_at"]) + + # Sort by camera_id and created_at + df = df.sort_values(by=["camera_id", "created_at"]).reset_index(drop=True) + + # Initialize variables + event_id = 0 + event_ids = [] # To store the assigned event IDs + + # Iterate through rows to assign event IDs + for i, row in df.iterrows(): + if i == 0: + # First detection starts a new event + event_ids.append(event_id) + else: + # Compare with the previous row + prev_row = df.iloc[i - 1] + time_diff = (row["created_at"] - prev_row["created_at"]).total_seconds() + + if row["camera_id"] != prev_row["camera_id"] or time_diff > time_threshold: + # Start a new event + event_id += 1 + + event_ids.append(event_id) + + # Add the event_id column to the DataFrame + df["event_id"] = event_ids + return df diff --git a/app/utils/display.py b/app/utils/display.py index 092008dd..4118f410 100644 --- a/app/utils/display.py +++ b/app/utils/display.py @@ -11,8 +11,7 @@ from geopy.distance import geodesic import config as cfg -from services import api_client -from utils.sites import get_sites +from utils.data import read_stored_DataFrame DEPARTMENTS = requests.get(cfg.GEOJSON_FILE, timeout=10).json() @@ -34,12 +33,12 @@ def build_departments_geojson(): return geojson -def calculate_new_polygon_parameters(azimuth, opening_angle, localization): +def calculate_new_polygon_parameters(azimuth, opening_angle, bboxes): """ - This function compute the vision polygon parameters based on localization + This function compute the vision polygon parameters based on bboxes """ - # Assuming localization is in the format [x0, y0, x1, y1, confidence] - x0, _, width, _ = localization + # Assuming bboxes is in the format [x0, y0, x1, y1, confidence] + x0, _, width, _ = bboxes xc = (x0 + width / 2) / 100 # New azimuth @@ -51,7 +50,7 @@ def calculate_new_polygon_parameters(azimuth, opening_angle, localization): return int(new_azimuth) % 360, int(new_opening_angle) -def build_sites_markers(user_headers, user_credentials): +def build_sites_markers(api_cameras): """ This function reads the site markers by making the API, that contains all the information about the sites equipped with detection units. @@ -70,17 +69,16 @@ def build_sites_markers(user_headers, user_credentials): "popupAnchor": [0, -20], # Point from which the popup should open relative to the iconAnchor } - user_token = user_headers["Authorization"].split(" ")[1] - api_client.token = user_token + api_cameras, _ = read_stored_DataFrame(api_cameras) - client_sites = get_sites(user_credentials) + client_sites = api_cameras.drop_duplicates(subset=["lat", "lon"], keep="first") # Keeps the first occurrence markers = [] for _, site in client_sites.iterrows(): site_id = site["id"] lat = round(site["lat"], 4) lon = round(site["lon"], 4) - site_name = site["name"].replace("_", " ").title() + site_name = site["name"][:-3].replace("_", " ").title() markers.append( dl.Marker( id=f"site_{site_id}", # Necessary to set an id for each marker to receive callbacks @@ -102,12 +100,12 @@ def build_sites_markers(user_headers, user_credentials): return markers, client_sites -def build_vision_polygon(site_lat, site_lon, azimuth, opening_angle, dist_km, localization=None): +def build_vision_polygon(site_lat, site_lon, azimuth, opening_angle, dist_km, bboxes=None): """ Create a vision polygon using dl.Polygon. This polygon is placed on the map using alerts data. """ - if len(localization): - azimuth, opening_angle = calculate_new_polygon_parameters(azimuth, opening_angle, localization[0]) + if len(bboxes): + azimuth, opening_angle = calculate_new_polygon_parameters(azimuth, opening_angle, bboxes[0]) # The center corresponds the point from which the vision angle "starts" center = [site_lat, site_lon] @@ -137,7 +135,7 @@ def build_vision_polygon(site_lat, site_lon, azimuth, opening_angle, dist_km, lo return polygon, azimuth -def build_alerts_map(user_headers, user_credentials, id_suffix=""): +def build_alerts_map(api_cameras, id_suffix=""): """ The following function mobilises functions defined hereabove or in the utils module to instantiate and return a dl.Map object, corresponding to the "Alerts and Infrastructure" view. @@ -150,7 +148,7 @@ def build_alerts_map(user_headers, user_credentials, id_suffix=""): "height": "100%", } - markers, client_sites = build_sites_markers(user_headers, user_credentials) + markers, client_sites = build_sites_markers(api_cameras) map_object = dl.Map( center=[ @@ -171,20 +169,20 @@ def build_alerts_map(user_headers, user_credentials, id_suffix=""): return map_object -def create_event_list_from_alerts(api_events): +def create_event_list_from_alerts(api_events, cameras): """ This function build the list of events on the left based on event data """ if api_events.empty: return [] - filtered_events = api_events.sort_values("created_at").drop_duplicates("id", keep="last")[::-1] + filtered_events = api_events.sort_values("created_at").drop_duplicates("event_id", keep="last")[::-1] return [ html.Button( - id={"type": "event-button", "index": event["id"]}, + id={"type": "event-button", "index": event["event_id"]}, children=[ html.Div( - f"{event['device_login'][:-2].replace('_', ' ')} - {int(event['device_azimuth'])}°", + f"{cameras[cameras["id"] == event["camera_id"]]['name'].values[0][:-3].replace('_', ' ')} : {int(event['azimuth'])}°", style={"fontWeight": "bold"}, ), html.Div(event["created_at"].strftime("%Y-%m-%d %H:%M")), diff --git a/app/utils/sites.py b/app/utils/sites.py index c65708fe..0486ef45 100644 --- a/app/utils/sites.py +++ b/app/utils/sites.py @@ -5,11 +5,8 @@ from typing import Any, Dict, Optional -import pandas as pd import requests -import config as cfg - def get_token(api_url: str, login: str, pwd: str) -> str: response = requests.post(f"{api_url}/login/access-token", data={"username": login, "password": pwd}, timeout=10) @@ -23,16 +20,3 @@ def api_request(method_type: str, route: str, headers=Dict[str, str], payload: O response = getattr(requests, method_type)(route, headers=headers, **kwargs) return response.json() - - -def get_sites(user_credentials): - api_url = cfg.API_URL.rstrip("/") - superuser_login = user_credentials["username"] - superuser_pwd = user_credentials["password"] - - superuser_auth = { - "Authorization": f"Bearer {get_token(api_url, superuser_login, superuser_pwd)}", - "Content-Type": "application/json", - } - api_sites = api_request("get", f"{api_url}/sites/", superuser_auth) - return pd.DataFrame(api_sites) From 365cd8c55c1acb76c947353d4dc89195be2162fa Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:09:33 +0100 Subject: [PATCH 04/11] update api version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index b206137d..dc936549 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,7 @@ dash = ">=2.14.0" dash-bootstrap-components = ">=1.5.0" dash-leaflet = "^0.1.4" pandas = ">=2.1.4" -pyroclient = { git = "https://github.com/pyronear/pyro-api.git", branch = "old-production", subdirectory = "client" } +pyroclient = { git = "https://github.com/pyronear/pyro-api.git", branch = "main", subdirectory = "client" } python-dotenv = ">=1.0.0" geopy = ">=2.4.0" From 2c9a841a2f6649aa219edfca68f416fa54581e07 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:10:46 +0100 Subject: [PATCH 05/11] remove unused --- app/utils/sites.py | 22 ---------------------- 1 file changed, 22 deletions(-) delete mode 100644 app/utils/sites.py diff --git a/app/utils/sites.py b/app/utils/sites.py deleted file mode 100644 index 0486ef45..00000000 --- a/app/utils/sites.py +++ /dev/null @@ -1,22 +0,0 @@ -# Copyright (C) 2020-2024, Pyronear. - -# This program is licensed under the Apache License 2.0. -# See LICENSE or go to for full license details. - -from typing import Any, Dict, Optional - -import requests - - -def get_token(api_url: str, login: str, pwd: str) -> str: - response = requests.post(f"{api_url}/login/access-token", data={"username": login, "password": pwd}, timeout=10) - if response.status_code != 200: - raise ValueError(response.json()["detail"]) - return response.json()["access_token"] - - -def api_request(method_type: str, route: str, headers=Dict[str, str], payload: Optional[Dict[str, Any]] = None): - kwargs = {"json": payload} if isinstance(payload, dict) else {} - - response = getattr(requests, method_type)(route, headers=headers, **kwargs) - return response.json() From 1b89f4aac4c242169841974ec27ecb7183b0f529 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:14:33 +0100 Subject: [PATCH 06/11] headers --- app/callbacks/data_callbacks.py | 2 +- app/callbacks/display_callbacks.py | 2 +- app/components/alerts.py | 2 +- app/components/navbar.py | 2 +- app/config.py | 2 +- app/index.py | 2 +- app/layouts/main_layout.py | 2 +- app/logging_config.py | 2 +- app/main.py | 2 +- app/pages/homepage.py | 2 +- app/pages/login.py | 2 +- app/services/api.py | 2 +- app/utils/data.py | 2 +- app/utils/display.py | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/app/callbacks/data_callbacks.py b/app/callbacks/data_callbacks.py index 97dd73db..e935d0ac 100644 --- a/app/callbacks/data_callbacks.py +++ b/app/callbacks/data_callbacks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/callbacks/display_callbacks.py b/app/callbacks/display_callbacks.py index 472037c1..0ad09bb8 100644 --- a/app/callbacks/display_callbacks.py +++ b/app/callbacks/display_callbacks.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023-2024, Pyronear. +# Copyright (C) 2023-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/components/alerts.py b/app/components/alerts.py index 8b56a9aa..d37be843 100644 --- a/app/components/alerts.py +++ b/app/components/alerts.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/components/navbar.py b/app/components/navbar.py index 678a79a8..e1a16ef4 100644 --- a/app/components/navbar.py +++ b/app/components/navbar.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023-2024, Pyronear. +# Copyright (C) 2023-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/config.py b/app/config.py index 27d30f13..09bcd826 100644 --- a/app/config.py +++ b/app/config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/index.py b/app/index.py index 8ff33d36..54776e39 100644 --- a/app/index.py +++ b/app/index.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/layouts/main_layout.py b/app/layouts/main_layout.py index 624d4f5a..5f585363 100644 --- a/app/layouts/main_layout.py +++ b/app/layouts/main_layout.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/logging_config.py b/app/logging_config.py index 3c9af0ea..40c7a7f4 100644 --- a/app/logging_config.py +++ b/app/logging_config.py @@ -1,4 +1,4 @@ -# Copyright (C) 2023-2024, Pyronear. +# Copyright (C) 2023-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/main.py b/app/main.py index ccbe27d9..ac5b0aeb 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/pages/homepage.py b/app/pages/homepage.py index 6608a77a..0863a85e 100644 --- a/app/pages/homepage.py +++ b/app/pages/homepage.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/pages/login.py b/app/pages/login.py index 0e2aac91..97f5b8b4 100644 --- a/app/pages/login.py +++ b/app/pages/login.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/services/api.py b/app/services/api.py index b3cbf705..4099d41e 100644 --- a/app/services/api.py +++ b/app/services/api.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/utils/data.py b/app/utils/data.py index 26158fec..4309539f 100644 --- a/app/utils/data.py +++ b/app/utils/data.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. diff --git a/app/utils/display.py b/app/utils/display.py index 4118f410..0ca06720 100644 --- a/app/utils/display.py +++ b/app/utils/display.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2024, Pyronear. +# Copyright (C) 2020-2025, Pyronear. # This program is licensed under the Apache License 2.0. # See LICENSE or go to for full license details. From f8e13396dbd361ac4dc7b59dc1106fcf8f4dfee7 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 8 Jan 2025 17:26:08 +0100 Subject: [PATCH 07/11] fix style --- app/utils/display.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/app/utils/display.py b/app/utils/display.py index 0ca06720..85e8b65b 100644 --- a/app/utils/display.py +++ b/app/utils/display.py @@ -182,7 +182,10 @@ def create_event_list_from_alerts(api_events, cameras): id={"type": "event-button", "index": event["event_id"]}, children=[ html.Div( - f"{cameras[cameras["id"] == event["camera_id"]]['name'].values[0][:-3].replace('_', ' ')} : {int(event['azimuth'])}°", + ( + f"{cameras[cameras['id'] == event['camera_id']]['name'].values[0][:-3].replace('_', ' ')}" + f" : {int(event['azimuth'])}°" + ), style={"fontWeight": "bold"}, ), html.Div(event["created_at"].strftime("%Y-%m-%d %H:%M")), From 7e9ce27a6677d896e7998aced0a4415acc370c82 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Tue, 28 Jan 2025 18:43:31 +0100 Subject: [PATCH 08/11] adapt tp new api routes --- app/callbacks/data_callbacks.py | 109 +++++++++++++++----- app/callbacks/display_callbacks.py | 158 +++++++++-------------------- app/components/alerts.py | 4 +- app/config.py | 7 ++ app/layouts/main_layout.py | 37 +++---- app/pages/homepage.py | 1 - app/utils/data.py | 28 ----- app/utils/display.py | 13 ++- 8 files changed, 163 insertions(+), 194 deletions(-) diff --git a/app/callbacks/data_callbacks.py b/app/callbacks/data_callbacks.py index e935d0ac..a00d7f45 100644 --- a/app/callbacks/data_callbacks.py +++ b/app/callbacks/data_callbacks.py @@ -4,19 +4,19 @@ # See LICENSE or go to for full license details. import json -from datetime import datetime, timedelta +from io import StringIO import dash import logging_config import pandas as pd -from dash import dcc, html +from dash import callback_context, dcc, html from dash.dependencies import Input, Output, State from dash.exceptions import PreventUpdate from main import app import config as cfg from services import api_client, get_token -from utils.data import assign_event_ids, process_bbox, read_stored_DataFrame +from utils.data import process_bbox logger = logging_config.configure_logging(cfg.DEBUG, cfg.SENTRY_DSN) @@ -145,19 +145,19 @@ def get_cameras(user_token): logger.info("Get cameras data") cameras = pd.DataFrame(api_client.fetch_cameras().json()) - return json.dumps({"data": cameras.to_json(orient="split"), "data_loaded": True}) + return cameras.to_json(orient="split") @app.callback( - Output("api_detections", "data"), + Output("api_sequences", "data"), [Input("main_api_fetch_interval", "n_intervals"), Input("api_cameras", "data")], [ - State("api_detections", "data"), + State("api_sequences", "data"), State("user_token", "data"), ], prevent_initial_call=True, ) -def api_watcher(n_intervals, api_cameras, local_detections, user_token): +def api_watcher(n_intervals, api_cameras, local_sequences, user_token): """ Callback to periodically fetch alerts data from the API. @@ -176,27 +176,82 @@ def api_watcher(n_intervals, api_cameras, local_detections, user_token): if user_token is None: raise PreventUpdate - logger.info("Start Fetching the events") - # Fetch Detections - previous_time_event = (datetime.now() - timedelta(days=2)).strftime("%Y-%m-%d_%H:%M:%S") - response = api_client.fetch_unlabeled_detections(from_date=previous_time_event, limit=50) - api_detections = pd.DataFrame(response.json()) - - local_detections, _ = read_stored_DataFrame(local_detections) - if len(api_detections) == 0: - return json.dumps( - { - "data": pd.DataFrame().to_json(orient="split"), - "data_loaded": True, - } - ) + logger.info("Start Fetching Sequences") + # Fetch Sequences + response = api_client.fetch_latest_sequences() + api_sequences = pd.DataFrame(response.json()) + + local_sequences = pd.read_json(StringIO(local_sequences), orient="split") + if len(api_sequences) == 0: + return pd.DataFrame().to_json(orient="split") else: - api_detections["processed_bboxes"] = api_detections["bboxes"].apply(process_bbox) - api_detections = assign_event_ids(api_detections, time_threshold=30 * 60) - if not local_detections.empty: - aligned_api_detections, aligned_local_detections = api_detections["id"].align(local_detections["id"]) - if all(aligned_api_detections == aligned_local_detections): + if not local_sequences.empty: + aligned_api_sequences, aligned_local_sequences = api_sequences["id"].align(local_sequences["id"]) + if all(aligned_api_sequences == aligned_local_sequences): return dash.no_update - return json.dumps({"data": api_detections.to_json(orient="split"), "data_loaded": True}) + return api_sequences.to_json(orient="split") + + +@app.callback( + [Output("are_detections_loaded", "data"), Output("sequence_on_display", "data"), Output("api_detections", "data")], + [Input("api_sequences", "data"), Input("sequence_id_on_display", "data"), Input("api_detections", "data")], + State("are_detections_loaded", "data"), + prevent_initial_call=True, +) +def load_detections(api_sequences, sequence_id_on_display, api_detections, are_detections_loaded): + # Deserialize data + api_sequences = pd.read_json(StringIO(api_sequences), orient="split") + sequence_id_on_display = str(sequence_id_on_display) + are_detections_loaded = json.loads(are_detections_loaded) + api_detections = json.loads(api_detections) + + # Initialize sequence_on_display + sequence_on_display = pd.DataFrame().to_json(orient="split") + + # Identify which input triggered the callback + ctx = callback_context + if not ctx.triggered: + raise PreventUpdate + + triggered_input = ctx.triggered[0]["prop_id"].split(".")[0] + + if triggered_input == "sequence_id_on_display": + # If the displayed sequence changes, load its detections if not already loaded + if sequence_id_on_display not in api_detections: + response = api_client.fetch_sequences_detections(sequence_id_on_display) + detections = pd.DataFrame(response.json()) + detections["processed_bboxes"] = detections["bboxes"].apply(process_bbox) + api_detections[sequence_id_on_display] = detections.to_json(orient="split") + + sequence_on_display = api_detections[sequence_id_on_display] + last_seen_at = api_sequences.loc[ + api_sequences["id"].astype("str") == sequence_id_on_display, "last_seen_at" + ].iloc[0] + + # Ensure last_seen_at is stored as a string + are_detections_loaded[sequence_id_on_display] = str(last_seen_at) + + else: + # If no specific sequence is triggered, load detections for the first missing sequence + for _, row in api_sequences.iterrows(): + sequence_id = str(row["id"]) + last_seen_at = row["last_seen_at"] + + if sequence_id not in are_detections_loaded or are_detections_loaded[sequence_id] != str(last_seen_at): + response = api_client.fetch_sequences_detections(sequence_id) + detections = pd.DataFrame(response.json()) + detections["processed_bboxes"] = detections["bboxes"].apply(process_bbox) + api_detections[sequence_id] = detections.to_json(orient="split") + are_detections_loaded[sequence_id] = str(last_seen_at) + break + + # Clean up old sequences that are no longer in api_sequences + sequences_in_api = api_sequences["id"].astype("str").values + to_drop = [key for key in are_detections_loaded if key not in sequences_in_api] + for key in to_drop: + are_detections_loaded.pop(key, None) + + # Serialize and return data + return json.dumps(are_detections_loaded), sequence_on_display, json.dumps(api_detections) diff --git a/app/callbacks/display_callbacks.py b/app/callbacks/display_callbacks.py index 0ad09bb8..8fedbda9 100644 --- a/app/callbacks/display_callbacks.py +++ b/app/callbacks/display_callbacks.py @@ -5,10 +5,10 @@ import ast import json +from io import StringIO import dash import logging_config -import numpy as np import pandas as pd from dash.dependencies import ALL, Input, Output, State from dash.exceptions import PreventUpdate @@ -16,7 +16,6 @@ import config as cfg from services import api_client -from utils.data import read_stored_DataFrame from utils.display import build_vision_polygon, create_event_list_from_alerts logger = logging_config.configure_logging(cfg.DEBUG, cfg.SENTRY_DSN) @@ -24,14 +23,14 @@ # Create event list @app.callback( - Output("alert-list-container", "children"), + Output("sequence-list-container", "children"), [ - Input("api_detections", "data"), + Input("api_sequences", "data"), Input("to_acknowledge", "data"), ], State("api_cameras", "data"), ) -def update_event_list(api_detections, to_acknowledge, cameras): +def update_event_list(api_sequences, to_acknowledge, cameras): """ Updates the event list based on changes in the events data or acknowledgement actions. @@ -44,34 +43,21 @@ def update_event_list(api_detections, to_acknowledge, cameras): """ logger.info("update_event_list") - api_detections, event_data_loaded = read_stored_DataFrame(api_detections) - cameras, _ = read_stored_DataFrame(cameras) + api_sequences = pd.read_json(StringIO(api_sequences), orient="split") + cameras = pd.read_json(StringIO(cameras), orient="split") - print("api") - if not event_data_loaded: - raise PreventUpdate - - if len(api_detections): + if len(api_sequences): # Drop acknowledge event for faster update - api_detections = api_detections[~api_detections["event_id"].isin([to_acknowledge])] - - # Drop event with less than 5 alerts or less then 2 bbox - drop_event = [] - for event_id in np.unique(api_detections["event_id"].values): - event_alerts = api_detections[api_detections["event_id"] == event_id] - if np.sum([len(box) > 2 for box in event_alerts["bboxes"]]) < 2 or len(event_alerts) < 5: - drop_event.append(event_id) - - api_detections = api_detections[~api_detections["event_id"].isin([drop_event])] + api_sequences = api_sequences[~api_sequences["id"].isin([to_acknowledge])] - return create_event_list_from_alerts(api_detections, cameras) + return create_event_list_from_alerts(api_sequences, cameras) # Select the event id @app.callback( [ Output({"type": "event-button", "index": ALL}, "style"), - Output("event_id_on_display", "data"), + Output("sequence_id_on_display", "data"), Output("auto-move-button", "n_clicks"), Output("custom_js_trigger", "title"), ], @@ -80,12 +66,12 @@ def update_event_list(api_detections, to_acknowledge, cameras): ], [ State({"type": "event-button", "index": ALL}, "id"), - State("api_detections", "data"), - State("event_id_on_display", "data"), + State("api_sequences", "data"), + State("sequence_id_on_display", "data"), ], prevent_initial_call=True, ) -def select_event_with_button(n_clicks, button_ids, local_alerts, event_id_on_display): +def select_event_with_button(n_clicks, button_ids, api_sequences, sequence_id_on_display): """ Handles event selection through button clicks. @@ -93,7 +79,7 @@ def select_event_with_button(n_clicks, button_ids, local_alerts, event_id_on_dis - n_clicks (list): List of click counts for each event button. - button_ids (list): List of button IDs corresponding to events. - local_alerts (json): JSON formatted data containing current alert information. - - event_id_on_display (int): Currently displayed event ID. + - sequence_id_on_display (int): Currently displayed event ID. Returns: - list: List of styles for event buttons. @@ -103,13 +89,10 @@ def select_event_with_button(n_clicks, button_ids, local_alerts, event_id_on_dis logger.info("select_event_with_button") ctx = dash.callback_context - local_alerts, alerts_data_loaded = read_stored_DataFrame(local_alerts) - if len(local_alerts) == 0: + api_sequences = pd.read_json(StringIO(api_sequences), orient="split") + if api_sequences.empty: return [[], 0, 1, "reset_zoom"] - if not alerts_data_loaded: - raise PreventUpdate - # Extracting the index of the clicked button button_id = ctx.triggered[0]["prop_id"].split(".")[0] if button_id: @@ -148,67 +131,27 @@ def select_event_with_button(n_clicks, button_ids, local_alerts, event_id_on_dis return [styles, button_index, 1, "reset_zoom"] -# Get event_id data -@app.callback( - Output("alert_on_display", "data"), - Input("event_id_on_display", "data"), - State("api_detections", "data"), - prevent_initial_call=True, -) -def update_display_data(event_id_on_display, local_alerts): - """ - Updates the display data based on the currently selected event ID. - - Parameters: - - event_id_on_display (int): Currently displayed event ID. - - local_alerts (json): JSON formatted data containing current alert information. - - Returns: - - json: JSON formatted data for the selected event. - """ - logger.info("update_display_data") - local_alerts, data_loaded = read_stored_DataFrame(local_alerts) - - if not data_loaded: - raise PreventUpdate - - if event_id_on_display == 0: - return json.dumps( - { - "data": pd.DataFrame().to_json(orient="split"), - "data_loaded": True, - } - ) - else: - if event_id_on_display == 0: - event_id_on_display = local_alerts["event_id"].values[0] - - alert_on_display = local_alerts[local_alerts["event_id"] == event_id_on_display] - - return json.dumps({"data": alert_on_display.to_json(orient="split"), "data_loaded": True}) - - @app.callback( [ Output("main-image", "src"), # Output for the image Output("bbox-positioning", "style"), Output("image-slider", "max"), ], - [Input("image-slider", "value"), Input("alert_on_display", "data")], + [Input("image-slider", "value"), Input("sequence_on_display", "data")], [ - State("alert-list-container", "children"), + State("sequence-list-container", "children"), State("language", "data"), ], prevent_initial_call=True, ) -def update_image_and_bbox(slider_value, alert_data, alert_list, lang): +def update_image_and_bbox(slider_value, sequence_on_display, sequence_list, lang): """ Updates the image and bounding box display based on the slider value. Parameters: - slider_value (int): Current value of the image slider. - alert_data (json): JSON formatted data for the selected event. - - alert_list (list): List of ongoing alerts. + - sequence_list (list): List of ongoing alerts. Returns: - html.Img: An image element displaying the selected alert image. @@ -221,19 +164,19 @@ def update_image_and_bbox(slider_value, alert_data, alert_list, lang): no_alert_image_src = "./assets/images/no-alert-default-es.png" bbox_style = {"display": "none"} # Default style for the bounding box - alert_data, data_loaded = read_stored_DataFrame(alert_data) + sequence_on_display = pd.read_json(StringIO(sequence_on_display), orient="split") - if not data_loaded: + if sequence_on_display.empty: raise PreventUpdate - if len(alert_list) == 0: + if len(sequence_list) == 0: return no_alert_image_src, bbox_style, 0 # Filter images with non-empty URLs images, boxes = zip( *( (alert["url"], alert["processed_bboxes"]) - for _, alert in alert_data.iterrows() + for _, alert in sequence_on_display.iterrows() if alert["url"] # Only include if url is not empty ) ) @@ -329,11 +272,11 @@ def toggle_auto_move(n_clicks, data): State("image-slider", "value"), State("image-slider", "max"), State("auto-move-button", "n_clicks"), - State("alert-list-container", "children"), + State("sequence-list-container", "children"), ], prevent_initial_call=True, ) -def auto_move_slider(n_intervals, current_value, max_value, auto_move_clicks, alert_list): +def auto_move_slider(n_intervals, current_value, max_value, auto_move_clicks, sequence_list): """ Automatically moves the image slider based on a regular interval and the current auto-move state. @@ -342,12 +285,12 @@ def auto_move_slider(n_intervals, current_value, max_value, auto_move_clicks, al - current_value (int): Current value of the image slider. - max_value (int): Maximum value of the image slider. - auto_move_clicks (int): Number of clicks on the auto-move button. - - alert_list (list): List of ongoing alerts. + - sequence_list (list): List of ongoing alerts. Returns: - int: Updated value for the image slider. """ - if auto_move_clicks % 2 != 0 and len(alert_list): # Auto-move is active and there is ongoing alerts + if auto_move_clicks % 2 != 0 and len(sequence_list): # Auto-move is active and there is ongoing alerts return (current_value + 1) % (max_value + 1) else: raise PreventUpdate @@ -356,10 +299,10 @@ def auto_move_slider(n_intervals, current_value, max_value, auto_move_clicks, al @app.callback( Output("download-link", "href"), [Input("image-slider", "value")], - [State("alert_on_display", "data")], + [State("sequence_on_display", "data")], prevent_initial_call=True, ) -def update_download_link(slider_value, alert_data): +def update_download_link(slider_value, sequence_on_display): """ Updates the download link for the currently displayed image. @@ -370,13 +313,13 @@ def update_download_link(slider_value, alert_data): Returns: - str: URL for downloading the current image. """ - alert_data, data_loaded = read_stored_DataFrame(alert_data) - if data_loaded and len(alert_data): + sequence_on_display = pd.read_json(StringIO(sequence_on_display), orient="split") + if len(sequence_on_display): try: - return alert_data["url"].values[slider_value] + return sequence_on_display["url"].values[slider_value] except Exception as e: logger.info(e) - logger.info(f"Size of the alert_data dataframe: {alert_data.size}") + logger.info(f"Size of the alert_data dataframe: {sequence_on_display.size}") return "" # Return empty string if no image URL is available @@ -395,11 +338,11 @@ def update_download_link(slider_value, alert_data): Output("alert-information", "style"), Output("slider-container", "style"), ], - Input("alert_on_display", "data"), + Input("sequence_on_display", "data"), State("api_cameras", "data"), prevent_initial_call=True, ) -def update_map_and_alert_info(alert_data, cameras): +def update_map_and_alert_info(sequence_on_display, cameras): """ Updates the map's vision polygons, center, and alert information based on the current alert data. @@ -419,24 +362,21 @@ def update_map_and_alert_info(alert_data, cameras): - dict: Style settings for the slider container. """ logger.info("update_map_and_alert_info") - alert_data, data_loaded = read_stored_DataFrame(alert_data) - cameras, _ = read_stored_DataFrame(cameras) - - if not data_loaded: - raise PreventUpdate + sequence_on_display = pd.read_json(StringIO(sequence_on_display), orient="split") + cameras = pd.read_json(StringIO(cameras), orient="split") - if not alert_data.empty: + if not sequence_on_display.empty: # Convert the 'bboxes' column to a list (empty lists if the original value was '[]'). - alert_data["bboxes"] = alert_data["bboxes"].apply( + sequence_on_display["bboxes"] = sequence_on_display["bboxes"].apply( lambda x: ast.literal_eval(x) if isinstance(x, str) and x.strip() != "[]" else [] ) # Filter out rows where 'bboxes' is not empty and get the last one. # If all are empty, then simply get the last row of the DataFrame. row_with_bboxes = ( - alert_data[alert_data["bboxes"].astype(bool)].iloc[-1] - if not alert_data[alert_data["bboxes"].astype(bool)].empty - else alert_data.iloc[-1] + sequence_on_display[sequence_on_display["bboxes"].astype(bool)].iloc[-1] + if not sequence_on_display[sequence_on_display["bboxes"].astype(bool)].empty + else sequence_on_display.iloc[-1] ) row_cam = cameras[cameras["id"] == row_with_bboxes["camera_id"]] @@ -490,33 +430,33 @@ def update_map_and_alert_info(alert_data, cameras): Output("to_acknowledge", "data"), [Input("acknowledge-button", "n_clicks")], [ - State("event_id_on_display", "data"), + State("sequence_id_on_display", "data"), State("user_token", "data"), ], prevent_initial_call=True, ) -def acknowledge_event(n_clicks, event_id_on_display, user_token): +def acknowledge_event(n_clicks, sequence_id_on_display, user_token): """ Acknowledges the selected event and updates the state to reflect this. Parameters: - n_clicks (int): Number of clicks on the acknowledge button. - - event_id_on_display (int): Currently displayed event ID. + - sequence_id_on_display (int): Currently displayed event ID. - user_token (dict): User authorization headers for API requests. Returns: - int: The ID of the event that has been acknowledged. """ - if event_id_on_display == 0 or n_clicks == 0: + if sequence_id_on_display == 0 or n_clicks == 0: raise PreventUpdate if cfg.SAFE_DEV_MODE == "True": raise PreventUpdate api_client.token = user_token - # call_api(api_client.acknowledge_event, user_credentials)(event_id=int(event_id_on_display)) + # call_api(api_client.acknowledge_event, user_credentials)(event_id=int(sequence_id_on_display)) - return event_id_on_display + return sequence_id_on_display # Modal issue let's add this later diff --git a/app/components/alerts.py b/app/components/alerts.py index d37be843..a89a27a4 100644 --- a/app/components/alerts.py +++ b/app/components/alerts.py @@ -11,7 +11,7 @@ def create_event_list(): Creates a container for the alert list with a fixed height and scrollable content. This function generates a Dash HTML Div element containing a header and an empty container. - The empty container ('alert-list-container') is meant to be populated with alert buttons + The empty container ('sequence-list-container') is meant to be populated with alert buttons dynamically via a callback. The container has a fixed height and is scrollable, allowing users to browse through a potentially long list of alerts. @@ -27,6 +27,6 @@ def create_event_list(): return html.Div( [ - html.Div(id="alert-list-container", style=event_list_style, children=[]), # Empty container + html.Div(id="sequence-list-container", style=event_list_style, children=[]), # Empty container ] ) diff --git a/app/config.py b/app/config.py index 09bcd826..57a748fa 100644 --- a/app/config.py +++ b/app/config.py @@ -30,3 +30,10 @@ MAX_ALERTS_PER_EVENT = 10 CAM_OPENING_ANGLE = 87 CAM_RANGE_KM = 15 + +API_URL = "https://alertapi.pyronear.org/" +# API_LOGIN="pyroadmin" +# API_PWD = "kctt&NMsRb7j!3?c" +# API_PWD = "HelloTrees2023!" +API_LOGIN = "test-mateo" +API_PWD = "testmateo" diff --git a/app/layouts/main_layout.py b/app/layouts/main_layout.py index 5f585363..0594c494 100644 --- a/app/layouts/main_layout.py +++ b/app/layouts/main_layout.py @@ -18,8 +18,6 @@ else: user_token = None -print("user token AA", user_token) - def get_main_layout(): return html.Div( @@ -38,37 +36,32 @@ def get_main_layout(): ] ), dcc.Interval(id="main_api_fetch_interval", interval=30 * 1000), + dcc.Store( + id="api_sequences", + storage_type="session", + data=pd.DataFrame().to_json(orient="split"), + ), dcc.Store( id="api_detections", storage_type="session", - data=json.dumps( - { - "data": pd.DataFrame().to_json(orient="split"), - "data_loaded": False, - } - ), + data=json.dumps({}), + ), + dcc.Store( + id="are_detections_loaded", + storage_type="session", + data=json.dumps({}), ), dcc.Store( id="api_cameras", storage_type="session", - data=json.dumps( - { - "data": pd.DataFrame().to_json(orient="split"), - "data_loaded": False, - } - ), + data=pd.DataFrame().to_json(orient="split"), ), dcc.Store( - id="alert_on_display", + id="sequence_on_display", storage_type="session", - data=json.dumps( - { - "data": pd.DataFrame().to_json(orient="split"), - "data_loaded": False, - } - ), + data=pd.DataFrame().to_json(orient="split"), ), - dcc.Store(id="event_id_on_display", data=0), + dcc.Store(id="sequence_id_on_display", data=0), dcc.Store(id="auto-move-state", data={"active": True}), # Add this to your app.layout dcc.Store(id="bbox_visibility", data={"visible": True}), diff --git a/app/pages/homepage.py b/app/pages/homepage.py index 0863a85e..820e1dc6 100644 --- a/app/pages/homepage.py +++ b/app/pages/homepage.py @@ -15,7 +15,6 @@ def homepage_layout(user_token, api_cameras, lang="fr"): - print("user token 00", user_token) translate = { "fr": { "animate_on_off": "Activer / Désactiver l'animation", diff --git a/app/utils/data.py b/app/utils/data.py index 4309539f..5f2c92f0 100644 --- a/app/utils/data.py +++ b/app/utils/data.py @@ -4,9 +4,7 @@ # See LICENSE or go to for full license details. import ast -import json from datetime import datetime -from io import StringIO from typing import List import pandas as pd @@ -37,32 +35,6 @@ def convert_time(df): return df_ts_local -def read_stored_DataFrame(data): - """ - Reads a JSON-formatted string representing a pandas DataFrame stored in a dcc.Store. - - Args: - data (str): A JSON-formatted string representing the stored DataFrame. - - Returns: - tuple: A tuple containing the DataFrame and a boolean indicating whether data has been loaded. - """ - # if "false" in data: - # return pd.DataFrame().to_json(orient="split"), False - data_dict = json.loads(data) - - # Check if 'data' is empty or if 'columns' is empty - if not len(data_dict["data"]): - # If either is empty, create an empty DataFrame - return pd.DataFrame().to_json(orient="split"), data_dict["data_loaded"] - else: - # Otherwise, read the JSON data into a DataFrame - return ( - pd.read_json(StringIO(data_dict["data"]), orient="split"), - data_dict["data_loaded"], - ) - - def process_bbox(input_str): """ Processes the bounding box information from a xyxy string input to a xywh list of integer coordinates. diff --git a/app/utils/display.py b/app/utils/display.py index 85e8b65b..18ce4fe1 100644 --- a/app/utils/display.py +++ b/app/utils/display.py @@ -4,14 +4,16 @@ # See LICENSE or go to for full license details. +from io import StringIO + import dash_leaflet as dl +import pandas as pd import requests from dash import html from geopy import Point from geopy.distance import geodesic import config as cfg -from utils.data import read_stored_DataFrame DEPARTMENTS = requests.get(cfg.GEOJSON_FILE, timeout=10).json() @@ -69,7 +71,7 @@ def build_sites_markers(api_cameras): "popupAnchor": [0, -20], # Point from which the popup should open relative to the iconAnchor } - api_cameras, _ = read_stored_DataFrame(api_cameras) + api_cameras = pd.read_json(StringIO(api_cameras), orient="split") client_sites = api_cameras.drop_duplicates(subset=["lat", "lon"], keep="first") # Keeps the first occurrence markers = [] @@ -175,11 +177,12 @@ def create_event_list_from_alerts(api_events, cameras): """ if api_events.empty: return [] - filtered_events = api_events.sort_values("created_at").drop_duplicates("event_id", keep="last")[::-1] + + filtered_events = api_events.sort_values("started_at").drop_duplicates("id", keep="last")[::-1] return [ html.Button( - id={"type": "event-button", "index": event["event_id"]}, + id={"type": "event-button", "index": event["id"]}, children=[ html.Div( ( @@ -188,7 +191,7 @@ def create_event_list_from_alerts(api_events, cameras): ), style={"fontWeight": "bold"}, ), - html.Div(event["created_at"].strftime("%Y-%m-%d %H:%M")), + html.Div(event["started_at"].strftime("%Y-%m-%d %H:%M")), ], n_clicks=0, style={ From ad5c2967815b55dae89ad0ccec28068597f91f10 Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Tue, 28 Jan 2025 19:33:38 +0100 Subject: [PATCH 09/11] add acknowledge --- app/callbacks/display_callbacks.py | 73 +++++++++++++++++--------- app/layouts/main_layout.py | 84 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 25 deletions(-) diff --git a/app/callbacks/display_callbacks.py b/app/callbacks/display_callbacks.py index 923e4059..906883a7 100644 --- a/app/callbacks/display_callbacks.py +++ b/app/callbacks/display_callbacks.py @@ -423,36 +423,59 @@ def update_map_and_alert_info(sequence_on_display, cameras): @app.callback( - Output("to_acknowledge", "data"), - [Input("acknowledge-button", "n_clicks")], + [Output("confirmation-modal", "style"), Output("to_acknowledge", "data")], [ - State("sequence_id_on_display", "data"), - State("user_token", "data"), + Input("acknowledge-button", "n_clicks"), + Input("confirm-wildfire", "n_clicks"), + Input("confirm-non-wildfire", "n_clicks"), + Input("cancel-confirmation", "n_clicks"), ], + [State("sequence_id_on_display", "data"), State("user_token", "data")], prevent_initial_call=True, ) -def acknowledge_event(n_clicks, sequence_id_on_display, user_token): - """ - Acknowledges the selected event and updates the state to reflect this. - - Parameters: - - n_clicks (int): Number of clicks on the acknowledge button. - - sequence_id_on_display (int): Currently displayed event ID. - - user_token (dict): User authorization headers for API requests. - - Returns: - - int: The ID of the event that has been acknowledged. - """ - if sequence_id_on_display == 0 or n_clicks == 0: - raise PreventUpdate - - if cfg.SAFE_DEV_MODE == "True": - raise PreventUpdate - - api_client.token = user_token - # call_api(api_client.acknowledge_event, user_credentials)(event_id=int(sequence_id_on_display)) +def acknowledge_event( + acknowledge_clicks, confirm_wildfire, confirm_non_wildfire, cancel, sequence_id_on_display, user_token +): + ctx = dash.callback_context - return sequence_id_on_display + if not ctx.triggered: + raise dash.exceptions.PreventUpdate + + triggered_id = ctx.triggered[0]["prop_id"].split(".")[0] + + # Modal styles + modal_visible_style = { + "position": "fixed", + "top": "50%", + "left": "50%", + "transform": "translate(-50%, -50%)", + "z-index": "1000", + "background-color": "rgba(0, 0, 0, 0.5)", + } + modal_hidden_style = {"display": "none"} + + if triggered_id == "acknowledge-button": + # Show the modal + if acknowledge_clicks > 0: + return modal_visible_style, dash.no_update + + elif triggered_id == "confirm-wildfire": + # Send wildfire confirmation to the API + api_client.token = user_token + api_client.label_sequence(sequence_id_on_display, True) + return modal_hidden_style, sequence_id_on_display + + elif triggered_id == "confirm-non-wildfire": + # Send non-wildfire confirmation to the API + api_client.token = user_token + api_client.label_sequence(sequence_id_on_display, False) + return modal_hidden_style, sequence_id_on_display + + elif triggered_id == "cancel-confirmation": + # Cancel action + return modal_hidden_style, dash.no_update + + raise dash.exceptions.PreventUpdate # Modal issue let's add this later diff --git a/app/layouts/main_layout.py b/app/layouts/main_layout.py index 0594c494..886cc899 100644 --- a/app/layouts/main_layout.py +++ b/app/layouts/main_layout.py @@ -70,5 +70,89 @@ def get_main_layout(): # [TEMPORARY FIX] Storing the user's credentials to refresh the token when needed dcc.Store(id="to_acknowledge", data=0), dcc.Store(id="trigger_no_detections", data=False), + html.Div( + id="confirmation-modal", + style={ + "display": "none", # Hidden by default + "position": "fixed", + "top": "0", + "left": "0", + "width": "100%", + "height": "100%", + "background-color": "rgba(0, 0, 0, 0.5)", + "z-index": "1000", + "justify-content": "center", + "align-items": "center", + }, + children=[ + html.Div( + [ + html.H4( + "Est-ce une fumée suspecte ? ", + style={ + "margin-bottom": "20px", + "font-size": "20px", + "font-weight": "bold", + }, + ), + html.Div( + [ + html.Button( + "Oui, c'est une fumée", + id="confirm-wildfire", + n_clicks=0, + style={ + "margin-right": "10px", + "padding": "10px 20px", + "background-color": "#4CAF50", + "color": "white", + "border": "none", + "border-radius": "5px", + "cursor": "pointer", + }, + ), + html.Button( + "No, c'est un faux positif", + id="confirm-non-wildfire", + n_clicks=0, + style={ + "margin-right": "10px", + "padding": "10px 20px", + "background-color": "#f44336", + "color": "white", + "border": "none", + "border-radius": "5px", + "cursor": "pointer", + }, + ), + html.Button( + "Cancel", + id="cancel-confirmation", + n_clicks=0, + style={ + "padding": "10px 20px", + "background-color": "#555", + "color": "white", + "border": "none", + "border-radius": "5px", + "cursor": "pointer", + }, + ), + ], + style={"display": "flex", "justify-content": "center"}, + ), + ], + style={ + "background-color": "white", + "padding": "30px", + "border-radius": "10px", + "box-shadow": "0 4px 8px rgba(0, 0, 0, 0.2)", + "max-width": "400px", + "width": "100%", + "text-align": "center", + }, + ), + ], + ), ] ) From 79c86384a3268c0e3e84b6cdc6816e5cc9b3905f Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Tue, 28 Jan 2025 19:36:20 +0100 Subject: [PATCH 10/11] update token --- app/callbacks/data_callbacks.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/callbacks/data_callbacks.py b/app/callbacks/data_callbacks.py index a00d7f45..b3107d42 100644 --- a/app/callbacks/data_callbacks.py +++ b/app/callbacks/data_callbacks.py @@ -143,6 +143,8 @@ def login_callback(n_clicks, username, password, user_token, lang): ) def get_cameras(user_token): logger.info("Get cameras data") + if user_token is not None: + api_client.token = user_token cameras = pd.DataFrame(api_client.fetch_cameras().json()) return cameras.to_json(orient="split") From ed6ccd74bf7444b1a31b1e044a4380bb446283cd Mon Sep 17 00:00:00 2001 From: MateoLOSTANLEN Date: Wed, 29 Jan 2025 10:40:08 +0100 Subject: [PATCH 11/11] drop conf --- app/config.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/config.py b/app/config.py index 57a748fa..09bcd826 100644 --- a/app/config.py +++ b/app/config.py @@ -30,10 +30,3 @@ MAX_ALERTS_PER_EVENT = 10 CAM_OPENING_ANGLE = 87 CAM_RANGE_KM = 15 - -API_URL = "https://alertapi.pyronear.org/" -# API_LOGIN="pyroadmin" -# API_PWD = "kctt&NMsRb7j!3?c" -# API_PWD = "HelloTrees2023!" -API_LOGIN = "test-mateo" -API_PWD = "testmateo"