diff --git a/locales/dynalab.pot b/locales/dynalab.pot new file mode 100644 index 0000000..d98ba0f --- /dev/null +++ b/locales/dynalab.pot @@ -0,0 +1,58 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2026-07-09 11:20+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=CHARSET\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/dynalab/app.py:53 +msgid "Fetched DynaLab config" +msgstr "" + +#: src/dynalab/app.py:56 +msgid "Starting JSON data server" +msgstr "" + +#: src/dynalab/app.py:66 +msgid "JSON data server started" +msgstr "" + +#: src/dynalab/app.py:68 +msgid "DynaLab Ready" +msgstr "" + +#: src/dynalab/ui/windows.py:101 +msgid "File" +msgstr "" + +#: src/dynalab/ui/windows.py:102 +msgid "Quit" +msgstr "" + +#: src/dynalab/ui/windows.py:103 src/dynalab/ui/windows.py:110 +msgid "Record" +msgstr "" + +#: src/dynalab/ui/windows.py:105 +msgid "Configuration" +msgstr "" + +#: src/dynalab/ui/windows.py:107 +msgid "Window" +msgstr "" + +#: src/dynalab/ui/windows.py:116 +msgid "Data Input" +msgstr "" diff --git a/locales/en/LC_MESSAGES/dynalab.mo b/locales/en/LC_MESSAGES/dynalab.mo new file mode 100644 index 0000000..0bbb589 Binary files /dev/null and b/locales/en/LC_MESSAGES/dynalab.mo differ diff --git a/locales/en/LC_MESSAGES/dynalab.po b/locales/en/LC_MESSAGES/dynalab.po new file mode 100644 index 0000000..a72c427 --- /dev/null +++ b/locales/en/LC_MESSAGES/dynalab.po @@ -0,0 +1,56 @@ +# DynaLab English Translations. +# Copyright (C) 2026 Exergie +# This file is distributed under the same license as the DynaLab package. +# Hector van der Aa , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: DynaLab\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-07-08 23:50+0100\n" +"Last-Translator: Hector van der Aa \n" +"Language-Team: English\n" +"Language: en\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/dynalab/app.py:53 +msgid "Fetched DynaLab config" +msgstr "Fetched DynaLab config" + +#: src/dynalab/app.py:56 +msgid "Starting JSON data server" +msgstr "Starting JSON data server" + +#: src/dynalab/app.py:66 +msgid "JSON data server started" +msgstr "JSON data server started" + +#: src/dynalab/app.py:68 +msgid "DynaLab Ready" +msgstr "DynaLab Ready" + +#: src/dynalab/ui/windows.py:101 +msgid "File" +msgstr "File" + +#: src/dynalab/ui/windows.py:102 +msgid "Quit" +msgstr "Quit" + +#: src/dynalab/ui/windows.py:103 src/dynalab/ui/windows.py:110 +msgid "Record" +msgstr "Record" + +#: src/dynalab/ui/windows.py:105 +msgid "Configuration" +msgstr "Configuration" + +#: src/dynalab/ui/windows.py:107 +msgid "Window" +msgstr "Window" + +#: src/dynalab/ui/windows.py:116 +msgid "Data Input" +msgstr "Data Input" diff --git a/locales/fr/LC_MESSAGES/dynalab.mo b/locales/fr/LC_MESSAGES/dynalab.mo new file mode 100644 index 0000000..6545ed6 Binary files /dev/null and b/locales/fr/LC_MESSAGES/dynalab.mo differ diff --git a/locales/fr/LC_MESSAGES/dynalab.po b/locales/fr/LC_MESSAGES/dynalab.po new file mode 100644 index 0000000..f9ac9f8 --- /dev/null +++ b/locales/fr/LC_MESSAGES/dynalab.po @@ -0,0 +1,56 @@ +# DynaLab French Translations. +# Copyright (C) 2026 Exergie +# This file is distributed under the same license as the DynaLab package. +# Hector van der Aa , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: DynaLab\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2026-07-08 23:50+0100\n" +"Last-Translator: Hector van der Aa \n" +"Language-Team: French\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: src/dynalab/app.py:53 +msgid "Fetched DynaLab config" +msgstr "Configuration DynaLab récupérée" + +#: src/dynalab/app.py:56 +msgid "Starting JSON data server" +msgstr "Démarrage du serveur de données JSON" + +#: src/dynalab/app.py:66 +msgid "JSON data server started" +msgstr "Serveur de données JSON démarré" + +#: src/dynalab/app.py:68 +msgid "DynaLab Ready" +msgstr "DynaLab prêt" + +#: src/dynalab/ui/windows.py:101 +msgid "File" +msgstr "Fichier" + +#: src/dynalab/ui/windows.py:102 +msgid "Quit" +msgstr "Quitter" + +#: src/dynalab/ui/windows.py:103 src/dynalab/ui/windows.py:110 +msgid "Record" +msgstr "Enregistrer" + +#: src/dynalab/ui/windows.py:105 +msgid "Configuration" +msgstr "Configuration" + +#: src/dynalab/ui/windows.py:107 +msgid "Window" +msgstr "Fenêtre" + +#: src/dynalab/ui/windows.py:116 +msgid "Data Input" +msgstr "Saisie des données" diff --git a/scripts/i18n_compile.sh b/scripts/i18n_compile.sh new file mode 100755 index 0000000..0d8bd49 --- /dev/null +++ b/scripts/i18n_compile.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="dynalab" +LOCALES_DIR="locales" + +found_po=false + +for po_file in "$LOCALES_DIR"/*/LC_MESSAGES/"$DOMAIN.po"; do + if [[ ! -f "$po_file" ]]; then + continue + fi + + found_po=true + mo_file="${po_file%.po}.mo" + + echo "Checking $po_file" + msgfmt --check "$po_file" + + echo "Compiling $po_file -> $mo_file" + msgfmt "$po_file" -o "$mo_file" +done + +if [[ "$found_po" == false ]]; then + echo "No .po files found in $LOCALES_DIR/*/LC_MESSAGES/$DOMAIN.po" + exit 1 +fi + +echo "Compiled all locales" diff --git a/scripts/i18n_extract.sh b/scripts/i18n_extract.sh new file mode 100755 index 0000000..ec753bb --- /dev/null +++ b/scripts/i18n_extract.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +DOMAIN="dynalab" +SOURCE_DIR="src/dynalab" +LOCALES_DIR="locales" +POT_FILE="$LOCALES_DIR/$DOMAIN.pot" + +mkdir -p "$LOCALES_DIR" + +xgettext \ + --language=Python \ + --keyword=_ \ + --keyword=gettext_ \ + --from-code=UTF-8 \ + --output="$POT_FILE" \ + $(find "$SOURCE_DIR" -name "*.py" | sort) + +echo "Generated $POT_FILE" diff --git a/src/dynalab/app.py b/src/dynalab/app.py index c7dadef..bcbc5f2 100644 --- a/src/dynalab/app.py +++ b/src/dynalab/app.py @@ -13,6 +13,7 @@ import sys from dynalab.config import ConfigLoadError, load_config from dynalab.data.json import json_thread_main +from dynalab.i18n import init_i18n, _ from dynalab.state import AppState from dynalab.tags import WINDOW_DATA_INPUT_GAUGE_GRID from dynalab.ui.error import build_error_modal, show_error_modal @@ -28,6 +29,8 @@ from dynalab.ui.windows import ( import dpg_gauges as dpgg from dpg_gauges.exceptions import GaugeNotFoundError +from dynalab.utils import resource_path + def _setup_logging(level: int = logging.INFO) -> None: logging.basicConfig( @@ -38,29 +41,6 @@ def _setup_logging(level: int = logging.INFO) -> None: ) -def _asset_path(relative_path: str) -> str: - path = Path(relative_path) - candidates: list[Path] = [] - - bundle_dir = getattr(sys, "_MEIPASS", None) - if bundle_dir is not None: - candidates.append(Path(bundle_dir) / path) - - candidates.extend( - ( - Path.cwd() / path, - Path(__file__).resolve().parents[2] / path, - ) - ) - - for candidate in candidates: - if candidate.exists(): - return str(candidate) - - searched = ", ".join(str(candidate) for candidate in candidates) - raise FileNotFoundError(f"Missing asset {relative_path!r}. Searched: {searched}") - - def _init_worker( state: AppState, stop_event: threading.Event, message_queue: Queue[str] ) -> None: @@ -69,10 +49,11 @@ def _init_worker( message_queue.put("Fetching DynaLab config") time.sleep(0.5) state.app_config = load_config() - message_queue.put("Fetched DynaLab config") + state.language = init_i18n(state.app_config.display.language) + message_queue.put(_("Fetched DynaLab config")) time.sleep(0.5) - message_queue.put("Starting JSON data server") + message_queue.put(_("Starting JSON data server")) # Init and start json connector thread state.data_json_thread = Thread( target=json_thread_main, @@ -82,9 +63,9 @@ def _init_worker( ) state.data_json_thread.start() time.sleep(0.5) - message_queue.put("JSON data server started") + message_queue.put(_("JSON data server started")) time.sleep(0.5) - message_queue.put("DynaLab Ready") + message_queue.put(_("DynaLab Ready")) time.sleep(1.0) stop_event.set() @@ -130,16 +111,16 @@ def run() -> None: # Load fonts at 2x size for HiDPI scaling with dpg.font_registry(): app_font = dpg.add_font( - _asset_path("assets/fonts/Inter-Regular.ttf"), 18 * 2 + resource_path("assets/fonts/Inter-Regular.ttf"), 18 * 2 ) mono_font = dpg.add_font( - _asset_path("assets/fonts/JetBrainsMono-Regular.ttf"), + resource_path("assets/fonts/JetBrainsMono-Regular.ttf"), size=36, label="mono_font", ) dpg.bind_font(app_font) - _load_image(_asset_path("assets/images/logo@300.png"), "logo") + _load_image(resource_path("assets/images/logo@300.png"), "logo") build_loading() build_error_modal() diff --git a/src/dynalab/config.py b/src/dynalab/config.py index c3821b5..6ce8a23 100644 --- a/src/dynalab/config.py +++ b/src/dynalab/config.py @@ -13,7 +13,12 @@ class JsonConnectorConfig(BaseModel): port: int = Field(default=8765, ge=1, le=65535) +class DisplayConfig(BaseModel): + language: str = "" + + class AppConfig(BaseModel): + display: DisplayConfig = DisplayConfig() json_connector: JsonConnectorConfig = JsonConnectorConfig() diff --git a/src/dynalab/data/json.py b/src/dynalab/data/json.py index bed78fd..7108303 100644 --- a/src/dynalab/data/json.py +++ b/src/dynalab/data/json.py @@ -82,6 +82,7 @@ async def _handle_connector( for signal in connector_hello.signals: state.new_signal_queue.put(signal) + state.available_signals.append(signal) heartbeat_task = asyncio.create_task(_heartbeat_loop(writer)) timeout_event = asyncio.Event() diff --git a/src/dynalab/i18n.py b/src/dynalab/i18n.py new file mode 100644 index 0000000..cb962f3 --- /dev/null +++ b/src/dynalab/i18n.py @@ -0,0 +1,57 @@ +import gettext +import locale + +from dynalab.utils import resource_path + + +DOMAIN = "dynalab" +SUPPORTED_LANGUAGES = {"en", "fr"} + +_translation: gettext.NullTranslations = gettext.NullTranslations() + + +def _normalize_language(language: str | None) -> str: + if not language: + return "en" + + language = language.split(".")[0] + language = language.replace("-", "_") + language = language.split("_")[0] + + if language in SUPPORTED_LANGUAGES: + return language + + return "en" + + +def detect_system_language() -> str: + language, _encoding = locale.getlocale() + + if not language: + language, _encoding = locale.getdefaultlocale() + + return _normalize_language(language) + + +def init_i18n(config_language: str | None) -> str: + global _translation + + if config_language: + language = _normalize_language(config_language) + else: + language = detect_system_language() + + locale_dir = resource_path("locales") + + _translation = gettext.translation( + DOMAIN, localedir=locale_dir, languages=[language], fallback=True + ) + + return language + + +def gettext_(message: str) -> str: + return _translation.gettext(message) + + +_ = gettext_ diff --git a/src/dynalab/state.py b/src/dynalab/state.py index 265c26b..8ed5141 100644 --- a/src/dynalab/state.py +++ b/src/dynalab/state.py @@ -7,6 +7,7 @@ import threading from uuid import UUID from dynalab_protocol.models import SignalDescriptor +from dynalab_protocol.dlconf import DLConf from dynalab.config import AppConfig @@ -19,6 +20,7 @@ class AppState: new_signal_queue: Queue[SignalDescriptor] = Queue() removed_signal_queue: Queue[SignalDescriptor] = Queue() + available_signals: list[SignalDescriptor] = field(default_factory=list) live_data: dict[UUID, float] = field(default_factory=dict) new_data_id: Queue[UUID] = Queue() @@ -28,3 +30,8 @@ class AppState: init_error: str | None = None window_map: dict[str, str] = field(default_factory=dict) + + language: str = "" + + loaded_dlconf: DLConf | None = None + building_dlconf: DLConf | None = None diff --git a/src/dynalab/tags.py b/src/dynalab/tags.py index 0d8993c..06597d6 100644 --- a/src/dynalab/tags.py +++ b/src/dynalab/tags.py @@ -2,6 +2,9 @@ # Copyright (C) 2026 Association Exergie # SPDX-License-Identifier: GPL-3.0-or-later +WINDOW_RECORD_CONFIGURATION: str = "window_record_configuration" +WINDOW_RECORD_CONFIGURATION_ADD_SIGNAL: str = "window_record_configuration_add_signal" + # Window groups WINDOW_GROUP_DATA_INPUT: str = "window_group_data_input" WINDOW_GROUP_RECORD: str = "window_group_record" @@ -13,3 +16,8 @@ MENU_FILE_QUIT: str = "menu_file_quit" MENU_RECORD_CONFIGURATION: str = "menu_record_configuration" MENU_WINDOW_RECORD: str = "menu_window_record" MENU_WINDOW_DATA_INPUT: str = "menu_window_data_input" + +LISTBOX_RECORD_CONFIGURATION: str = "listbox_record_configuration" +LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL: str = "listbox_record_configuration_add_singal" + +TEXT_INPUT_RECORD_CONFIGURATION: str = "text_input_record_configuration" diff --git a/src/dynalab/ui/render.py b/src/dynalab/ui/render.py new file mode 100644 index 0000000..82c422e --- /dev/null +++ b/src/dynalab/ui/render.py @@ -0,0 +1,30 @@ +import dearpygui.dearpygui as dpg + +from dynalab.state import AppState +from dynalab.tags import ( + LISTBOX_RECORD_CONFIGURATION, + LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL, + TEXT_INPUT_RECORD_CONFIGURATION, +) + + +def render_window_record_configuration(state: AppState) -> None: + dpg.set_value( + item=TEXT_INPUT_RECORD_CONFIGURATION, value=state.building_dlconf.name + ) + + items: list[str] = [] + for signal in state.building_dlconf.signals: + name: str = f"{signal.name} | {signal.id}" + items.append(name) + + dpg.configure_item(LISTBOX_RECORD_CONFIGURATION, items=items) + + +def render_listbox_record_configuration_add_item(state: AppState) -> None: + items: list[str] = [] + for item in state.available_signals: + name: str = f"{item.name} | {item.id}" + items.append(name) + + dpg.configure_item(LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL, items=items) diff --git a/src/dynalab/ui/routines.py b/src/dynalab/ui/routines.py index 9a50582..6a370c7 100644 --- a/src/dynalab/ui/routines.py +++ b/src/dynalab/ui/routines.py @@ -1,7 +1,22 @@ +import uuid + +from dynalab_protocol.dlconf import DLConf from dynalab.state import AppState import dearpygui.dearpygui as dpg +from dynalab.tags import ( + LISTBOX_RECORD_CONFIGURATION, + LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL, + TEXT_INPUT_RECORD_CONFIGURATION, + WINDOW_RECORD_CONFIGURATION, + WINDOW_RECORD_CONFIGURATION_ADD_SIGNAL, +) +from dynalab.ui.render import ( + render_window_record_configuration, + render_listbox_record_configuration_add_item, +) + def switch_window(sender, app_data, user_data: AppState) -> None: for button_tag, window_group_tag in user_data.window_map.items(): @@ -9,3 +24,54 @@ def switch_window(sender, app_data, user_data: AppState) -> None: dpg.show_item(window_group_tag) else: dpg.hide_item(window_group_tag) + + +def listbox_record_configuration_remove_selected_item( + sender, app_data, user_data: AppState +) -> None: + selected = dpg.get_value(LISTBOX_RECORD_CONFIGURATION) + if selected is None: + return + try: + split = selected.split(" | ")[1] + except Exception: + return + signal_id = uuid.UUID(split) + render_window_record_configuration(user_data) + + +def listbox_record_configuration_add_item( + sender, app_data, user_data: AppState +) -> None: + render_listbox_record_configuration_add_item(user_data) + dpg.show_item(WINDOW_RECORD_CONFIGURATION_ADD_SIGNAL) + + +def listbox_record_configuration_add_item_select( + sender, app_data, user_data: AppState +) -> None: + value: str = dpg.get_value(LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL) + try: + split = value.split(" | ")[1] + except Exception: + return + signal_id = uuid.UUID(split) + if user_data.building_dlconf is None: + user_data.building_dlconf = DLConf() + + signal = next( + (signal for signal in user_data.available_signals if signal.id == signal_id), + None, + ) + + if signal is not None: + user_data.building_dlconf.signals.append(signal) + render_window_record_configuration(user_data) + dpg.hide_item(WINDOW_RECORD_CONFIGURATION_ADD_SIGNAL) + + +def show_window_record_configuration(sender, app_data, user_data: AppState) -> None: + if user_data.building_dlconf is None: + user_data.building_dlconf = DLConf() + render_window_record_configuration(user_data) + dpg.show_item(WINDOW_RECORD_CONFIGURATION) diff --git a/src/dynalab/ui/windows.py b/src/dynalab/ui/windows.py index ab158c0..62cdf3b 100644 --- a/src/dynalab/ui/windows.py +++ b/src/dynalab/ui/windows.py @@ -7,15 +7,27 @@ import dpg_gauges as dpgg from dynalab.state import AppState from dynalab.tags import ( + LISTBOX_RECORD_CONFIGURATION, + LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL, MENU_FILE_QUIT, MENU_RECORD_CONFIGURATION, MENU_WINDOW_DATA_INPUT, MENU_WINDOW_RECORD, + TEXT_INPUT_RECORD_CONFIGURATION, WINDOW_DATA_INPUT_GAUGE_GRID, WINDOW_GROUP_DATA_INPUT, WINDOW_GROUP_RECORD, + WINDOW_RECORD_CONFIGURATION, + WINDOW_RECORD_CONFIGURATION_ADD_SIGNAL, ) -from dynalab.ui.routines import switch_window +from dynalab.ui.routines import ( + listbox_record_configuration_add_item, + listbox_record_configuration_add_item_select, + listbox_record_configuration_remove_selected_item, + show_window_record_configuration, + switch_window, +) +from dynalab.i18n import _ LOADING_WINDOW_WIDTH = 600 @@ -94,23 +106,85 @@ def center_loading_content() -> None: dpg.set_item_width("loading_text_left_spacer", text_spacer_width) +def build_record_configuration_add_signal_window(state: AppState) -> None: + with dpg.window( + label="Add Signal", + tag=WINDOW_RECORD_CONFIGURATION_ADD_SIGNAL, + no_collapse=True, + show=False, + min_size=(500, 300), + modal=True, + ): + dpg.add_listbox( + tag=LISTBOX_RECORD_CONFIGURATION_ADD_SINGAL, + num_items=10, + width=480, + ) + with dpg.group(horizontal=True): + dpg.add_button( + label="Select", + callback=listbox_record_configuration_add_item_select, + user_data=state, + ) + dpg.add_button( + label="Refresh", + callback=listbox_record_configuration_add_item, + user_data=state, + ) + + +def build_record_configuration_window(state: AppState) -> None: + with dpg.window( + label="Recording Configuration", + tag=WINDOW_RECORD_CONFIGURATION, + no_collapse=True, + show=False, + min_size=(500, 300), + ): + with dpg.group(): + dpg.add_text("Configuration Name") + dpg.add_input_text(tag=TEXT_INPUT_RECORD_CONFIGURATION) + dpg.add_text("Selected Signals") + dpg.add_listbox( + tag=LISTBOX_RECORD_CONFIGURATION, + num_items=10, + width=480, + ) + with dpg.group(horizontal=True): + dpg.add_button( + label="Add", + callback=listbox_record_configuration_add_item, + user_data=state, + ) + dpg.add_button( + label="Remove", + callback=listbox_record_configuration_remove_selected_item, + user_data=state, + ) + + def build_windows(state: AppState) -> None: with dpg.window(label="DynaLab", tag="main_window", no_collapse=True): with dpg.menu_bar(): - with dpg.menu(label="File"): - dpg.add_menu_item(tag=MENU_FILE_QUIT, label="Quit", enabled=True) - with dpg.menu(label="Record"): - dpg.add_menu_item(tag=MENU_RECORD_CONFIGURATION, label="Configuration") - with dpg.menu(label="Window"): + with dpg.menu(label=_("File")): + dpg.add_menu_item(tag=MENU_FILE_QUIT, label=_("Quit"), enabled=True) + with dpg.menu(label=_("Record")): + dpg.add_menu_item( + tag=MENU_RECORD_CONFIGURATION, + label=_("Configuration"), + callback=show_window_record_configuration, + user_data=state, + ) + with dpg.menu(label=_("Window")): dpg.add_menu_item( tag=MENU_WINDOW_RECORD, - label="Record", + label=_("Record"), callback=switch_window, user_data=state, ) dpg.add_menu_item( tag=MENU_WINDOW_DATA_INPUT, - label="Data Input", + label=_("Data Input"), callback=switch_window, user_data=state, ) @@ -121,9 +195,7 @@ def build_windows(state: AppState) -> None: # with dpg.group(tag=MENU_WINDOW_DATA_INPUT): with dpg.group(tag=WINDOW_GROUP_DATA_INPUT): state.window_map[MENU_WINDOW_DATA_INPUT] = WINDOW_GROUP_DATA_INPUT - with dpgg.gauge_panel( - label="Autobreak narrow panel", autosize_x=True, autosize_y=True - ): + with dpgg.gauge_panel(autosize_x=True, autosize_y=True): with dpgg.gauge_grid( columns="auto", min_column_width=180, @@ -134,3 +206,6 @@ def build_windows(state: AppState) -> None: with dpg.group(tag=WINDOW_GROUP_RECORD, show=False): state.window_map[MENU_WINDOW_RECORD] = WINDOW_GROUP_RECORD dpg.add_text("Some text") + + build_record_configuration_window(state) + build_record_configuration_add_signal_window(state) diff --git a/src/dynalab/utils.py b/src/dynalab/utils.py new file mode 100644 index 0000000..f6defb8 --- /dev/null +++ b/src/dynalab/utils.py @@ -0,0 +1,28 @@ +# Copyright (C) 2026 Hector van der Aa +# Copyright (C) 2026 Association Exergie +# SPDX-License-Identifier: GPL-3.0-or-later +from pathlib import Path +import sys + + +def resource_path(relative_path: str) -> str: + path = Path(relative_path) + candidates: list[Path] = [] + + bundle_dir = getattr(sys, "_MEIPASS", None) + if bundle_dir is not None: + candidates.append(Path(bundle_dir) / path) + + candidates.extend( + ( + Path.cwd() / path, + Path(__file__).resolve().parents[2] / path, + ) + ) + + for candidate in candidates: + if candidate.exists(): + return str(candidate) + + searched = ", ".join(str(candidate) for candidate in candidates) + raise FileNotFoundError(f"Missing asset {relative_path!r}. Searched: {searched}")