From c7cb0378d1c64e6446be3f12b75ad0d6c6804496 Mon Sep 17 00:00:00 2001 From: Abdelkouddous LHACHIMI Date: Fri, 27 Mar 2026 16:08:09 +0100 Subject: [PATCH] Initial template --- .gitignore | 197 ++ config/Endobest_Dashboard_Config.xlsx | Bin 0 -> 79106 bytes config/eb_dashboard_extended_template.xlsx | Bin 0 -> 41198 bytes config/eb_dashboard_monitoring_template.xlsx | Bin 0 -> 120540 bytes eb_dashboard.bat | 4 + eb_dashboard.py | 1539 +++++++++++++ eb_dashboard_check_only-exe.bat | 3 + eb_dashboard_check_only.bat | 4 + eb_dashboard_check_only_debug-exe.bat | 3 + eb_dashboard_check_only_debug.bat | 4 + eb_dashboard_constants.py | 143 ++ eb_dashboard_debug-exe.bat | 3 + eb_dashboard_debug.bat | 4 + eb_dashboard_excel_export.py | 2094 ++++++++++++++++++ eb_dashboard_excel_only-exe.bat | 3 + eb_dashboard_excel_only.bat | 4 + eb_dashboard_quality_checks.py | 1313 +++++++++++ eb_dashboard_utils.py | 220 ++ eb_org_center_mapping.xlsx | Bin 0 -> 18804 bytes 19 files changed, 5538 insertions(+) create mode 100644 .gitignore create mode 100644 config/Endobest_Dashboard_Config.xlsx create mode 100644 config/eb_dashboard_extended_template.xlsx create mode 100644 config/eb_dashboard_monitoring_template.xlsx create mode 100644 eb_dashboard.bat create mode 100644 eb_dashboard.py create mode 100644 eb_dashboard_check_only-exe.bat create mode 100644 eb_dashboard_check_only.bat create mode 100644 eb_dashboard_check_only_debug-exe.bat create mode 100644 eb_dashboard_check_only_debug.bat create mode 100644 eb_dashboard_constants.py create mode 100644 eb_dashboard_debug-exe.bat create mode 100644 eb_dashboard_debug.bat create mode 100644 eb_dashboard_excel_export.py create mode 100644 eb_dashboard_excel_only-exe.bat create mode 100644 eb_dashboard_excel_only.bat create mode 100644 eb_dashboard_quality_checks.py create mode 100644 eb_dashboard_utils.py create mode 100644 eb_org_center_mapping.xlsx diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9424118 --- /dev/null +++ b/.gitignore @@ -0,0 +1,197 @@ +# ---> Python +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# ---> VisualStudioCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# ignore all artefacts +Reports History/ +Script History/ +Endobest Reporting/ +jsons history/ +nul + diff --git a/config/Endobest_Dashboard_Config.xlsx b/config/Endobest_Dashboard_Config.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..c7ec03a260404ea0b27ecdd7b312ac8a05eb33b9 GIT binary patch literal 79106 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 literal 0 HcmV?d00001 diff --git a/config/eb_dashboard_extended_template.xlsx b/config/eb_dashboard_extended_template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..8274c393f8bf590d7d7b0b13b59431f40d0801eb GIT binary patch literal 41198 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 V@E`JQK)8T^Ab_VIv&lcc{eSXDG|T`1 literal 0 HcmV?d00001 diff --git a/config/eb_dashboard_monitoring_template.xlsx b/config/eb_dashboard_monitoring_template.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..83a5a213ef6f8b934d0da0f5b3e16fe7510838e2 GIT binary patch literal 120540 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

pc!fR&)ny+uu9%NJ(DKPX}-#(ASCbr!MMRTmP>r zK6Srj#Wu_DEC@}=Gr~Ca+>Ub@IACgB5rx~j9X^x?4my;4s8ljO6wrMSG)s}h_!gPP z6K0Cnv$HHMesH{0MLPHOhCgba2&oVycs(1PhOtgvuDGO5r|MsFs6^JJJgQ zmIsyv0eW!dvyzk&)bZffyX78{LpUj)wi3i*qjKJyw@o*sY&( zdi^mt=^n08!@0J?hCu`*Q4gq4{eYCFX7#hU2`zowf@l3`xzV23^8_S~pzIznRHRg`Y2`WWAmq->N%;lgYMJY5`DfzHK$PrQ>t1-BCxyqi6y~{)~k>=(S z>kU1_J}d0?L)L4Eu>@zOOzl%4#L`RS3>c&l6vnWH^Qk#ik?%TaS=m2Wh+OIBi&>En-#|@V zCrF<@av}OU{djeV#nMlVI=7%oquK607*pHJ6;1=ysN-Cdm_H9+Z+6k+xd6;_^?T?8 z1!DwyQU7~aF;#H}Z!b^>%7N|}pt$<05_Wg3;(|GPqO>JA^x!va~Q*27C;A^(&O(<)lX3Gfk`m_ zRf&JmrEB@zpc5!M8rkz(QAqI% z#opM^!jSpb`!5UgIRN=KbfF#)z={^Q-S(*tz;EdczX0s)U4Zo6KecDCI*C~busu(K zck!o-{pn!h_+8Xk*VfQL(H_V^82uvxA%yl62v+h^ydwIUX4F)#Zxt&Z*6`; z0R^VS2BQ2?jb9s`pI75~;KdU_R@pCr|5F&obHL|u3r~R6<^KWjiJ5*5_?+8+0$c+| zi~QM8zq`_(ckLg$(sQ=_32?RYKLGyEHGIxLKLM^({Q~?O8~t3y=M>r#f==Bpgl7aA z^Rx3`gxYh^=WNsysA>J*s`ZCG+T1~$Y{^II?e*M2X&!5VIfbau({HJ;R z-!u4gfWIza{{;Ba{T$$FIr|*tuPeAeQ3QeW$=}v<&x`-n=>D^KU;iJ)|7C(dFa1~R j<{BAR_N_FMT_WIDTH}}}/{{total_fmt:<{BAR_TOTAL_FMT_WIDTH}}} " + f"[{{elapsed:<{BAR_TIME_WIDTH}}}<{{remaining:>{BAR_TIME_WIDTH}}}, " + f"{{rate_fmt:>{BAR_RATE_WIDTH}}}]{{postfix}}") + + +# ============================================================================ +# BLOCK 2: DECORATORS & RESILIENCE +# ============================================================================ + +def new_token(): + """Refresh access token using the refresh token""" + global access_token, refresh_token + with _token_refresh_lock: + for attempt in range(ERROR_MAX_RETRY): + try: + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(API_AUTH_REFRESH_TOKEN_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, + json={"refresh_token": refresh_token}, timeout=20) + response.raise_for_status() + access_token = response.json()["access_token"] + refresh_token = response.json()["refresh_token"] + return + except httpx.RequestError as exc: + logging.warning(f"Refresh Token Error (Attempt {attempt + 1}) : {exc}") + clear_httpx_client() + except httpx.HTTPStatusError as exc: + logging.warning( + f"Refresh Token Error (Attempt {attempt + 1}) : {exc.response.status_code} for Url {exc.request.url}") + clear_httpx_client() + finally: + if attempt < ERROR_MAX_RETRY - 1: + sleep(WAIT_BEFORE_RETRY) + # Refresh token exhausted — attempt full re-login with stored credentials + logging.warning("Refresh token exhausted. Attempting re-login with stored credentials.") + _do_login(_stored_username, _stored_password) + logging.info("Re-login successful. New tokens acquired.") + + +def api_call_with_retry(func): + """Decorator for API calls with automatic retry and token refresh on 401 errors""" + @functools.wraps(func) + def wrapper(*args, **kwargs): + func_name = func.__name__ + total_attempts = 0 + batch_count = 1 + + while True: + for attempt in range(ERROR_MAX_RETRY): + total_attempts += 1 + try: + return func(*args, **kwargs) + except (httpx.RequestError, httpx.HTTPStatusError) as exc: + logging.warning(f"Error in {func_name} (Attempt {total_attempts}): {exc}") + + # Refresh the thread-local client if an error occurs + # to avoid potential pool corruption or stale connections + clear_httpx_client() + + if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 401: + logging.info(f"Token expired for {func_name}. Refreshing token.") + try: + new_token() + except (httpx.RequestError, httpx.HTTPStatusError) as token_exc: + logging.warning(f"Token refresh/re-login failed for {func_name}: {token_exc}") + + if attempt < ERROR_MAX_RETRY - 1: + sleep(WAIT_BEFORE_RETRY) + else: + # Max retries reached for this batch + if batch_count < MAX_BATCHS_OF_RETRIES: + logging.warning(f"Batch {batch_count}/{MAX_BATCHS_OF_RETRIES} failed for {func_name}. " + f"Waiting {WAIT_BEFORE_NEW_BATCH_OF_RETRIES}s before automatic retry batch.") + batch_count += 1 + sleep(WAIT_BEFORE_NEW_BATCH_OF_RETRIES) + break # Exit for loop to restart batch in while True + else: + # All automatic batches exhausted — apply on_retry_exhausted policy + with _user_interaction_lock: + if on_retry_exhausted == "ignore": + ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"}) + logging.warning(f"[AUTO-IGNORE] Skipping {func_name} for Patient {ctx['id']} ({ctx['pseudo']}). Error: {exc}") + return None + + elif on_retry_exhausted == "abort": + logging.critical(f"[AUTO-ABORT] Stopping script after persistent error in {func_name}. Error: {exc}") + raise httpx.RequestError(message=f"Persistent error in {func_name} (auto-aborted)") + + else: # "ask" — display error then interactive prompt + console.print(f"\n[bold red]Persistent error in {func_name} after {batch_count} batches ({total_attempts} attempts).[/bold red]") + console.print(f"[red]Exception: {exc}[/red]") + + choice = questionary.select( + f"What would you like to do for {func_name}?", + choices=[ + "Retry (try another batch of retries)", + "Ignore (return None and continue)", + "Stop script (critical error)" + ] + ).ask() + + if choice == "Retry (try another batch of retries)": + logging.info(f"User chose to retry {func_name}. Restarting batch sequence.") + batch_count = 1 # Reset batch counter for the next interactive round + break # Exit for loop to restart batch in while True + elif choice == "Ignore (return None and continue)": + ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"}) + logging.warning(f"[IGNORE] User opted to skip {func_name} for Patient {ctx['id']} ({ctx['pseudo']}). Error: {exc}") + return None + else: + logging.critical(f"User chose to stop script after persistent error in {func_name}.") + raise httpx.RequestError(message=f"Persistent error in {func_name} (stopped by user)") + + return wrapper + + +# ============================================================================ +# BLOCK 3: AUTHENTICATION +# ============================================================================ + +def _do_login(username, password): + """Performs the two-step authentication (IAM → RC) with the given credentials. + Updates global access_token and refresh_token on success. + Raises httpx.RequestError or httpx.HTTPStatusError on failure. + Must NOT acquire _token_refresh_lock (caller's responsibility). + """ + global access_token, refresh_token + + client = get_httpx_client() + client.base_url = IAM_URL + response = client.post(API_AUTH_LOGIN_ENDPOINT, + json={"username": username, "password": password}, + timeout=20) + response.raise_for_status() + master_token = response.json()["access_token"] + user_id = response.json()["userId"] + + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(API_AUTH_CONFIG_TOKEN_ENDPOINT, + headers={"Authorization": f"Bearer {master_token}"}, + json={"userId": user_id, "clientId": RC_APP_ID, + "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36"}, + timeout=20) + response.raise_for_status() + access_token = response.json()["access_token"] + refresh_token = response.json()["refresh_token"] + + +def login(): + global _stored_username, _stored_password + + user_name = (questionary.text("login :", default=DEFAULT_USER_NAME).ask()) + password = (questionary.password("password :", default=DEFAULT_PASSWORD).ask()) + if not (user_name and password): + return "Exit" + + try: + _do_login(user_name, password) + except httpx.RequestError as exc: + print(f"Login Error : {exc}") + logging.warning(f"Login Error : {exc}") + return "Error" + except httpx.HTTPStatusError as exc: + print(f"Login Error : {exc.response.status_code} for Url {exc.request.url}") + logging.warning(f"Login Error : {exc.response.status_code} for Url {exc.request.url}") + return "Error" + + _stored_username = user_name + _stored_password = password + print() + print("Login Success") + return "Success" + + +# ============================================================================ +# BLOCK 3B: FILE UTILITIES +# ============================================================================ + +def ask_on_retry_exhausted(): + """Asks the user what to do when all API retry batches are exhausted.""" + global on_retry_exhausted + choice = questionary.select( + "On retry exhausted :", + choices=[ + "Ask (interactive prompt)", + "Ignore (return None and continue)", + "Abort (stop script)" + ] + ).ask() + if choice is None or choice == "Ask (interactive prompt)": + on_retry_exhausted = "ask" + elif choice == "Ignore (return None and continue)": + on_retry_exhausted = "ignore" + else: + on_retry_exhausted = "abort" + + +def ask_fetch_six_month_visit(): + """Asks the user whether to fetch 6-month visit data (slow API call, ~5s per patient).""" + global fetch_six_month_visit + choice = questionary.select( + "Fetch 6-month visit progress data? (slow, ~5s per patient) :", + choices=[ + "No (skip, faster execution)", + "Yes (fetch 6-month visit data)" + ] + ).ask() + fetch_six_month_visit = (choice == "Yes (fetch 6-month visit data)") + + +def wait_for_scheduled_launch(): + """Asks the user when to start the processing and waits if needed. + Options: Immediately / In X minutes / At HH:MM + """ + choice = questionary.select( + "When to start processing ?", + choices=["Immediately", "In X minutes", "At HH:MM"] + ).ask() + + if choice is None or choice == "Immediately": + return + + if choice == "In X minutes": + minutes_str = questionary.text( + "Number of minutes :", + validate=lambda x: x.isdigit() and int(x) > 0 + ).ask() + if not minutes_str: + return + target_time = datetime.now() + timedelta(minutes=int(minutes_str)) + + else: # "At HH:MM" + time_str = questionary.text( + "Start time (HH:MM) :", + validate=lambda x: bool(re.match(r'^\d{2}:\d{2}$', x)) and + 0 <= int(x.split(':')[0]) <= 23 and + 0 <= int(x.split(':')[1]) <= 59 + ).ask() + if not time_str: + return + now = datetime.now() + h, m = int(time_str.split(':')[0]), int(time_str.split(':')[1]) + target_time = now.replace(hour=h, minute=m, second=0, microsecond=0) + if target_time <= now: + console.print("[yellow]⚠ Specified time is already past. Starting immediately.[/yellow]") + return + + print() + try: + while True: + remaining = target_time - datetime.now() + if remaining.total_seconds() <= 0: + break + total_secs = int(remaining.total_seconds()) + h = total_secs // 3600 + m = (total_secs % 3600) // 60 + s = total_secs % 60 + target_str = target_time.strftime('%H:%M:%S') + print(f"\r Starting in {h:02d}:{m:02d}:{s:02d}... (at {target_str}) — Ctrl+C to cancel ", + end="", flush=True) + sleep(1) + # Flush keyboard buffer to prevent stray keystrokes from polluting subsequent prompts + while msvcrt.kbhit(): + msvcrt.getwch() + print() + console.print("[green]✓ Starting processing.[/green]") + except KeyboardInterrupt: + print() + console.print("[bold red]Launch cancelled by user.[/bold red]") + raise SystemExit(0) + + +def load_json_file(filename): + """ + Load a JSON file from disk. + + Args: + filename: Path to JSON file + + Returns: + Parsed JSON data or None if file not found or error occurred + """ + if os.path.exists(filename): + try: + with open(filename, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logging.warning(f"Could not load JSON file '{filename}': {e}") + console.print(f"[yellow]⚠ Warning: Could not load JSON file '{filename}': {e}[/yellow]") + return None + + +# ============================================================================ +# BLOCK 4: INCLUSIONS MAPPING CONFIGURATION +# ============================================================================ + +def load_inclusions_mapping_config(): + """Loads and validates the inclusions mapping configuration from the Excel file.""" + global inclusions_mapping_config + config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) + + try: + # Load with data_only=True to read calculated values instead of formulas + # (e.g., if mapping columns use formulas like =+1, we get 1, not the formula text) + workbook = openpyxl.load_workbook(config_path, data_only=True) + except FileNotFoundError: + error_msg = f"Error: Configuration file not found at: {config_path}" + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + if INCLUSIONS_MAPPING_TABLE_NAME not in workbook.sheetnames: + error_msg = f"Error: Sheet ''{INCLUSIONS_MAPPING_TABLE_NAME}'' not found in the configuration file." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + sheet = workbook[INCLUSIONS_MAPPING_TABLE_NAME] + headers = [cell.value for cell in sheet[1]] + + temp_config = [] + + for row_index, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): + field_config = dict(zip(headers, row)) + + # --- Validation and Parsing --- + if field_config.get("source_name") == "Not Specified": + continue + + field_name = field_config.get("field_name") + if not field_name or not isinstance(field_name, str): + error_msg = f"Error in config file, row {row_index}: 'field_name' is mandatory." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + field_config["field_name"] = re.sub(r'\s*\([^)]*\)$', '', field_name).strip() + + # Parse source_id prefix (q_id=, q_name=, q_category=, record, inclusion, request) + source_id_raw = field_config.get("source_id", "") + if source_id_raw and isinstance(source_id_raw, str): + if source_id_raw.startswith("q_id="): + field_config["source_type"] = "q_id" + field_config["source_value"] = source_id_raw[5:] + elif source_id_raw.startswith("q_name="): + field_config["source_type"] = "q_name" + field_config["source_value"] = source_id_raw[7:] + elif source_id_raw.startswith("q_category="): + field_config["source_type"] = "q_category" + field_config["source_value"] = source_id_raw[11:] + elif source_id_raw == "record": + field_config["source_type"] = "record" + field_config["source_value"] = None + elif source_id_raw == "inclusion": + field_config["source_type"] = "inclusion" + field_config["source_value"] = None + elif source_id_raw == "request": + field_config["source_type"] = "request" + field_config["source_value"] = None + elif source_id_raw == "6_month_visit": + field_config["source_type"] = "6_month_visit" + field_config["source_value"] = None + else: + field_config["source_type"] = None + field_config["source_value"] = source_id_raw + else: + field_config["source_type"] = None + field_config["source_value"] = None + + for json_field in ["field_path", "field_condition", "true_if_any", "value_labels"]: + value = field_config.get(json_field) + if value: + if not isinstance(value, str): + error_msg = f"Error in config file, row {row_index}, field '{json_field}': Invalid value, must be a JSON string." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + try: + field_config[json_field] = json.loads(value) + except json.JSONDecodeError: + error_msg = f"Error in config file, row {row_index}, field '{json_field}': Invalid JSON format." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + else: + field_config[json_field] = None + + if not field_config.get("field_path"): + error_msg = f"Error in config file, row {row_index}: 'field_path' is mandatory when a field is specified." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + temp_config.append(field_config) + + inclusions_mapping_config = temp_config + console.print(f"Loaded {len(inclusions_mapping_config)} fields from inclusions mapping configuration.", style="green") + + +def load_organizations_mapping_config(): + """Loads and validates the organizations mapping configuration from the Excel file.""" + global organizations_mapping_config + config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) + + try: + # Load with data_only=True to read calculated values instead of formulas + # (e.g., if mapping columns use formulas like =+1, we get 1, not the formula text) + workbook = openpyxl.load_workbook(config_path, data_only=True) + except FileNotFoundError: + error_msg = f"Error: Configuration file not found at: {config_path}" + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + if ORGANIZATIONS_MAPPING_TABLE_NAME not in workbook.sheetnames: + # Organizations mapping is optional, so return empty config if sheet not found + logging.info(f"Sheet '{ORGANIZATIONS_MAPPING_TABLE_NAME}' not found in configuration file. Organizations mapping is optional.") + organizations_mapping_config = [] + return + + sheet = workbook[ORGANIZATIONS_MAPPING_TABLE_NAME] + headers = [cell.value for cell in sheet[1]] + + # Filter out None headers (empty columns) + headers_filtered = [h for h in headers if h is not None] + + mapping_config = [] + try: + for row in sheet.iter_rows(min_row=2, values_only=True): + if all(cell is None for cell in row): + break + + # Trim row to match filtered headers length + row_filtered = row[:len(headers_filtered)] + config_dict = dict(zip(headers_filtered, row_filtered)) + mapping_config.append(config_dict) + except Exception as e: + error_msg = f"Error parsing organizations mapping: {e}" + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + finally: + workbook.close() + + organizations_mapping_config = mapping_config + if mapping_config: + console.print(f"Loaded {len(organizations_mapping_config)} organizations from organizations mapping configuration.", style="green") + else: + console.print("No organizations mapping found (this is optional).", style="yellow") + + +# ============================================================================ +# BLOCK 5: DATA SEARCH & EXTRACTION +# ============================================================================ + +def _find_questionnaire_by_id(qcm_dict, qcm_id): + """Finds a questionnaire by ID (direct dictionary lookup).""" + if not isinstance(qcm_dict, dict): + return None + qcm_data = qcm_dict.get(qcm_id) + return qcm_data.get("answers") if qcm_data else None + + +def _find_questionnaire_by_name(qcm_dict, name): + """Finds a questionnaire by name (sequential search, returns first match).""" + if not isinstance(qcm_dict, dict): + return None + for qcm in qcm_dict.values(): + if get_nested_value(qcm, ["questionnaire", "name"]) == name: + return qcm.get("answers") + return None + + +def _find_questionnaire_by_category(qcm_dict, category): + """Finds a questionnaire by category (sequential search, returns first match).""" + if not isinstance(qcm_dict, dict): + return None + for qcm in qcm_dict.values(): + if get_nested_value(qcm, ["questionnaire", "category"]) == category: + return qcm.get("answers") + return None + + +def _get_field_value_from_questionnaire(all_questionnaires, field_config): + """ + Gets the raw value for a field from questionnaires (without post-processing). + """ + # Find questionnaire based on type + source_type = field_config.get("source_type") + source_value = field_config.get("source_value") + + if source_type == "q_id": + answers = _find_questionnaire_by_id(all_questionnaires, source_value) + elif source_type == "q_name": + answers = _find_questionnaire_by_name(all_questionnaires, source_value) + elif source_type == "q_category": + answers = _find_questionnaire_by_category(all_questionnaires, source_value) + else: + answers = None + + return get_nested_value(answers, field_config["field_path"], default="undefined") + + +def get_value_from_inclusion(inclusion_dict, key): + """Helper to find a key in the new nested inclusion structure.""" + for group in inclusion_dict.values(): + if isinstance(group, dict) and key in group: + return group[key] + return None + + +# ============================================================================ +# BLOCK 6: CUSTOM FUNCTIONS & FIELD PROCESSING +# ============================================================================ + +def _execute_custom_function(function_name, args, output_inclusion): + """Executes a custom function for a calculated field.""" + if function_name == "search_in_fields_using_regex": + if not args or len(args) < 2: + return "$$$$ Argument Error: search_in_fields_using_regex requires at least 2 arguments" + + regex_pattern = args[0] + field_names = args[1:] + + field_values = [] + all_undefined = True + + for field_name in field_names: + value = get_value_from_inclusion(output_inclusion, field_name) + field_values.append(value) + if value is not None and value != "undefined": + all_undefined = False + + if all_undefined: + return "undefined" + + try: + for value in field_values: + # We only try to match on string values. + if isinstance(value, str) and re.search(regex_pattern, value, re.IGNORECASE): + return True + except re.error as e: + return f"$$$$ Regex Error: {e}" + + return False + + elif function_name == "extract_parentheses_content": + if not args or len(args) != 1: + return "$$$$ Argument Error: extract_parentheses_content requires 1 argument" + + field_name = args[0] + value = get_value_from_inclusion(output_inclusion, field_name) + + if value is None or value == "undefined": + return "undefined" + + match = re.search(r'\((.*?)\)', str(value)) + return match.group(1) if match else "undefined" + + elif function_name == "append_terminated_suffix": + if not args or len(args) != 2: + return "$$$$ Argument Error: append_terminated_suffix requires 2 arguments" + + status = get_value_from_inclusion(output_inclusion, args[0]) + is_terminated = get_value_from_inclusion(output_inclusion, args[1]) + + if status is None or status == "undefined": + return "undefined" + + if not isinstance(is_terminated, bool) or not is_terminated: + return status + + return f"{status} - AP" + + elif function_name == "if_then_else": + # Unified conditional function + # Syntax: ["operator", arg1, arg2_optional, result_if_true, result_if_false] + # Operators: "is_true", "is_false", "all_true", "is_defined", "is_undefined", "all_defined", "==", "!=" + + if not args or len(args) < 4: + return "$$$$ Argument Error: if_then_else requires at least 4 arguments" + + operator = args[0] + + # Helper function to resolve value (literal or field name) + def resolve_value(arg): + # If boolean literal + if isinstance(arg, bool): + return arg + # If numeric literal + if isinstance(arg, (int, float)): + return arg + # If string literal (starts with $) + if isinstance(arg, str) and arg.startswith("$"): + return arg[1:] # Remove the $ prefix + # Otherwise, treat as field name + return get_value_from_inclusion(output_inclusion, arg) + + # Determine condition based on operator + if operator == "is_true": + if len(args) != 4: + return "$$$$ Argument Error: is_true requires 4 arguments" + value = resolve_value(args[1]) + if value is None or value == "undefined": + return "undefined" + condition = (value is True) + result_if_true = resolve_value(args[2]) + result_if_false = resolve_value(args[3]) + + elif operator == "is_false": + if len(args) != 4: + return "$$$$ Argument Error: is_false requires 4 arguments" + value = resolve_value(args[1]) + if value is None or value == "undefined": + return "undefined" + condition = (value is False) + result_if_true = resolve_value(args[2]) + result_if_false = resolve_value(args[3]) + + elif operator == "all_true": + if len(args) != 4: + return "$$$$ Argument Error: all_true requires 4 arguments" + fields_arg = args[1] + if not isinstance(fields_arg, list): + return "$$$$ Argument Error: all_true requires arg1 to be a list of field names" + + conditions = [] + for field_name in fields_arg: + field_value = get_value_from_inclusion(output_inclusion, field_name) + if field_value is None or field_value == "undefined": + return "undefined" + conditions.append(field_value) + + condition = all(conditions) + result_if_true = resolve_value(args[2]) + result_if_false = resolve_value(args[3]) + + elif operator == "is_defined": + if len(args) != 4: + return "$$$$ Argument Error: is_defined requires 4 arguments" + value = resolve_value(args[1]) + condition = (value is not None and value != "undefined") + result_if_true = resolve_value(args[2]) + result_if_false = resolve_value(args[3]) + + elif operator == "is_undefined": + if len(args) != 4: + return "$$$$ Argument Error: is_undefined requires 4 arguments" + value = resolve_value(args[1]) + condition = (value is None or value == "undefined") + result_if_true = resolve_value(args[2]) + result_if_false = resolve_value(args[3]) + + elif operator == "all_defined": + if len(args) != 4: + return "$$$$ Argument Error: all_defined requires 4 arguments" + fields_arg = args[1] + if not isinstance(fields_arg, list): + return "$$$$ Argument Error: all_defined requires arg1 to be a list of field names" + + for field_name in fields_arg: + field_value = get_value_from_inclusion(output_inclusion, field_name) + if field_value is None or field_value == "undefined": + condition = False + break + else: + condition = True + + result_if_true = resolve_value(args[2]) + result_if_false = resolve_value(args[3]) + + elif operator == "==": + if len(args) != 5: + return "$$$$ Argument Error: == requires 5 arguments" + value1 = resolve_value(args[1]) + value2 = resolve_value(args[2]) + + if value1 is None or value1 == "undefined" or value2 is None or value2 == "undefined": + return "undefined" + + condition = (value1 == value2) + result_if_true = resolve_value(args[3]) + result_if_false = resolve_value(args[4]) + + elif operator == "!=": + if len(args) != 5: + return "$$$$ Argument Error: != requires 5 arguments" + value1 = resolve_value(args[1]) + value2 = resolve_value(args[2]) + + if value1 is None or value1 == "undefined" or value2 is None or value2 == "undefined": + return "undefined" + + condition = (value1 != value2) + result_if_true = resolve_value(args[3]) + result_if_false = resolve_value(args[4]) + + else: + return f"$$$$ Unknown Operator: {operator}" + + return result_if_true if condition else result_if_false + + return f"$$$$ Unknown Custom Function: {function_name}" + + +def process_inclusions_mapping(output_inclusion, inclusion_data, record_data, request_data, all_questionnaires, six_month_visit_data): + """Processes and adds the inclusions mapping fields to the inclusion dictionary.""" + for field in inclusions_mapping_config: + field_name = field["field_name"] + field_group = field.get("field_group", "Extended_Fields") # Default group if not specified + final_value = "undefined" # Default value + + # Check condition + condition_field_name = field.get("field_condition") + if condition_field_name: + condition_value = get_value_from_inclusion(output_inclusion, condition_field_name) + + if condition_value is None or condition_value == "undefined": + final_value = "undefined" + elif not isinstance(condition_value, bool): + final_value = "$$$$ Condition Field Error" + elif not condition_value: + final_value = "N/A" + + # If condition allows, process the field + if final_value == "undefined": + source_name = field.get("source_name") + source_type = field.get("source_type") + field_path = field.get("field_path") + + # Get raw value from appropriate source + if source_name == "Calculated": + function_name = field.get("source_id") + args = field_path + final_value = _execute_custom_function(function_name, args, output_inclusion) + elif source_type in ["q_id", "q_name", "q_category"]: + final_value = _get_field_value_from_questionnaire(all_questionnaires, field) + elif source_type == "record": + final_value = get_nested_value(record_data, field_path, default="undefined") + elif source_type == "inclusion": + final_value = get_nested_value(inclusion_data, field_path, default="undefined") + elif source_type == "request": + final_value = get_nested_value(request_data, field_path, default="undefined") + elif source_type == "6_month_visit": + final_value = get_nested_value(six_month_visit_data, field_path, default="undefined") + else: + final_value = f"$$$$ Unknown Source Type: {source_type}" + + # If the source data itself is missing (e.g., 6-month visit not created), log a warning but continue + if final_value == "$$$$ No Data": + patient_id = inclusion_data.get("id", "Unknown") + pseudo = inclusion_data.get("pseudo", "Unknown") + logging.warning(f"No '{source_type}' data source found for Patient {patient_id} / {pseudo} (Field: {field_name})") + final_value = "undefined" + + # Post-processing: Apply true_if_any and value_labels transformations (for all sources) + if final_value not in ["undefined", "$$$$ No Data"]: + # Check if any value matches + check_values = field.get("true_if_any") + if check_values: + raw_value_set = set(final_value if isinstance(final_value, list) else [final_value]) + check_values_set = set(check_values if isinstance(check_values, list) else [check_values]) + final_value = not raw_value_set.isdisjoint(check_values_set) + + # Map value to label + value_labels = field.get("value_labels") + if value_labels and final_value not in ["$$$$ Format Error : Array expected"]: + found = False + for label_map in value_labels: + if label_map.get("value") == final_value: + final_value = get_nested_value(label_map, ["text", "fr"], default=f"$$$$ Value Error : {final_value}") + found = True + break + if not found: + final_value = f"$$$$ Value Error : {final_value}" + + # Post-processing: If the value is a list (e.g. from a wildcard), join it with a pipe. + if isinstance(final_value, list): + final_value = "|".join(map(str, final_value)) + + # Post-processing: Format score dictionaries + if isinstance(final_value, dict) and 'total' in final_value and 'max' in final_value: + final_value = f"{final_value['total']}/{final_value['max']}" + + # Post-processing: Apply field template + field_template = field.get("field_template") + if field_template and final_value not in ["undefined", "N/A"] and isinstance(final_value, (str, int, float, bool)): + final_value = field_template.replace("$value", str(final_value)) + + # Ensure the group sub-dictionary exists + if field_group not in output_inclusion: + output_inclusion[field_group] = {} + output_inclusion[field_group][field_name] = final_value + + +# ============================================================================ +# BLOCK 7: BUSINESS API CALLS +# ============================================================================ + +@api_call_with_retry +def get_all_organizations(): + start_time = perf_counter() + with console.status("[bold green]Getting Organizations...", spinner="dots"): + client = get_httpx_client() + client.base_url = RC_URL + response = client.get(API_RC_GET_ALL_ORGANIZATIONS_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, timeout=1200) + response.raise_for_status() + duration = perf_counter() - start_time + console.print(f"Organizations loaded. ({duration:.2f}s)", style="green") + return response.json() + + +@api_call_with_retry +def _get_organization_statistics(organization_id): + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(API_RC_INCLUSION_STATISTICS_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, + json={"protocolId": RC_ENDOBEST_PROTOCOL_ID, "center": organization_id, + "excludedCenters": RC_ENDOBEST_EXCLUDED_CENTERS}, + timeout=API_TIMEOUT) + response.raise_for_status() + return response.json()["statistic"] + + +def get_organization_counters(organization): + organization_id = organization['id'] + stats = _get_organization_statistics(organization_id) + + organization["patients_count"] = stats.get("totalInclusions", 0) + organization["preincluded_count"] = stats.get("preIncluded", 0) + organization["included_count"] = stats.get("included", 0) + organization["prematurely_terminated_count"] = stats.get("prematurelyTerminated", 0) + return organization + + +@api_call_with_retry +def get_organization_inclusions(organization_id, limit, page): + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(f"{API_RC_SEARCH_INCLUSIONS_ENDPOINT}?limit={limit}&page={page}", + headers={"Authorization": f"Bearer {access_token}"}, + json={"protocolId": RC_ENDOBEST_PROTOCOL_ID, "center": organization_id, + "keywords": ""}, timeout=API_TIMEOUT) + response.raise_for_status() + return response.json()["data"] + + +@api_call_with_retry +def get_record_by_patient_id(patient_id, organization_id): + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(API_RC_GET_RECORD_BY_PATIENT_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, + json={"center": organization_id, "patientId": patient_id, + "mode": "exchange", "state": "ongoing", "includeEndoParcour": False, + "sourceClient": "pro_prm"}, + timeout=API_TIMEOUT) + response.raise_for_status() + return response.json() + + +@api_call_with_retry +def get_request_by_tube_id(tube_id): + client = get_httpx_client() + client.base_url = GDD_URL + response = client.get(f"{API_GDD_GET_REQUEST_BY_TUBE_ID_ENDPOINT}/{tube_id}?isAdmin=true&organization=undefined", + headers={"Authorization": f"Bearer {access_token}"}, timeout=API_TIMEOUT) + response.raise_for_status() + return response.json() + + +@api_call_with_retry +def search_visit_by_pseudo_and_order(pseudo, order): + """Searches for a visit by patient pseudo and visit order.""" + client = get_httpx_client() + client.base_url = RC_URL + response = client.post(API_RC_SEARCH_VISITS_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, + json={"visitOrder": order, "keywords": pseudo}, + timeout=API_TIMEOUT) + response.raise_for_status() + resp_json = response.json() + if isinstance(resp_json, dict): + data = resp_json.get("data") + if isinstance(data, list) and len(data) > 0: + return data[0] + return None + + +@api_call_with_retry +def get_all_questionnaires_by_patient(patient_id, record_data): + """Fetches all questionnaires for a patient with a single API call.""" + client = get_httpx_client() + client.base_url = RC_URL + + payload = { + "context": "clinic_research", + "subject": patient_id + } + + # Extract blockedQcmVersions from record (same logic as get_questionnaire_answers) + if record_data is None: + all_blocked_versions = [] + else: + all_blocked_versions = get_nested_value(record_data, path=["record", "protocol_inclusions", 0, "blockedQcmVersions"], + default=[]) + # Ensure it's a list even if get_nested_value returns "$$$$ No Data" + if all_blocked_versions == "$$$$ No Data": + all_blocked_versions = [] + + if all_blocked_versions: + payload["blockedQcmVersions"] = all_blocked_versions + + response = client.post(API_RC_GET_SURVEYS_ENDPOINT, + headers={"Authorization": f"Bearer {access_token}"}, + json=payload, + timeout=API_TIMEOUT) + response.raise_for_status() + response_data = response.json() + + # Build dictionary with questionnaire metadata for searching + results = {} + for item in response_data: + q_id = get_nested_value(item, path=["questionnaire", "id"]) + q_name = get_nested_value(item, path=["questionnaire", "name"]) + q_category = get_nested_value(item, path=["questionnaire", "category"]) + answers = get_nested_value(item, path=["answers"], default={}) + if q_id: + results[q_id] = { + "questionnaire": { + "id": q_id, + "name": q_name, + "category": q_category + }, + "answers": answers + } + return results + + +# ============================================================================ +# BLOCK 7b: ORGANIZATION CENTER MAPPING +# ============================================================================ + +def load_organization_center_mapping(): + """ + Loads organization ↔ center mapping from Excel file in script directory. + + Returns: + dict: {organization_name_normalized: center_name} or {} if error/skip + """ + mapping_file = ORG_CENTER_MAPPING_FILE_NAME + + if not os.path.exists(mapping_file): + console.print(f"[yellow]⚠ Mapping file not found at: {mapping_file}. Skipping center mapping.[/yellow]") + return {} + + try: + workbook = openpyxl.load_workbook(mapping_file) + except Exception as e: + console.print(f"[yellow]⚠ Error loading mapping file: {e}. Skipping center mapping.[/yellow]") + logging.warning(f"Error loading mapping file: {e}") + return {} + + if ORG_CENTER_MAPPING_TABLE_NAME not in workbook.sheetnames: + console.print(f"[yellow]⚠ Sheet '{ORG_CENTER_MAPPING_TABLE_NAME}' not found in mapping file. Skipping center mapping.[/yellow]") + return {} + + sheet = workbook[ORG_CENTER_MAPPING_TABLE_NAME] + headers = [cell.value for cell in sheet[1]] + + # Validate required columns + if "Organization_Name" not in headers or "Center_Name" not in headers: + console.print(f"[yellow]⚠ Required columns 'Organization_Name' or 'Center_Name' not found in mapping file. Skipping center mapping.[/yellow]") + return {} + + # Load mapping rows + mapping_rows = [] + try: + for row in sheet.iter_rows(min_row=2, values_only=True): + # Skip empty rows + if all(cell is None for cell in row): + continue + + row_dict = dict(zip(headers, row)) + org_name = row_dict.get("Organization_Name") + center_name = row_dict.get("Center_Name") + + if org_name and center_name: + mapping_rows.append({ + "Organization_Name": org_name, + "Center_Name": center_name + }) + except Exception as e: + console.print(f"[yellow]⚠ Error reading mapping file rows: {e}. Skipping center mapping.[/yellow]") + logging.warning(f"Error reading mapping file rows: {e}") + return {} + + # === VALIDATE: Check for duplicates on NORMALIZED versions === + org_names_normalized = {} # {normalized: original} + center_names_normalized = {} # {normalized: original} + + for row in mapping_rows: + org_name_raw = row["Organization_Name"] + center_name_raw = row["Center_Name"] + + # Normalize + org_normalized = org_name_raw.strip().lower() if isinstance(org_name_raw, str) else str(org_name_raw).strip().lower() + center_normalized = center_name_raw.strip().lower() if isinstance(center_name_raw, str) else str(center_name_raw).strip().lower() + + # Check for duplicates + if org_normalized in org_names_normalized: + console.print(f"[yellow]⚠ Duplicate found in Organization_Name: '{org_name_raw}'. Skipping center mapping.[/yellow]") + logging.warning(f"Duplicate in Organization_Name: '{org_name_raw}'") + return {} + + if center_normalized in center_names_normalized: + console.print(f"[yellow]⚠ Duplicate found in Center_Name: '{center_name_raw}'. Skipping center mapping.[/yellow]") + logging.warning(f"Duplicate in Center_Name: '{center_name_raw}'") + return {} + + # Store normalized version + org_names_normalized[org_normalized] = org_name_raw + center_names_normalized[center_normalized] = center_name_raw + + # === BUILD MAPPING DICT === + mapping_dict = {} + for row in mapping_rows: + org_name_raw = row["Organization_Name"] + center_name_raw = row["Center_Name"] + + # Normalize key, keep center_name clean (strip but not lower) + org_normalized = org_name_raw.strip().lower() if isinstance(org_name_raw, str) else str(org_name_raw).strip().lower() + center_clean = center_name_raw.strip() if isinstance(center_name_raw, str) else str(center_name_raw).strip() + + mapping_dict[org_normalized] = center_clean + + return mapping_dict + + +def apply_center_mapping(organizations_list, mapping_dict): + """ + Applies organization → center mapping to organizations list. + Adds 'Center_Name' field to each organization only if mapping succeeded. + + Args: + organizations_list: List of organization dicts + mapping_dict: {organization_name_normalized: center_name} + """ + if not mapping_dict: + # Mapping dict is empty due to error → skip mapping + return + + unmapped = [] + + for org in organizations_list: + org_name = org.get("name", "") + org_name_normalized = org_name.strip().lower() + + # Try to find match in mapping dict + if org_name_normalized in mapping_dict: + org["Center_Name"] = mapping_dict[org_name_normalized] + else: + # Fallback to organization name + org["Center_Name"] = org_name + unmapped.append(org_name) + + # Display results + if not unmapped: + console.print(f"[green]✓ All {len(organizations_list)} organizations mapped successfully.[/green]") + else: + console.print(f"[yellow]⚠ {len(unmapped)} organization(s) not mapped:[/yellow]") + for org_name in sorted(unmapped): + console.print(f"[yellow] - {org_name}[/yellow]") + + +# ============================================================================ +# BLOCK 8: PROCESSING ORCHESTRATION +# ============================================================================ + +def _process_inclusion_data(inclusion, organization): + """Processes a single inclusion record and returns a dictionary.""" + organization_id = organization["id"] + patient_id = get_nested_value(inclusion, path=["id"]) + pseudo = get_nested_value(inclusion, path=["pseudo"], default="Unknown") + + # Set thread-local context for detailed error logging in decorators + ctx = {"id": patient_id, "pseudo": pseudo} + thread_local_storage.current_patient_context = ctx + + # Initialize empty output structure + output_inclusion = {} + + # --- Prepare all data sources --- + # 1. Launch Visit Search asynchronously (it's slow, ~5s) — only if enabled by user + # We use run_with_context to pass the patient identity to the new thread + if fetch_six_month_visit: + visit_future = subtasks_thread_pool.submit(run_with_context, search_visit_by_pseudo_and_order, ctx, pseudo, 2) + else: + visit_future = None + + # 2. Prepare inclusion_data: enrich inclusion with organization info + inclusion_data = dict(inclusion) + inclusion_data["organization_id"] = organization_id + inclusion_data["organization_name"] = organization["name"] + if "Center_Name" in organization: + inclusion_data["center_name"] = organization["Center_Name"] + + # 3. Prepare record_data (sequential as it's often needed for questionnaires) + record_data = get_record_by_patient_id(patient_id, organization_id) + + # 4. Get tube_id for request and launch in parallel with questionnaires + tube_id = get_nested_value(record_data, path=["record", "clinicResearchData", 0, "requestMetaData", "tubeId"], default="undefined") + request_future = subtasks_thread_pool.submit(run_with_context, get_request_by_tube_id, ctx, tube_id) + all_questionnaires = get_all_questionnaires_by_patient(patient_id, record_data) + + # --- Synchronize all asynchronous tasks --- + try: + request_data = request_future.result() + except Exception as e: + logging.error(f"Error fetching request data for patient {patient_id}: {e}") + request_data = None + + try: + six_month_visit_data = visit_future.result() if visit_future is not None else {} + except Exception as e: + logging.error(f"Error searching 6-month visit for patient {pseudo}: {e}") + six_month_visit_data = None + + # --- Process all fields from configuration --- + process_inclusions_mapping(output_inclusion, inclusion_data, record_data, request_data, all_questionnaires, six_month_visit_data) + + return output_inclusion + + +def process_organization(organization, index, total_organizations): + global threads_list, global_pbar + position = get_thread_position() + 2 + + organization_id = organization["id"] + output_inclusions = [] + inclusions = get_organization_inclusions(organization_id, 1000, 1) + + with tqdm(total=len(inclusions), unit='Incl.', + desc=f"{str(index) + "/" + str(total_organizations):<9} - {organization['name'][:40]:<40}", + position=position, leave=False, bar_format=custom_bar_format) as incl_pbar: + for inclusion in inclusions: + output_inclusion = _process_inclusion_data(inclusion, organization) + output_inclusions.append(output_inclusion) + incl_pbar.update(1) + with _global_pbar_lock: + if global_pbar: + global_pbar.update(1) + + return output_inclusions + + +# ============================================================================ +# BLOCK 9: MAIN EXECUTION +# ============================================================================ + +def main(): + global global_pbar, excel_export_config, excel_export_enabled + + # --- Check for CLI Check_Only mode --- + check_only_mode = "--check-only" in sys.argv + + if check_only_mode: + run_check_only_mode(sys.argv) + return + + # --- Check for CLI Excel_Only mode --- + excel_only_mode = "--excel-only" in sys.argv + + if excel_only_mode: + # Load mapping configs for Excel export (same as normal workflow) + print() + load_inclusions_mapping_config() + load_organizations_mapping_config() + + # Completely externalized Excel-only workflow + export_excel_only(sys.argv, INCLUSIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME, + inclusions_mapping_config, organizations_mapping_config) + return + + # === NORMAL MODE: Full data collection === + + print() + login_status = login() + while login_status == "Error": + login_status = login() + if login_status == "Exit": + return + + print() + ask_fetch_six_month_visit() + + print() + number_of_threads = int((questionary.text("Number of threads :", default="12", + validate=lambda x: x.isdigit() and 0 < int(x) <= MAX_THREADS).ask())) + + print() + ask_on_retry_exhausted() + + print() + wait_for_scheduled_launch() + + print() + load_inclusions_mapping_config() + load_organizations_mapping_config() + + # === LOAD AND VALIDATE EXCEL EXPORT CONFIGURATION === + print() + console.print("[bold cyan]Loading Excel export configuration...[/bold cyan]") + + # Validate Excel config (no data loading - JSONs don't exist yet in NORMAL MODE) + # prepare_excel_export() displays error messages directly to console + excel_export_config, has_config_critical, _ = \ + prepare_excel_export(inclusions_mapping_config, organizations_mapping_config) + + # Ask user confirmation if critical errors found + if has_config_critical: + print() + answer = questionary.confirm( + "⚠ Critical configuration errors detected. Continue anyway?", + default=False + ).ask() + if not answer: + console.print("[bold red]Aborted by user[/bold red]") + return + else: + excel_export_enabled = False # Skip Excel export if user continues despite errors + else: + excel_export_enabled = True if excel_export_config else False # Config is valid, Excel export can proceed + + print() + start_time = perf_counter() + organizations_list = get_all_organizations() + organizations_list = [org for org in organizations_list if org["id"] not in RC_ENDOBEST_EXCLUDED_CENTERS] + + # === APPLY ORGANIZATION CENTER MAPPING === + print() + print("Mapping organizations to centers...") + mapping_dict = load_organization_center_mapping() + apply_center_mapping(organizations_list, mapping_dict) + + print() + with ThreadPoolExecutor(max_workers=number_of_threads) as counter_pool: + futures = [counter_pool.submit(get_organization_counters, org) for org in organizations_list] + organizations_list_with_counters = [] + for future in tqdm(as_completed(futures), total=len(futures), desc=f"{'Fetching Organizations Counters':<52}", + unit="orgs.", bar_format=custom_bar_format): + try: + updated_org = future.result() + organizations_list_with_counters.append(updated_org) + except Exception as exc: + print(f"\nCRITICAL ERROR while fetching counters: {exc}") + counter_pool.shutdown(wait=False, cancel_futures=True) + raise + organizations_list = organizations_list_with_counters + print() + + inclusions_total_count = sum(org.get('patients_count', 0) for org in organizations_list) + organizations_list.sort(key=lambda org: (-org.get('patients_count', 0), org.get('name', ''))) + + number_of_organizations = len(organizations_list) + print(f"{inclusions_total_count} Inclusions in {number_of_organizations} Organizations...") + print() + + output_inclusions = [] + with tqdm(total=inclusions_total_count, unit="incl.", desc=f"{'Overall Progress':<52}", position=0, leave=True, + bar_format=custom_bar_format) as overall_progress_pbar: + global_pbar = overall_progress_pbar + + with ThreadPoolExecutor(max_workers=number_of_threads) as thread_pool: + futures = [thread_pool.submit(process_organization, organization, index + 1, number_of_organizations) + for index, organization in enumerate(organizations_list)] + + for future in as_completed(futures): + try: + result = future.result() + output_inclusions.extend(result) + except Exception as exc: + logging.critical(f"Arrêt dû à une exception critique dans un worker: {exc}", exc_info=True) + print(f"\nERREUR CRITIQUE dans un thread de traitement, arrêt du processus:") + print(f"Exception: {exc}") + print("Traceback original du worker:") + traceback.print_exc() + print( + "Signal d'arrêt envoyé au pool principal avec tentative d'annulation des tâches en attente...") + thread_pool.shutdown(wait=False, cancel_futures=True) + raise + + def get_sort_key(item): + org_name = get_nested_value(item, ["Patient_Identification", "Organisation_Name"], default='') + pseudo = get_nested_value(item, ["Patient_Identification", "Pseudo"], default='') + date_str = get_nested_value(item, ["Inclusion", "Inclusion_Date"], default='') + + sort_date = datetime.max + if date_str and date_str != "undefined": + try: + sort_date = datetime.strptime(date_str, '%d/%m/%Y') + except (ValueError, TypeError): + pass + + return org_name, sort_date, pseudo + + try: + print() + print() + print("Sorting results...") + output_inclusions.sort(key=get_sort_key) + + # === QUALITY CHECKS (before backup to avoid losing history on crash) === + print() + has_coherence_critical, has_regression_critical = run_quality_checks( + current_inclusions=output_inclusions, # list: données en mémoire (nouvellement collectées) + organizations_list=organizations_list, # list: données en mémoire avec compteurs + old_inclusions_filename=INCLUSIONS_FILE_NAME # str: "endobest_inclusions.json" (version courante sur disque) + ) + + # === CHECK FOR CRITICAL ISSUES AND ASK USER CONFIRMATION === + if has_coherence_critical or has_regression_critical: + print() + console.print("[bold red]⚠ CRITICAL issues detected in quality checks![/bold red]") + confirm_write = questionary.confirm( + "Do you want to write the results anyway?", + default=True + ).ask() + + if not confirm_write: + console.print("[yellow]✗ Output writing cancelled by user. Files were not modified.[/yellow]") + console.print("[yellow] You can re-run the script to try again.[/yellow]") + print() + print(f"Elapsed time : {str(timedelta(seconds=perf_counter() - start_time))}") + return + + # === BACKUP OLD FILES (only after checks pass and user confirmation) === + backup_output_files() + + # === WRITE NEW FILES === + print("Writing files...") + + with open(INCLUSIONS_FILE_NAME, 'w', encoding='utf-8') as f_json: + json.dump(output_inclusions, f_json, indent=4, ensure_ascii=False) + with open(ORGANIZATIONS_FILE_NAME, 'w', encoding='utf-8') as f_json: + json.dump(organizations_list, f_json, indent=4, ensure_ascii=False) + + console.print("[green]✓ Data saved to JSON files[/green]") + print() + + # === EXCEL EXPORT === + # Completely externalized Excel export workflow + run_normal_mode_export(excel_export_enabled, excel_export_config, + inclusions_mapping_config, organizations_mapping_config) + + except IOError as io_err: + logging.critical(f"Error while writing json file : {io_err}") + print(f"Error while writing json file : {io_err}") + except Exception as exc: + logging.critical(f"Error while writing json file : {exc}") + print(f"Error while writing json file : {exc}") + + print() + print(f"Elapsed time : {str(timedelta(seconds=perf_counter() - start_time))}") + + +if __name__ == '__main__': + + try: + main() + except Exception as e: + logging.critical(f"Le script principal s'est terminé prématurément à cause d'une exception: {e}", exc_info=True) + print(f"Le script s'est arrêté à cause d'une erreur : {e}") + finally: + if 'subtasks_thread_pool' in globals() and subtasks_thread_pool: + subtasks_thread_pool.shutdown(wait=False, cancel_futures=True) + print('\n') + input("Press Enter to exit...") diff --git a/eb_dashboard_check_only-exe.bat b/eb_dashboard_check_only-exe.bat new file mode 100644 index 0000000..df4de76 --- /dev/null +++ b/eb_dashboard_check_only-exe.bat @@ -0,0 +1,3 @@ +@echo off +eb_dashboard.exe --check-only %* + diff --git a/eb_dashboard_check_only.bat b/eb_dashboard_check_only.bat new file mode 100644 index 0000000..98e3de2 --- /dev/null +++ b/eb_dashboard_check_only.bat @@ -0,0 +1,4 @@ +@echo off +call C:\PythonProjects\.rcvenv\Scripts\activate.bat +python eb_dashboard.py --check-only %* + diff --git a/eb_dashboard_check_only_debug-exe.bat b/eb_dashboard_check_only_debug-exe.bat new file mode 100644 index 0000000..a956d60 --- /dev/null +++ b/eb_dashboard_check_only_debug-exe.bat @@ -0,0 +1,3 @@ +@echo off +eb_dashboard.exe --check-only --debug %* + diff --git a/eb_dashboard_check_only_debug.bat b/eb_dashboard_check_only_debug.bat new file mode 100644 index 0000000..e0550b0 --- /dev/null +++ b/eb_dashboard_check_only_debug.bat @@ -0,0 +1,4 @@ +@echo off +call C:\PythonProjects\.rcvenv\Scripts\activate.bat +python eb_dashboard.py --check-only --debug %* + diff --git a/eb_dashboard_constants.py b/eb_dashboard_constants.py new file mode 100644 index 0000000..8cec7cc --- /dev/null +++ b/eb_dashboard_constants.py @@ -0,0 +1,143 @@ +""" +Endobest Dashboard - Centralized Constants Module + +This module defines ALL constants used across the Endobest Dashboard application. +It serves as the single source of truth for all configuration values. + +All other modules MUST import constants from this module, NOT define them locally. + +Structure: +- File names & paths +- Table names (Excel sheets) +- API endpoints +- Authentication credentials +- Threading & retry parameters +- Protocol IDs +- UI formatting constants +""" + +# ============================================================================ +# FILE NAMES & PATHS +# ============================================================================ + +INCLUSIONS_FILE_NAME = "endobest_inclusions.json" +ORGANIZATIONS_FILE_NAME = "endobest_organizations.json" +OLD_FILE_SUFFIX = "_old" +CONFIG_FOLDER_NAME = "config" + +# ============================================================================ +# EXCEL CONFIGURATION FILES +# ============================================================================ + +DASHBOARD_CONFIG_FILE_NAME = "Endobest_Dashboard_Config.xlsx" +ORG_CENTER_MAPPING_FILE_NAME = "eb_org_center_mapping.xlsx" + +# ============================================================================ +# TABLE NAMES (Excel sheets in DASHBOARD_CONFIG_FILE_NAME) +# ============================================================================ + +INCLUSIONS_MAPPING_TABLE_NAME = "Inclusions_Mapping" +ORGANIZATIONS_MAPPING_TABLE_NAME = "Organizations_Mapping" +EXCEL_WORKBOOKS_TABLE_NAME = "Excel_Workbooks" +EXCEL_SHEETS_TABLE_NAME = "Excel_Sheets" +REGRESSION_CHECK_TABLE_NAME = "Regression_Check" +ORG_CENTER_MAPPING_TABLE_NAME = "Org_Center_Mapping" + +# ============================================================================ +# API ENDPOINTS & AUTHENTICATION +# ============================================================================ + +IAM_URL = "https://api-auth.ziwig-connect.com" +RC_URL = "https://api-hcp.ziwig-connect.com" +GDD_URL = "https://api-lab.ziwig-connect.com" +RC_APP_ID = "602aea51-cdb2-4f73-ac99-fd84050dc393" + +DEFAULT_USER_NAME = "ziwig-invest2@yopmail.com" +DEFAULT_PASSWORD = "pbrrA765$bP3beiuyuiyhiuy!agxagx" + +# ============================================================================ +# RESEARCH PROTOCOL CONFIGURATION +# ============================================================================ + +RC_ENDOBEST_PROTOCOL_ID = "3c7bcb4d-91ed-4e9f-b93f-99d8447a276e" +RC_ENDOBEST_EXCLUDED_CENTERS = [ + "e18e7487-60d5-4110-b465-b4156fe0e7f3", + "5582bd75-12fd-4d8e-bfd6-d63c43667a99", + "e053512f-d989-4564-8a73-b3d2d1b38fec" +] + +# ============================================================================ +# API ENDPOINTS +# ============================================================================ + +# Authentication endpoints +API_AUTH_LOGIN_ENDPOINT = "/api/auth/ziwig-pro/login" +API_AUTH_CONFIG_TOKEN_ENDPOINT = "/api/auth/config-token" +API_AUTH_REFRESH_TOKEN_ENDPOINT = "/api/auth/refreshToken" + +# Research Clinic (RC) endpoints +API_RC_GET_ALL_ORGANIZATIONS_ENDPOINT = "/api/inclusions/getAllOrganizations" +API_RC_INCLUSION_STATISTICS_ENDPOINT = "/api/inclusions/inclusion-statistics" +API_RC_SEARCH_INCLUSIONS_ENDPOINT = "/api/inclusions/search" +API_RC_GET_RECORD_BY_PATIENT_ENDPOINT = "/api/records/byPatient" +API_RC_GET_SURVEYS_ENDPOINT = "/api/surveys/filter/with-answers" +API_RC_SEARCH_VISITS_ENDPOINT = "/api/visits/visits/search" + +# GDD (Lab/Diagnostic) endpoints +API_GDD_GET_REQUEST_BY_TUBE_ID_ENDPOINT = "/api/requests/by-tube-id" + +# ============================================================================ +# THREADING & RETRY PARAMETERS +# ============================================================================ + +ERROR_MAX_RETRY = 10 +WAIT_BEFORE_RETRY = 1 +WAIT_BEFORE_NEW_BATCH_OF_RETRIES = 20 +MAX_BATCHS_OF_RETRIES = 3 +MAX_THREADS = 40 + +# Excel operation retry parameters (for handling transient xlwings/Excel failures) +# Applies to: SaveAs, Range.Select(), and other COM operations that can fail transiently on Excel 2013 +EXCEL_COM_MAX_RETRIES = 3 # Maximum retry attempts for transient COM failures +EXCEL_COM_RETRY_DELAY = 0.5 # Delay in seconds between retries + +# ============================================================================ +# LOGGING CONFIGURATION +# ============================================================================ + +LOG_FILE_NAME = "dashboard.log" + +# ============================================================================ +# API CONFIGURATION +# ============================================================================ + +API_TIMEOUT = 60 # seconds - timeout for all API calls + +# ============================================================================ +# EXCEL EXPORT CONFIGURATION +# ============================================================================ + +# Output file conflict handling actions +OUTPUT_ACTION_OVERWRITE = "Overwrite" +OUTPUT_ACTION_INCREMENT = "Increment" +OUTPUT_ACTION_BACKUP = "Backup" +OUTPUT_ACTIONS = [OUTPUT_ACTION_OVERWRITE, OUTPUT_ACTION_INCREMENT, OUTPUT_ACTION_BACKUP] + +# Excel export data source types +SOURCE_TYPE_INCLUSIONS = "Inclusions" +SOURCE_TYPE_ORGANIZATIONS = "Organizations" +SOURCE_TYPE_VARIABLE = "Variable" +SOURCE_TYPES = [SOURCE_TYPE_INCLUSIONS, SOURCE_TYPE_ORGANIZATIONS, SOURCE_TYPE_VARIABLE] + +# Excel export target types (for data filling) +TARGET_TYPE_TABLE = "Table" # Excel structured table (ListObject) - has headers, supports Resize() +TARGET_TYPE_NAMED_RANGE = "NamedRange" # Simple named range - no headers, resize via Name.RefersTo + +# ============================================================================ +# UI FORMATTING (Progress bars) +# ============================================================================ + +BAR_N_FMT_WIDTH = 4 +BAR_TOTAL_FMT_WIDTH = 4 +BAR_TIME_WIDTH = 8 +BAR_RATE_WIDTH = 10 diff --git a/eb_dashboard_debug-exe.bat b/eb_dashboard_debug-exe.bat new file mode 100644 index 0000000..96390a8 --- /dev/null +++ b/eb_dashboard_debug-exe.bat @@ -0,0 +1,3 @@ +@echo off +eb_dashboard.exe --debug %* + diff --git a/eb_dashboard_debug.bat b/eb_dashboard_debug.bat new file mode 100644 index 0000000..379f097 --- /dev/null +++ b/eb_dashboard_debug.bat @@ -0,0 +1,4 @@ +@echo off +call C:\PythonProjects\.rcvenv\Scripts\activate.bat +python eb_dashboard.py --debug %* + diff --git a/eb_dashboard_excel_export.py b/eb_dashboard_excel_export.py new file mode 100644 index 0000000..265ae72 --- /dev/null +++ b/eb_dashboard_excel_export.py @@ -0,0 +1,2094 @@ +""" +Endobest Dashboard - Excel Export Module + +This module handles generation of Excel workbooks from Inclusions and Organizations data. +Fully configurable via external Excel configuration file (Endobest_Dashboard_Config.xlsx). + +Features: +- Config-driven workbook generation (no code changes needed) +- Support for Variable templates and Table data fills +- Configurable filtering, sorting, and value replacement +- xlwings-based data processing with automatic formula recalculation +- Robust error handling and logging +""" + +import functools +import json +import logging +import os +import re +import shutil +import tempfile +import traceback +import zipfile +from datetime import datetime, timedelta, timezone +from time import perf_counter +from zoneinfo import ZoneInfo + +import openpyxl +from openpyxl.utils import get_column_letter +from rich.console import Console + +try: + import xlwings as xw +except ImportError: + xw = None + +from eb_dashboard_utils import get_nested_value, get_config_path +from eb_dashboard_constants import ( + INCLUSIONS_FILE_NAME, + ORGANIZATIONS_FILE_NAME, + DASHBOARD_CONFIG_FILE_NAME, + EXCEL_WORKBOOKS_TABLE_NAME, + EXCEL_SHEETS_TABLE_NAME, + OUTPUT_ACTION_OVERWRITE, + OUTPUT_ACTION_INCREMENT, + OUTPUT_ACTION_BACKUP, + OUTPUT_ACTIONS, + SOURCE_TYPE_INCLUSIONS, + SOURCE_TYPE_ORGANIZATIONS, + SOURCE_TYPE_VARIABLE, + SOURCE_TYPES, + TARGET_TYPE_TABLE, + TARGET_TYPE_NAMED_RANGE, + EXCEL_COM_MAX_RETRIES, + EXCEL_COM_RETRY_DELAY +) + +# ============================================================================ +# CONSTANTS +# ============================================================================ + +EXCEL_OUTPUT_FOLDER = os.getcwd() # Current working directory + +# ============================================================================ +# MODULE DEPENDENCIES (injected from main module) +# ============================================================================ + +console = None + +# NOTE: Constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH): +# Configuration Files: +# - INCLUSIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME, DASHBOARD_CONFIG_FILE_NAME +# - EXCEL_WORKBOOKS_TABLE_NAME, EXCEL_SHEETS_TABLE_NAME +# Output Handling: +# - OUTPUT_ACTION_OVERWRITE, OUTPUT_ACTION_INCREMENT, OUTPUT_ACTION_BACKUP, OUTPUT_ACTIONS +# Data Sources: +# - SOURCE_TYPE_INCLUSIONS, SOURCE_TYPE_ORGANIZATIONS, SOURCE_TYPE_VARIABLE, SOURCE_TYPES +# +# NOTE: Mapping table names (INCLUSIONS_MAPPING_TABLE_NAME, ORGANIZATIONS_MAPPING_TABLE_NAME) +# are defined in constants but loaded/used in main script (eb_dashboard.py) + + +def set_dependencies(console_instance): + """ + Inject console instance from main module. + + Args: + console_instance: Rich Console instance for formatted output + + Note: + File and table names are imported directly from eb_dashboard_constants.py + (SINGLE SOURCE OF TRUTH) + """ + global console + console = console_instance + + +# ============================================================================ +# PUBLIC FUNCTIONS +# ============================================================================ + +def load_excel_export_config(console_instance=None): + """ + Load and validate Excel export configuration from config file. + + Args: + console_instance: Optional Rich Console instance + + Returns: + Tuple of (excel_workbooks_config, excel_sheets_config, has_error, error_messages) + - excel_workbooks_config: List of workbook definitions + - excel_sheets_config: List of sheet fill definitions + - has_error: Boolean flag if critical errors found + - error_messages: List of error message strings + """ + global console + if console_instance: + console = console_instance + + config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) + error_messages = [] + + try: + workbook = openpyxl.load_workbook(config_path) + except FileNotFoundError: + error_msg = f"Error: Configuration file not found at: {config_path}" + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + return None, None, True, [error_msg] + + # Load Excel_Workbooks sheet + if EXCEL_WORKBOOKS_TABLE_NAME not in workbook.sheetnames: + error_msg = f"Error: Sheet '{EXCEL_WORKBOOKS_TABLE_NAME}' not found in configuration file." + error_messages.append(error_msg) + return None, None, True, error_messages + + excel_workbooks_sheet = workbook[EXCEL_WORKBOOKS_TABLE_NAME] + excel_workbooks_config = [] + + try: + headers = [cell.value for cell in excel_workbooks_sheet[1]] + for row_index, row in enumerate(excel_workbooks_sheet.iter_rows(min_row=2, values_only=True), start=2): + if all(cell is None for cell in row): + continue # Skip empty rows + + workbook_config = dict(zip(headers, row)) + + # Validate required fields + if not workbook_config.get("workbook_id"): + error_msg = f"Row {row_index}: 'workbook_id' is mandatory" + error_messages.append(error_msg) + continue + + if not workbook_config.get("workbook_template_name"): + error_msg = f"Row {row_index}: 'workbook_template_name' is mandatory" + error_messages.append(error_msg) + continue + + if not workbook_config.get("output_file_name_template"): + error_msg = f"Row {row_index}: 'output_file_name_template' is mandatory" + error_messages.append(error_msg) + continue + + if_output_exists = workbook_config.get("if_output_exists", OUTPUT_ACTION_OVERWRITE) + if if_output_exists not in OUTPUT_ACTIONS: + error_msg = f"Row {row_index}: 'if_output_exists' must be one of {OUTPUT_ACTIONS}" + error_messages.append(error_msg) + continue + + excel_workbooks_config.append(workbook_config) + except Exception as e: + error_msg = f"Error loading Excel_Workbooks sheet: {e}" + error_messages.append(error_msg) + return None, None, True, error_messages + + # Load Excel_Sheets sheet + if EXCEL_SHEETS_TABLE_NAME not in workbook.sheetnames: + error_msg = f"Error: Sheet '{EXCEL_SHEETS_TABLE_NAME}' not found in configuration file." + error_messages.append(error_msg) + return excel_workbooks_config, None, True, error_messages + + excel_sheets_sheet = workbook[EXCEL_SHEETS_TABLE_NAME] + excel_sheets_config = [] + + try: + headers = [cell.value for cell in excel_sheets_sheet[1]] + for row_index, row in enumerate(excel_sheets_sheet.iter_rows(min_row=2, values_only=True), start=2): + if all(cell is None for cell in row): + continue + + sheet_config = dict(zip(headers, row)) + + # Validate required fields + if not sheet_config.get("workbook_id"): + continue # Skip rows without workbook_id + + if not sheet_config.get("source_type"): + error_msg = f"Row {row_index}: 'source_type' is mandatory" + error_messages.append(error_msg) + continue + + source_type = sheet_config["source_type"] + if source_type not in SOURCE_TYPES: + error_msg = f"Row {row_index}: 'source_type' must be one of {SOURCE_TYPES}" + error_messages.append(error_msg) + continue + + if not sheet_config.get("source"): + error_msg = f"Row {row_index}: 'source' is mandatory" + error_messages.append(error_msg) + continue + + if not sheet_config.get("target_name"): + error_msg = f"Row {row_index}: 'target_name' is mandatory" + error_messages.append(error_msg) + continue + + # Parse JSON fields + has_json_error = False + for json_field in ["filter_condition", "sort_keys", "value_replacement"]: + value = sheet_config.get(json_field) + if value: + if isinstance(value, str): + try: + sheet_config[json_field] = json.loads(value) + except json.JSONDecodeError: + error_msg = f"Row {row_index}, field '{json_field}': Invalid JSON format" + error_messages.append(error_msg) + has_json_error = True + break # ← Skip this row entirely + # else: value is already parsed (dict/list), keep as-is + else: + # Empty/None value - leave as None or empty + sheet_config[json_field] = None + + if not has_json_error: + excel_sheets_config.append(sheet_config) + except Exception as e: + error_msg = f"Error loading Excel_Sheets sheet: {e}" + error_messages.append(error_msg) + return excel_workbooks_config, excel_sheets_config, True, error_messages + + workbook.close() + + has_error = len(error_messages) > 0 + return excel_workbooks_config, excel_sheets_config, has_error, error_messages + + +def validate_excel_config(excel_config, console_instance, inclusions_mapping_config=None, organizations_mapping_config=None): + """ + Validate Excel export configuration against templates. + + Args: + excel_config: Tuple of (workbooks_config, sheets_config) from load_excel_export_config() + console_instance: Rich Console instance + inclusions_mapping_config: Loaded inclusions mapping config (optional, for future use) + organizations_mapping_config: Loaded organizations mapping config (optional, for future use) + + Returns: + Tuple of (has_critical_error, error_messages) + """ + global console + if console_instance: + console = console_instance + + if not excel_config or not excel_config[0] or not excel_config[1]: + return False, [] # No config to validate + + excel_workbooks_config, excel_sheets_config = excel_config[0], excel_config[1] + error_messages = [] + + # Validate each workbook + for workbook_config in excel_workbooks_config: + workbook_id = workbook_config.get("workbook_id") + template_name = workbook_config.get("workbook_template_name") + + # Check template exists + template_path = os.path.join(get_config_path(), template_name) + if not os.path.exists(template_path): + error_msg = f"Template '{template_name}' (workbook_id: {workbook_id}) not found in config/" + error_messages.append(error_msg) + continue + + # Check template is valid Excel + try: + template_wb = openpyxl.load_workbook(template_path) + except Exception as e: + error_msg = f"Template '{template_name}' (workbook_id: {workbook_id}) is not a valid Excel file: {e}" + error_messages.append(error_msg) + continue + + # Validate sheets for this workbook + workbook_sheets = [s for s in excel_sheets_config if s.get("workbook_id") == workbook_id] + + for sheet_config in workbook_sheets: + target_name = sheet_config.get("target_name") + source_type = sheet_config.get("source_type") + + # Find the target in the template (check both named ranges AND tables) + target_found = False + if target_name in template_wb.defined_names: + target_found = True + else: + # Check if it's a table in any sheet + for sheet in template_wb.sheetnames: + sheet_obj = template_wb[sheet] + if hasattr(sheet_obj, 'tables') and target_name in sheet_obj.tables: + target_found = True + break + + # If target was found, validate based on source type + if target_found: + # For Variable sources, ensure it's a single cell + if source_type == SOURCE_TYPE_VARIABLE: + # Check if the defined name references a single cell + # NOTE: We still use openpyxl here because template_wb is already open from config loading + table_dims = _get_named_range_dimensions(template_wb, target_name) + if table_dims: + _, _, height, width = table_dims + if height != 1 or width != 1: + error_msg = f"Target '{target_name}' (template: {template_name}) for Variable source must reference a single cell (found {height}x{width})" + error_messages.append(error_msg) + + # For Table sources (Inclusions/Organizations), validate dimensions + elif source_type in [SOURCE_TYPE_INCLUSIONS, SOURCE_TYPE_ORGANIZATIONS]: + # Get the dimensions of the named range + # NOTE: We still use openpyxl here because template_wb is already open from config loading + table_dims = _get_named_range_dimensions(template_wb, target_name) + if table_dims: + _, _, height, width = table_dims + + # CRITICAL: Table height MUST be exactly 1 (template row only) + if height != 1: + error_msg = f"Target '{target_name}' (template: {template_name}, source_type: {source_type}) must have height=1 (found height={height}). " \ + f"Template row must be a single row." + error_messages.append(error_msg) + + # CRITICAL: Table width must be >= max(mapping_indices) + # Get the mapping column to validate indices + source = sheet_config.get("source") + if source: + mapping_config = inclusions_mapping_config if source_type == SOURCE_TYPE_INCLUSIONS else organizations_mapping_config + if mapping_config: + column_mapping = _get_column_mapping(mapping_config, source, source_type) + if column_mapping: + max_col_index = max(column_mapping.keys()) # 0-based index + if max_col_index >= width: + error_msg = f"Target '{target_name}' (template: {template_name}) width={width} is insufficient. " \ + f"Maximum column index from mapping is {max_col_index} (0-based). " \ + f"Width must be > {max_col_index}." + error_messages.append(error_msg) + else: + error_msg = f"Named range '{target_name}' (template: {template_name}, workbook_id: {workbook_id}) not found in template" + error_messages.append(error_msg) + + template_wb.close() + + return len(error_messages) > 0, error_messages + + +def export_to_excel(inclusions_data, organizations_data, excel_config, + inclusions_mapping_config=None, organizations_mapping_config=None): + """ + Main export function - orchestrates Excel workbook generation. + + Args: + inclusions_data: List of inclusion dictionaries + organizations_data: List of organization dictionaries + excel_config: Tuple of (workbooks_config, sheets_config) + inclusions_mapping_config: Inclusions field mapping configuration + organizations_mapping_config: Organizations field mapping configuration + + Returns: + Tuple of (success, error_count) + + Note: + Uses global console instance (injected from main script) + """ + if not excel_config or not excel_config[0] or not excel_config[1]: + console.print("[yellow]⚠ No Excel export configuration found, skipping[/yellow]") + return True, 0 + + excel_workbooks_config, excel_sheets_config = excel_config[0], excel_config[1] + + # Prepare template variables + template_vars = _prepare_template_variables() + + error_count = 0 + success_count = 0 + + # Track overall export duration + export_start_time = perf_counter() + + # Process each workbook + for workbook_config in excel_workbooks_config: + try: + workbook_id = workbook_config.get("workbook_id") + template_name = workbook_config.get("workbook_template_name") + output_template = workbook_config.get("output_file_name_template") + if_output_exists = workbook_config.get("if_output_exists", OUTPUT_ACTION_OVERWRITE) + + # Resolve output filename + try: + output_filename = output_template.format(**template_vars) + except KeyError as e: + console.print(f"[bold red]✗ Unknown variable in template: {e}[/bold red]") + error_count += 1 + continue + + output_path = os.path.join(EXCEL_OUTPUT_FOLDER, output_filename) + + # Log workbook processing start + logging.info(f"Processing workbook: {workbook_id} (template: {template_name}, output: {output_filename})") + + # PHASE PRÉPARATION: Handle existing file according to action + output_path = _handle_output_exists(output_path, if_output_exists) + + # XLWINGS PHASE: Open template, fill, save as output + template_path = os.path.join(get_config_path(), template_name) + + # Track workbook processing duration with spinning status + workbook_start_time = perf_counter() + + try: + if xw is None: + raise ImportError("xlwings is not installed. Install with: pip install xlwings") + + # Use status with spinner while processing the workbook + with console.status(f"[bold cyan]Exporting {output_filename}...", spinner="dots"): + # PERFORMANCE: Make Excel invisible BEFORE opening the workbook + app_xw = None + screen_updating_original = None + visible_original = None + try: + # Get or create Excel app in invisible mode + if xw.apps: + app_xw = xw.apps.active + visible_original = app_xw.visible + screen_updating_original = app_xw.screen_updating + else: + # Create new app in invisible mode + app_xw = xw.App(visible=False) + visible_original = False + screen_updating_original = True + + app_xw.visible = False # Make Excel invisible + app_xw.screen_updating = False # Disable screen updates + + except Exception as e: + logging.warning(f"Failed to manage Excel visibility: {e}") + app_xw = None + + # Open TEMPLATE directly (not a copy) + wb_xw = xw.Book(template_path, update_links=False) + + try: + # CAPTURE TEMPLATE STATE: Save initial state for restoration before save + template_state = _capture_workbook_state(wb_xw, workbook_context=f"{workbook_id} ({output_filename})") + logging.info(f"Captured template state: active_sheet='{template_state['active_sheet']}', {len(template_state['sheets'])} sheet(s)") + + # Get sheets for this workbook + workbook_sheets = [s for s in excel_sheets_config if s.get("workbook_id") == workbook_id] + + # Process each sheet with xlwings + for sheet_config in workbook_sheets: + _process_sheet_xlwings( + wb_xw, + sheet_config, + inclusions_data, + organizations_data, + inclusions_mapping_config=inclusions_mapping_config, + organizations_mapping_config=organizations_mapping_config, + template_vars=template_vars, + workbook_context=f"{workbook_id} ({output_filename})" + ) + + # RESTORE TEMPLATE STATE: Restore initial state before saving + _restore_workbook_state(wb_xw, template_state, workbook_context=f"{workbook_id} ({output_filename})") + logging.info(f"Restored template state before save") + + # Save as output file with forced overwrite (with retry mechanism) + # This preserves filesystem versioning for cloud storage + # Disable alerts to force silent overwrite + abs_output_path = os.path.abspath(output_path) + if app_xw: + display_alerts_original = app_xw.api.DisplayAlerts + app_xw.api.DisplayAlerts = False + try: + _save_workbook_with_retry(wb_xw, abs_output_path) + logging.info(f"Saved workbook to: {abs_output_path}") + finally: + if app_xw: + app_xw.api.DisplayAlerts = display_alerts_original + # Excel automatically recalculates formulas on save + # No need for separate recalculation step + + finally: + # Always close the workbook and restore visibility/screen updates + wb_xw.close() + if app_xw is not None: + try: + if screen_updating_original is not None: + app_xw.screen_updating = screen_updating_original + if visible_original is not None: + app_xw.visible = visible_original + except: + pass + + # Calculate duration and display success message + workbook_duration = perf_counter() - workbook_start_time + console.print(f"[green]✓ Created: {output_filename} ({workbook_duration:.2f}s)[/green]") + success_count += 1 + + except Exception as e: + console.print(f"[bold red]✗ Error processing {output_filename}: {e}[/bold red]") + logging.error(f"Excel export error for {output_filename}: {e}", exc_info=True) + error_count += 1 + continue + + except Exception as e: + console.print(f"[bold red]✗ Error processing workbook {workbook_id}: {e}[/bold red]") + logging.error(f"Excel workbook processing error: {e}", exc_info=True) + error_count += 1 + + # Summary with total duration + total_workbooks = success_count + error_count + export_duration = perf_counter() - export_start_time + + if error_count == 0: + # Success: all workbooks processed + console.print(f"\n[green]✓ Excel export completed successfully: {success_count}/{total_workbooks} workbooks generated ({export_duration:.2f}s)[/green]") + else: + # Failure: some or all workbooks failed + if success_count > 0: + # Partial success + console.print(f"\n[yellow]⚠ Excel export completed with errors ({export_duration:.2f}s)[/yellow]") + console.print(f"[green] {success_count} workbook(s) generated successfully[/green]") + console.print(f"[bold red] {error_count} workbook(s) failed[/bold red]") + else: + # Complete failure + console.print(f"\n[bold red]✗ Excel export failed: all {error_count} workbook(s) failed ({export_duration:.2f}s)[/bold red]") + + return error_count == 0, error_count + + +# ============================================================================ +# INTERNAL FUNCTIONS +# ============================================================================ + +def _prepare_template_variables(): + """ + Prepare variables available for Template String evaluation. + + Returns: + Dictionary of variables available to .format(**locals()) + """ + # Get UTC timestamp from inclusions file + # Use constant from eb_dashboard_constants (SINGLE SOURCE OF TRUTH) + inclusions_file = INCLUSIONS_FILE_NAME + if os.path.exists(inclusions_file): + file_mtime = os.path.getmtime(inclusions_file) + extract_date_time_utc = datetime.fromtimestamp(file_mtime, tz=timezone.utc) + else: + extract_date_time_utc = datetime.now(tz=timezone.utc) + + # Convert to Paris timezone + extract_date_time_french = extract_date_time_utc.astimezone( + ZoneInfo('Europe/Paris') + ) + + return { + 'extract_date_time_utc': extract_date_time_utc, + 'extract_date_time_french': extract_date_time_french, + } + + +def _apply_filter(item, filter_condition): + """ + Apply filter condition to item (AND logic for all conditions). + + Args: + item: Dictionary to filter + filter_condition: List of [field_name, operator, value] conditions + + Returns: + Boolean True if item passes all filters + """ + if not filter_condition: + return True # Empty filter = accept all + + for field_path, operator, expected_value in filter_condition: + actual_value = get_nested_value(item, field_path.split(".")) + + if actual_value is None: + return False # Missing field = filter out + + # Apply operator + if operator == "==": + if actual_value != expected_value: + return False + elif operator == "<>": + if actual_value == expected_value: + return False + elif operator == ">": + if not (actual_value > expected_value): + return False + elif operator == ">=": + if not (actual_value >= expected_value): + return False + elif operator == "<": + if not (actual_value < expected_value): + return False + elif operator == "<=": + if not (actual_value <= expected_value): + return False + + return True # All conditions passed + + +def _apply_sort(items, sort_keys): + """ + Apply multi-key sort to items with support for mixed asc/desc ordering. + + Args: + items: List of dictionaries to sort + sort_keys: List of [field_name, order] or [field_name, order, option] + where: + - order is "asc" or "desc" + - option (optional) can be: + * datetime format string (e.g., "%Y-%m-%d") for date parsing + * "*natsort" for natural alphanumeric sorting + Supports MIXED asc/desc on different columns! + + Returns: + Sorted list + """ + if not sort_keys: + return items + + def natural_sort_key(text): + """ + Helper for natural alphanumeric sorting. + Converts "ENDOBEST-003-920-BA" to ["endobest", "-", 3, "-", 920, "-", "ba"] + Python's native list comparison handles the rest element by element. + """ + def convert(segment): + return int(segment) if segment.isdigit() else segment.lower() + return [convert(s) for s in re.split(r'(\d+)', str(text)) if s] + + def compare_items(item1, item2): + """ + Comparator function for multi-key sorting with mixed asc/desc support. + Returns: -1 if item1 < item2, 0 if equal, 1 if item1 > item2 + """ + for sort_spec in sort_keys: + field_name = sort_spec[0] + order = sort_spec[1] if len(sort_spec) > 1 else "asc" + sort_option = sort_spec[2] if len(sort_spec) > 2 else None + + # Get values from both items + val1 = get_nested_value(item1, field_name.split(".")) + val2 = get_nested_value(item2, field_name.split(".")) + + # Handle undefined/None - place at end + is_undef1 = val1 in [None, "", "undefined"] + is_undef2 = val2 in [None, "", "undefined"] + + # Both undefined: equal + if is_undef1 and is_undef2: + continue + + # Only one undefined: undefined goes last + if is_undef1: + return 1 # item1 > item2 (undefined last) + if is_undef2: + return -1 # item1 < item2 (item2 is undefined) + + # Check if natural sort requested + is_natural_sort = (sort_option == "*natsort") + + # Parse datetime if option is a datetime format (not *natsort) + if sort_option and not is_natural_sort: + datetime_format = sort_option + try: + val1 = datetime.strptime(str(val1), datetime_format).timestamp() + except (ValueError, TypeError): + val1 = None + return 1 # Invalid datetime goes last + + try: + val2 = datetime.strptime(str(val2), datetime_format).timestamp() + except (ValueError, TypeError): + val2 = None + return -1 # Invalid datetime goes last + + # Apply natural sort transformation if requested + if is_natural_sort: + val1 = natural_sort_key(val1) + val2 = natural_sort_key(val2) + + # Compare values + # For strings (non-natsort), use case-insensitive comparison for natural alphabetical ordering + if isinstance(val1, str) and isinstance(val2, str): + val1_lower = val1.lower() + val2_lower = val2.lower() + if val1_lower < val2_lower: + cmp_result = -1 + elif val1_lower > val2_lower: + cmp_result = 1 + else: + # Case-insensitive equal, use case-sensitive as tiebreaker + if val1 < val2: + cmp_result = -1 + elif val1 > val2: + cmp_result = 1 + else: + cmp_result = 0 + else: + # Non-string comparison (numbers, dates, natsort lists, etc.) + if val1 < val2: + cmp_result = -1 + elif val1 > val2: + cmp_result = 1 + else: + cmp_result = 0 # Equal, continue to next sort key + + # Apply asc/desc ordering + if cmp_result != 0: + is_desc = isinstance(order, str) and order.lower() == "desc" + return cmp_result if not is_desc else -cmp_result + + # All keys are equal + return 0 + + # Use functools.cmp_to_key to convert comparator to key function + return sorted(items, key=functools.cmp_to_key(compare_items)) + + +def _apply_value_replacement(value, replacements): + """ + Apply value replacement rules (first-match-wins, strict type matching). + + Args: + value: Value to potentially replace + replacements: List of [value_before, value_after] pairs + + Returns: + Replaced value or original + + Note: + This function is currently prepared for future use in table data filling. + """ + if not replacements: + return value + + for value_before, value_after in replacements: + if value == value_before: # Strict equality + return value_after + + return value # No match, return original + + +# OBSOLETE: _preserve_media_in_workbook() removed - xlwings handles media preservation automatically +# When using xlwings, Excel natively preserves all media, images, and relationships + + +def _save_workbook_with_retry(wb_xw, output_path): + """ + Save workbook with retry mechanism for transient xlwings/Excel failures. + + Excel's SaveAs can fail randomly on some environments (especially Excel 2013). + This function retries the save operation with configurable retry count and delay. + + Args: + wb_xw: xlwings Book object + output_path: Absolute path where workbook should be saved + + Raises: + Exception: If SaveAs fails after all retry attempts + """ + from time import sleep + + for attempt in range(1, EXCEL_COM_MAX_RETRIES + 1): + try: + logging.info(f"SaveAs attempt {attempt}/{EXCEL_COM_MAX_RETRIES}: {output_path}") + wb_xw.api.SaveAs(output_path) + logging.info(f"SaveAs succeeded on attempt {attempt}") + return # Success + + except Exception as e: + error_msg = f"SaveAs failed on attempt {attempt}: {type(e).__name__}: {str(e)}" + + if attempt < EXCEL_COM_MAX_RETRIES: + # Intermediate retry - log as warning and sleep before retry + logging.warning(f"{error_msg} - Retrying in {EXCEL_COM_RETRY_DELAY}s...") + sleep(EXCEL_COM_RETRY_DELAY) + else: + # Final attempt failed - log as critical error and raise + logging.error(f"{error_msg} - All {EXCEL_COM_MAX_RETRIES} retry attempts exhausted") + raise + + +def _capture_workbook_state(wb_xw, workbook_context=""): + """ + Capture the visual state of the workbook (active sheet, selections, scroll positions). + + This allows restoration of the template's visual state after data processing, + ensuring recipients see the workbook exactly as designed in the template. + + Args: + wb_xw: xlwings Book object + workbook_context: String identifier for logging (workbook_id and filename) + + Returns: + dict: State dictionary with 'active_sheet' and 'sheets' state per sheet + """ + ctx = f"[{workbook_context}]" if workbook_context else "" + logging.info(f"{ctx} [CAPTURE_STATE] Starting workbook state capture") + logging.info(f"{ctx} [CAPTURE_STATE] Total sheets: {len(wb_xw.sheets)}") + + state = { + 'active_sheet': None, + 'sheets': {} + } + + try: + # Capture active sheet name + state['active_sheet'] = wb_xw.api.ActiveSheet.Name + logging.info(f"{ctx} [CAPTURE_STATE] Active sheet captured: '{state['active_sheet']}'") + except Exception as e: + logging.warning(f"{ctx} [CAPTURE_STATE] Could not capture active sheet: {type(e).__name__}: {str(e)}") + + # Capture state for each sheet + for idx, sheet in enumerate(wb_xw.sheets, 1): + logging.info(f"{ctx} [CAPTURE_STATE] Processing sheet {idx}/{len(wb_xw.sheets)}: '{sheet.name}'") + try: + # Activate sheet to get its state + sheet.activate() + logging.info(f"{ctx} [CAPTURE_STATE] Sheet '{sheet.name}' activated successfully") + sheet_api = sheet.api + + sheet_state = { + 'selection': None, + 'scroll_row': 1, + 'scroll_col': 1 + } + + # Capture selection address + try: + selection_address = sheet_api.Application.Selection.Address + sheet_state['selection'] = selection_address + logging.info(f"{ctx} [CAPTURE_STATE] Sheet '{sheet.name}' selection captured: {selection_address}") + except Exception as e: + sheet_state['selection'] = "A1" # Default + logging.warning(f"{ctx} [CAPTURE_STATE] Could not capture selection for sheet '{sheet.name}': {type(e).__name__}, defaulting to A1") + + # Capture scroll position + try: + scroll_row = sheet_api.Application.ActiveWindow.ScrollRow + scroll_col = sheet_api.Application.ActiveWindow.ScrollColumn + sheet_state['scroll_row'] = scroll_row + sheet_state['scroll_col'] = scroll_col + logging.info(f"{ctx} [CAPTURE_STATE] Sheet '{sheet.name}' scroll position captured: Row={scroll_row}, Col={scroll_col}") + except Exception as e: + logging.warning(f"{ctx} [CAPTURE_STATE] Could not capture scroll position for sheet '{sheet.name}': {type(e).__name__}, keeping defaults") + + state['sheets'][sheet.name] = sheet_state + logging.info(f"{ctx} [CAPTURE_STATE] Sheet '{sheet.name}' state complete: {sheet_state}") + + except Exception as e: + logging.error(f"{ctx} [CAPTURE_STATE] ERROR capturing state for sheet '{sheet.name}': {type(e).__name__}: {str(e)}") + + logging.info(f"{ctx} [CAPTURE_STATE] Workbook state capture complete. Captured {len(state['sheets'])} sheet(s)") + return state + + +def _restore_workbook_state(wb_xw, state, workbook_context=""): + """ + Restore the visual state of the workbook (active sheet, selections, scroll positions). + + Args: + wb_xw: xlwings Book object + state: State dictionary from _capture_workbook_state() + workbook_context: String identifier for logging (workbook_id and filename) + """ + if not state: + logging.warning("[RESTORE_STATE] Empty state provided, skipping restoration") + return + + from time import sleep + + ctx = f"[{workbook_context}]" if workbook_context else "" + logging.info(f"{ctx} [RESTORE_STATE] Starting workbook state restoration") + logging.info(f"{ctx} [RESTORE_STATE] Restoring {len(state.get('sheets', {}))} sheet(s)") + + # NOTE: Screen updating is already disabled at the global level (in export_to_excel) + # for the entire workbook processing cycle (from open to save). + # We do NOT re-disable it here to avoid state conflicts. + # The global setting ensures all operations (capture, process, restore, save) run efficiently. + + # CRITICAL: Excel 2013 COM layer lock recovery + # After bulk paste operations, Excel's COM layer can enter a "locked" state where Range.Select() + # fails persistently. This appears to be a fundamental limitation/bug in Excel 2013. + # To work around this, we need to: + # 1. Give Excel time to recover with a large delay + # 2. Then make a "dummy" Range.Select() to wake up the COM layer + # 3. Then proceed with real restorations + logging.info(f"{ctx} [RESTORE_STATE] Waiting for Excel COM layer to stabilize after bulk operations (2 seconds)...") + sleep(2.0) # Large delay to allow COM layer to recover + + # Track original visibility state (used for temporary visibility during retries) + original_app_visible = None + try: + if wb_xw.app: + original_app_visible = wb_xw.app.visible + + if not original_app_visible: + # Make Excel visible during restoration so user sees what's happening + # (important for selection restore retries which may take 2+ seconds) + wb_xw.app.visible = True + logging.info(f"{ctx} [RESTORE_STATE] Excel app temporarily made visible for restoration operations") + except Exception as e: + logging.warning(f"{ctx} [RESTORE_STATE] Could not manage Excel visibility during restoration: {type(e).__name__}: {str(e)}") + + # Wake up the COM layer with a dummy selection attempt on the first sheet + # This "primes" the COM layer so subsequent Range.Select() calls work reliably + try: + if len(wb_xw.sheets) > 0: + first_sheet = wb_xw.sheets[0] + first_sheet.activate() + logging.info(f"{ctx} [RESTORE_STATE] Priming COM layer by activating first sheet...") + first_sheet.api.Range("$A$1").Select() + logging.info(f"{ctx} [RESTORE_STATE] COM layer priming successful") + except Exception as e: + # This is not critical - if it fails, retries will handle it + logging.info(f"{ctx} [RESTORE_STATE] COM layer priming attempt completed (may have failed, retries will handle it)") + + # Restore state for each sheet + for idx, (sheet_name, sheet_state) in enumerate(state.get('sheets', {}).items(), 1): + logging.info(f"{ctx} [RESTORE_STATE] Processing sheet {idx}: '{sheet_name}'") + try: + sheet = wb_xw.sheets[sheet_name] + sheet.activate() + logging.info(f"{ctx} [RESTORE_STATE] Sheet '{sheet_name}' activated successfully") + + # Small delay after activation to ensure Excel has completed the sheet switch + sleep(0.3) + + sheet_api = sheet.api + + # Restore selection with retry mechanism for transient Excel COM failures + if sheet_state.get('selection'): + selection = sheet_state['selection'] + selection_restored = False + + # Try to restore original selection with retry + for attempt in range(1, EXCEL_COM_MAX_RETRIES + 1): + try: + logging.info(f"{ctx} [RESTORE_STATE] Selection restore attempt {attempt}/{EXCEL_COM_MAX_RETRIES} for '{selection}' on sheet '{sheet_name}'") + sheet_api.Range(selection).Select() + logging.info(f"{ctx} [RESTORE_STATE] Sheet '{sheet_name}' selection restored to: {selection}") + selection_restored = True + break # Success + except Exception as e: + error_msg = f"Selection restore failed on attempt {attempt}: {type(e).__name__}: {str(e)}" + + if attempt < EXCEL_COM_MAX_RETRIES: + # Intermediate retry - log as warning and sleep before retry + logging.warning(f"{ctx} [RESTORE_STATE] {error_msg} - Retrying in {EXCEL_COM_RETRY_DELAY}s...") + sleep(EXCEL_COM_RETRY_DELAY) + else: + # Final attempt failed - log as error, will default to A1 + logging.error(f"{ctx} [RESTORE_STATE] {error_msg} - All {EXCEL_COM_MAX_RETRIES} retry attempts exhausted") + + # If selection restore failed after all retries, default to A1 + if not selection_restored: + logging.warning(f"{ctx} [RESTORE_STATE] Could not restore selection '{selection}' for sheet '{sheet_name}' after {EXCEL_COM_MAX_RETRIES} attempts, defaulting to A1") + + # Try to set default A1 selection (using absolute reference: $A$1) + for attempt in range(1, EXCEL_COM_MAX_RETRIES + 1): + try: + logging.info(f"{ctx} [RESTORE_STATE] A1 default attempt {attempt}/{EXCEL_COM_MAX_RETRIES} for sheet '{sheet_name}'") + sheet_api.Range("$A$1").Select() + logging.info(f"{ctx} [RESTORE_STATE] Sheet '{sheet_name}' selection defaulted to A1") + break # Success + except Exception as e2: + error_msg = f"A1 default failed on attempt {attempt}: {type(e2).__name__}: {str(e2)}" + + if attempt < EXCEL_COM_MAX_RETRIES: + logging.warning(f"{ctx} [RESTORE_STATE] {error_msg} - Retrying in {EXCEL_COM_RETRY_DELAY}s...") + sleep(EXCEL_COM_RETRY_DELAY) + else: + logging.error(f"{ctx} [RESTORE_STATE] {error_msg} - All {EXCEL_COM_MAX_RETRIES} retry attempts exhausted") + + # Restore scroll position + try: + scroll_row = sheet_state.get('scroll_row', 1) + scroll_col = sheet_state.get('scroll_col', 1) + sheet_api.Application.ActiveWindow.ScrollRow = scroll_row + sheet_api.Application.ActiveWindow.ScrollColumn = scroll_col + logging.info(f"{ctx} [RESTORE_STATE] Sheet '{sheet_name}' scroll position restored: Row={scroll_row}, Col={scroll_col}") + except Exception as e: + logging.warning(f"{ctx} [RESTORE_STATE] Could not restore scroll position for sheet '{sheet_name}': {type(e).__name__}") + + except Exception as e: + logging.error(f"{ctx} [RESTORE_STATE] ERROR restoring state for sheet '{sheet_name}': {type(e).__name__}: {str(e)}") + + # Restore active sheet + if state.get('active_sheet'): + try: + from time import sleep + + active_sheet_name = state['active_sheet'] + wb_xw.sheets[active_sheet_name].activate() + + # Wait for sheet activation to complete on Excel 2013's COM layer + sleep(0.3) + + logging.info(f"{ctx} [RESTORE_STATE] Active sheet restored to: '{active_sheet_name}'") + except Exception as e: + logging.error(f"{ctx} [RESTORE_STATE] Could not restore active sheet '{state.get('active_sheet')}': {type(e).__name__}: {str(e)}") + + # Force sheet tabs to scroll to show the first sheet + # This ensures the tab bar starts from the first sheet, regardless of which sheet is active + # NOTE: ScrollWorkbookTabs only works when Excel is visible + try: + if len(wb_xw.sheets) > 0 and wb_xw.app: + logging.info(f"{ctx} [RESTORE_STATE] Attempting to scroll sheet tabs to first sheet") + + try: + # ScrollWorkbookTabs with negative number scrolls tabs LEFT (toward first sheet) + # Use large negative number (-100) to guarantee we reach the beginning + # Excel visibility is already managed at the beginning of this function + wb_xw.api.Application.ActiveWindow.ScrollWorkbookTabs(-100) + logging.info(f"{ctx} [RESTORE_STATE] Sheet tabs scrolled to beginning") + except Exception as e: + logging.warning(f"{ctx} [RESTORE_STATE] Could not scroll sheet tabs to beginning: {type(e).__name__}: {str(e)}") + except Exception as e: + logging.error(f"{ctx} [RESTORE_STATE] ERROR during sheet tabs scroll operation: {type(e).__name__}: {str(e)}") + + # Restore original visibility state (if we temporarily made it visible) + # NOTE: Screen updating restoration is handled at the global level (in export_to_excel) + # after the workbook is saved and closed + try: + if original_app_visible is not None and wb_xw.app: + if not original_app_visible and wb_xw.app.visible: + # Restore to hidden state if it was originally hidden + wb_xw.app.visible = False + logging.info(f"{ctx} [RESTORE_STATE] Excel app visibility restored to original state: False") + except Exception as e: + logging.warning(f"{ctx} [RESTORE_STATE] Could not restore Excel app visibility: {type(e).__name__}: {str(e)}") + + logging.info(f"{ctx} [RESTORE_STATE] Workbook state restoration complete") + + +def _handle_output_exists(output_path, action): + """ + Handle existing output file (Overwrite/Increment/Backup). + + Args: + output_path: Full path to output file + action: "Overwrite", "Increment", or "Backup" + + Returns: + Actual path to use (may be different if Increment/Backup) + """ + if not os.path.exists(output_path): + logging.info(f"Output file doesn't exist yet: {output_path}") + return output_path + + logging.info(f"Output file exists, applying '{action}' rule: {output_path}") + + if action == OUTPUT_ACTION_OVERWRITE: + logging.info(f"Overwriting existing file: {output_path}") + return output_path + + elif action == OUTPUT_ACTION_INCREMENT: + base, ext = os.path.splitext(output_path) + counter = 1 + while os.path.exists(f"{base}_{counter}{ext}"): + counter += 1 + new_path = f"{base}_{counter}{ext}" + logging.info(f"Using incremented filename: {new_path}") + return new_path + + elif action == OUTPUT_ACTION_BACKUP: + base, ext = os.path.splitext(output_path) + counter = 1 + backup_path = f"{base}_backup_{counter}{ext}" + while os.path.exists(backup_path): + counter += 1 + backup_path = f"{base}_backup_{counter}{ext}" + + try: + logging.info(f"Backing up existing file to: {backup_path}") + shutil.copy2(output_path, backup_path) + logging.info(f"Backup successful: {output_path} -> {backup_path}") + except Exception as e: + logging.error(f"Backup failed: {e}") + raise + + # Return original path - the existing file will be overwritten by SaveAs + return output_path + + return output_path + + +def _get_column_mapping(mapping_config, mapping_column_name, source_type): + """ + Extract column mapping from Inclusions_Mapping or Organizations_Mapping. + + The mapping column contains user-friendly 1-based indices (1, 2, 3, ...) + indicating which column in the Excel table each field should be placed. + These are converted to 0-based indices for internal use. + + Args: + mapping_config: List of mapping config rows (dicts with field_name, etc.) + mapping_column_name: Name of the mapping column to extract (e.g., "MainReport_PatientsList") + source_type: "Inclusions" or "Organizations" + + Returns: + Dictionary: {excel_column_index: source_field_name} + Example: {0: "Patient_Identification.Patient_Id", 1: "Inclusion.Status", ...} + Indices are 0-based (converted from 1-based user input) + or None if mapping_column not found + """ + if not mapping_config: + return None + + column_mapping = {} + + for row in mapping_config: + # Get the field name (source field in the JSON) + field_name = row.get("field_name") + if not field_name: + continue + + # Get the mapping value from the specified column + mapping_value = row.get(mapping_column_name) + if mapping_value is None or mapping_value == "": + continue # Skip empty mappings + + # Convert mapping_value to integer (1-based user-friendly index) + try: + user_col_index = int(mapping_value) + except (ValueError, TypeError): + logging.warning(f"Invalid column index '{mapping_value}' for field '{field_name}'") + continue + + # Convert 1-based to 0-based index + excel_col_index = user_col_index - 1 + + if excel_col_index < 0: + logging.warning(f"Column index '{user_col_index}' for field '{field_name}' must be >= 1") + continue + + # Store: excel_column_index -> field_name + # Field name needs to be qualified with group for Inclusions + # (extracted from the row's field_group if available) + if source_type == "Inclusions": + field_group = row.get("field_group", "") + if field_group: + full_field_name = f"{field_group}.{field_name}" + else: + full_field_name = field_name + else: + # For Organizations, field_name might already be qualified or standalone + full_field_name = field_name + + column_mapping[excel_col_index] = full_field_name + + return column_mapping if column_mapping else None + + +def _parse_range_dimensions(start_row, start_col, end_row, end_col, header_row_count=0): + """ + Shared utility: Calculate dimensions from cell coordinates. + + Args: + start_row, start_col: Starting cell (1-based, after headers) + end_row, end_col: Ending cell (1-based) + header_row_count: Number of header rows (0 if none) + + Returns: + Tuple of (width, total_height, data_height) + """ + width = end_col - start_col + 1 + total_height = end_row - start_row + 1 + data_height = total_height - header_row_count + return width, total_height, data_height + + +def _get_named_range_dimensions(workbook, range_name): + """ + Get dimensions of named range or table in workbook. + + Args: + workbook: openpyxl Workbook object + range_name: Name of the named range or table + + Returns: + Tuple of (sheet_name, start_cell, height, width) or None if not found + """ + # First check for defined named ranges (in openpyxl 3.x) + if range_name in workbook.defined_names: + defined_name = workbook.defined_names[range_name] + # Get the range reference from attr_text (e.g., "Sheet!$A$1:$B$10") + range_ref = defined_name.attr_text + + # Parse: "SheetName!$A$1:$B$10" + if '!' in range_ref: + sheet_name, cell_range = range_ref.split('!') + # Remove quotes if present + sheet_name = sheet_name.strip("'\"") + # Remove $ signs for parsing + cell_range = cell_range.replace('$', '') + + if sheet_name in workbook.sheetnames: + sheet = workbook[sheet_name] + + # Parse cell range (e.g., "A1:B10" or single "A1") + if ':' in cell_range: + start_cell_str, end_cell_str = cell_range.split(':') + start_cell = sheet[start_cell_str] + end_cell = sheet[end_cell_str] + width = end_cell.column - start_cell.column + 1 + height = end_cell.row - start_cell.row + 1 + else: + start_cell = sheet[cell_range] + width = 1 + height = 1 + + return sheet_name, start_cell, height, width + + # Check if it's a Table (Excel table object, not just a named range) + for sheet_name in workbook.sheetnames: + sheet = workbook[sheet_name] + if hasattr(sheet, 'tables') and range_name in sheet.tables: + table = sheet.tables[range_name] + # Table has a 'ref' property with the range (e.g., "A4:F5") + # Excel tables can have header rows (default 1, but can be 0) + table_ref = table.ref + header_row_count = getattr(table, 'headerRowCount', 1) or 0 # 0 if None or False + + # Parse cell range (e.g., "A4:F5") + if ':' in table_ref: + start_cell_str, end_cell_str = table_ref.split(':') + start_cell_temp = sheet[start_cell_str] + end_cell = sheet[end_cell_str] + width = end_cell.column - start_cell_temp.column + 1 + total_height = end_cell.row - start_cell_temp.row + 1 + + # Skip header rows: point to first DATA row + if header_row_count > 0: + data_start_row = start_cell_temp.row + header_row_count + start_cell = sheet.cell(row=data_start_row, column=start_cell_temp.column) + else: + start_cell = start_cell_temp + + # Calculate data row count (total - headers) + height = total_height - header_row_count + else: + start_cell = sheet[table_ref] + width = 1 + height = 1 + + return sheet_name, start_cell, height, width + + return None + +# OBSOLETE: _update_named_range_height() removed +# This function was only called by the old openpyxl-based _process_sheet() implementation +# xlwings uses table.Resize() via COM API instead, which is more reliable +# See PHASE 2 migration notes for details + + +# OBSOLETE: _recalculate_workbook() removed - xlwings handles formula recalculation automatically +# When using xlwings with wb.save(), Excel automatically recalculates all formulas + +# OBSOLETE: _process_sheet() removed - openpyxl implementation migrated to xlwings +# All sheet processing is now handled by _process_sheet_xlwings() using xlwings library +# This eliminates code duplication and provides better preservation of workbook structure + + +def _get_table_dimensions_xlwings(workbook_xw, range_name): + """ + Get dimensions of an Excel table OR named range using xlwings COM API. + + First searches for ListObjects (structured tables), then falls back to + simple named ranges if no table is found. + + Args: + workbook_xw: xlwings Book object (already open) + range_name: Name of the Excel table (ListObject) or named range + + Returns: + Tuple (sheet_name, start_cell, height, width, header_row_count, target_type) or None if not found + - start_cell: Points to FIRST DATA ROW (after headers for tables, first row for named ranges) + - height: Number of DATA ROWS (excluding headers for tables) + - header_row_count: Number of header rows (0 for named ranges, 0 or 1 for tables) + - target_type: TARGET_TYPE_TABLE or TARGET_TYPE_NAMED_RANGE + + Note: + - For tables with headers: start_cell points to first data row (after header) + - For tables without headers: start_cell points to first row of table + - For named ranges: start_cell points to first row (no headers assumed) + """ + # Helper class to mimic openpyxl Cell behavior + class CellRef: + def __init__(self, row, column): + self.row = row + self.column = column + @property + def coordinate(self): + col_letter = get_column_letter(self.column) + return f"{col_letter}{self.row}" + + # === PRIORITY 1: Check if it's a table (ListObject) === + # Excel tables are more reliable than plain named ranges with xlwings + for sheet in workbook_xw.sheets: + sheet_api = sheet.api + + # Try to get the table count - if this fails, the sheet has no ListObjects property + try: + table_count = sheet_api.ListObjects.Count + except: + # Sheet doesn't support ListObjects or has none + continue + + # If no tables in this sheet, skip + if table_count == 0: + continue + + # Iterate through tables by index + for i in range(1, table_count + 1): # COM indexing starts at 1 + try: + xl_table = sheet_api.ListObjects.Item(i) + table_name = xl_table.Name + if table_name == range_name: + # Found a table - get its range + xl_range = xl_table.Range + sheet_name = sheet.name + total_rows = xl_range.Rows.Count + total_cols = xl_range.Columns.Count + start_row = xl_range.Row + start_col = xl_range.Column + + # Get header row count from the table + # In COM API, ListObject has ShowHeaders property (boolean) and HeaderRowRange + # ShowHeaders: True if table has header row, False if not + try: + has_headers = xl_table.ShowHeaders + header_row_count = 1 if has_headers else 0 + except: + # If ShowHeaders not accessible, try HeaderRowRange + try: + header_range = xl_table.HeaderRowRange + header_row_count = 1 if header_range is not None else 0 + except: + # Fallback: assume headers exist (most common case) + header_row_count = 1 + + # Data height = total height - header rows + data_height = total_rows - header_row_count + + # start_cell points to the FIRST DATA ROW (after headers) + # If table has headers: skip them. If no headers: start at table start + if header_row_count > 0: + data_start_row = start_row + header_row_count + else: + data_start_row = start_row + start_cell = CellRef(data_start_row, start_col) + + logging.info(f"[TABLE FOUND] Located table '{range_name}' at {sheet_name}!{start_cell.coordinate} " + f"(data rows: {data_height}, headers: {header_row_count}, total width: {total_cols})") + return sheet_name, start_cell, data_height, total_cols, header_row_count, TARGET_TYPE_TABLE + except Exception as e: + # Error accessing this specific table, skip it + logging.warning(f"Error accessing table {i} in '{sheet.name}': {type(e).__name__}") + + # === PRIORITY 2: Check if it's a named range === + # Named ranges don't have headers - data starts at first row + try: + if range_name in workbook_xw.names: + named_range = workbook_xw.names[range_name] + target_range = named_range.refers_to_range + + sheet_name = target_range.sheet.name + start_row = target_range.row + start_col = target_range.column + total_rows = target_range.rows.count + total_cols = target_range.columns.count + + # Named ranges have no headers - all rows are data rows + header_row_count = 0 + data_height = total_rows + + start_cell = CellRef(start_row, start_col) + + logging.info(f"[NAMED RANGE FOUND] Located named range '{range_name}' at {sheet_name}!{start_cell.coordinate} " + f"(data rows: {data_height}, no headers, total width: {total_cols})") + return sheet_name, start_cell, data_height, total_cols, header_row_count, TARGET_TYPE_NAMED_RANGE + except Exception as e: + logging.warning(f"Error accessing named range '{range_name}': {type(e).__name__}: {str(e)}") + + # Range/table not found + logging.warning(f"Named range or table '{range_name}' not found in workbook") + return None + + +# ============================================================================ +# HELPER FUNCTIONS FOR SHEET PROCESSING (extracted from _process_sheet_xlwings) +# ============================================================================ + +def _fill_variable_in_sheet(workbook_xw, target_name, source_template, template_vars, workbook_context=""): + """ + Fill a single variable cell with evaluated template value. + + Args: + workbook_xw: xlwings Book object + target_name: Name of the target named range (single cell) + source_template: Template string with {variables} + template_vars: Dictionary of variable values + workbook_context: Context string for logging + + Returns: + Boolean True if successful + """ + try: + # Evaluate template string + cell_value = source_template.format(**template_vars) + except KeyError as e: + logging.warning(f"Unknown variable in template: {e}") + return False + + # Write to named cell using xlwings + try: + named_range = workbook_xw.names[target_name] + target_range = named_range.refers_to_range + target_range.value = cell_value + logging.info(f"Set variable '{target_name}' to '{cell_value}'") + return True + except KeyError: + logging.warning(f"Named range '{target_name}' not found in {workbook_context}") + return False + except Exception as e: + logging.warning(f"Error setting variable '{target_name}' in {workbook_context}: {e}") + return False + + +def _prepare_table_data(source_type, source, sheet_config, inclusions_data, organizations_data, + inclusions_mapping_config, organizations_mapping_config, target_name): + """ + Prepare table data: select source, apply filter/sort, get column mapping. + + Args: + source_type: Type of source (Inclusions or Organizations) + source: Source identifier (mapping name) + sheet_config: Sheet configuration dictionary + inclusions_data: Inclusions data list + organizations_data: Organizations data list + inclusions_mapping_config: Inclusions mapping config + organizations_mapping_config: Organizations mapping config + target_name: Target range name (for logging) + + Returns: + Tuple of (sorted_data, column_mapping) or (None, None) if error + """ + # Select source data and mapping config + if source_type == SOURCE_TYPE_INCLUSIONS: + source_data = inclusions_data + mapping_config = inclusions_mapping_config + else: + source_data = organizations_data + mapping_config = organizations_mapping_config + + # Apply filter and sort + filter_condition = sheet_config.get("filter_condition") + sort_keys = sheet_config.get("sort_keys") + + filtered_data = [item for item in source_data if _apply_filter(item, filter_condition)] + sorted_data = _apply_sort(filtered_data, sort_keys) + + # Get column mapping + column_mapping = _get_column_mapping(mapping_config, source, source_type) + if not column_mapping: + logging.warning(f"Column mapping '{source}' not found or empty for {target_name}") + return None, None + + return sorted_data, column_mapping + + +def _resize_table_range(workbook_xw, sheet_xw, target_name, start_cell, max_col, start_row, num_data_rows, header_row_count=0, target_type=TARGET_TYPE_TABLE): + """ + Resize Excel table (ListObject) or named range to match data dimensions. + + For Tables (ListObjects): Uses ListObject.Resize() COM API + For Named Ranges: Redefines the named range via Name.RefersTo property + + Args: + workbook_xw: xlwings Book object (needed for named range resize) + sheet_xw: xlwings Sheet object + target_name: Name of the table/named range + start_cell: Starting cell (CellRef) - points to FIRST DATA ROW (after headers for tables) + max_col: Maximum column (1-based) + start_row: Starting row (1-based, first data row) + num_data_rows: Number of data rows + header_row_count: Number of header rows in the table (0 for named ranges) + target_type: TARGET_TYPE_TABLE or TARGET_TYPE_NAMED_RANGE + + Returns: + None (logging handles errors) + """ + if num_data_rows <= 1: + return + + try: + # Calculate the last data row + last_data_row = start_row + num_data_rows - 1 + + if target_type == TARGET_TYPE_TABLE: + # === TABLE (ListObject) RESIZE === + excel_sheet = sheet_xw.api + + # Find the ListObject (Table) by name + for list_obj in excel_sheet.ListObjects: + if list_obj.Name == target_name: + # If header_row_count not provided (legacy fallback), get it from the table + if header_row_count == 0: + try: + has_headers = list_obj.ShowHeaders + header_row_count = 1 if has_headers else 0 + except: + header_row_count = 1 + + # For resize, include header rows if they exist + if header_row_count > 0: + first_row = start_row - header_row_count + else: + first_row = start_row + + resize_range_str = f"{get_column_letter(start_cell.column)}{first_row}:{get_column_letter(max_col)}{last_data_row}" + + # Perform resize via ListObject.Resize() + new_range = excel_sheet.Range(resize_range_str) + list_obj.Resize(new_range) + logging.info(f"Resized table '{target_name}' to {resize_range_str} (header_rows={header_row_count})") + break + + elif target_type == TARGET_TYPE_NAMED_RANGE: + # === NAMED RANGE RESIZE === + # Redefine the named range to cover all data rows + # Named ranges have no headers, so start_row is the first row + first_col_letter = get_column_letter(start_cell.column) + last_col_letter = get_column_letter(max_col) + + # Build the range address in A1 style + range_address = f"${first_col_letter}${start_row}:${last_col_letter}${last_data_row}" + + # Get the actual Range object from the sheet and assign it to the Name + # This avoids R1C1/A1 format issues by using the Range object directly + new_range = sheet_xw.range(range_address) + workbook_xw.api.Names(target_name).RefersTo = new_range.api + logging.info(f"Resized named range '{target_name}' to {sheet_xw.name}!{range_address}") + + except Exception as e: + logging.warning(f"Resize skipped for {target_name} ({target_type}): {e}") + + +def _duplicate_template_row(sheet_xw, start_cell, max_col, start_row, num_data_rows, target_name, workbook_context=""): + """ + Duplicate template row to all data rows via copy-paste. + + Args: + sheet_xw: xlwings Sheet object + start_cell: Starting cell (CellRef) + max_col: Maximum column (1-based) + start_row: Starting row (1-based) + num_data_rows: Number of data rows + target_name: Target range name (for logging) + workbook_context: Context string for logging + + Returns: + None (logging handles errors) + """ + if num_data_rows <= 1: + return + + try: + # Replicate template row to all data rows in a single operation + template_range_str = f"{get_column_letter(start_cell.column)}{start_row}:{get_column_letter(max_col)}{start_row}" + last_data_row = start_row + num_data_rows - 1 + full_target_range_str = f"{get_column_letter(start_cell.column)}{start_row}:{get_column_letter(max_col)}{last_data_row}" + + # Copy template row + sheet_xw.range(template_range_str).copy() + # Paste to entire range - Excel automatically replicates the template row + sheet_xw.range(full_target_range_str).paste() + + # CRITICAL: Deselect after paste to avoid COM layer lock + # After bulk paste on large ranges (85k+ cells), Excel's COM layer becomes saturated + # and leaves a massive selection active. This prevents subsequent Range.Select() calls. + # Solution: Reset Excel's selection state by switching sheets and back, then select A1. + try: + from time import sleep + logging.info(f"Deselecting range after bulk paste for {target_name}...") + + # Switch to another sheet to force Excel to reset selection state + other_sheets = [s for s in sheet_xw.book.sheets if s.name != sheet_xw.name] + if other_sheets: + other_sheets[0].activate() + sleep(0.1) + + # Reactivate our sheet - Excel resets selection management when returning + sheet_xw.activate() + + # Select A1 - COM should manage this easily now + sheet_xw.api.Range("$A$1").Select() + logging.info(f"Successfully deselected after bulk paste for {target_name} (sheet reactivation)") + + except Exception as e: + # Deselection is non-critical, log and continue if it fails + logging.warning(f"Deselection after paste failed for {target_name}: {type(e).__name__}: {str(e)}") + + except Exception as e: + logging.warning(f"Template duplication failed for {target_name} in {workbook_context}: {e}") + + +def _fill_table_with_data(sheet_xw, start_cell, start_row, start_col, sorted_data, column_mapping, + value_replacement, target_name, sheet_name): + """ + Fill table with data: group contiguous columns and transfer via bulk 2D arrays. + + Args: + sheet_xw: xlwings Sheet object + start_cell: Starting cell (CellRef) + start_row: Starting row (1-based) + start_col: Starting column (1-based) + sorted_data: Sorted list of data items + column_mapping: Dict mapping Excel column indices to source field paths + value_replacement: Value replacement configuration (or None) + target_name: Target range name (for logging) + sheet_name: Sheet name (for logging) + + Returns: + None (logging handles errors and success) + """ + try: + # === Prepare column mapping and group contiguous columns === + col_order = sorted(column_mapping.keys()) + + # Group contiguous columns for optimal bulk update + contiguous_groups = [] + if col_order: + current_group = [col_order[0]] + for i in range(1, len(col_order)): + if col_order[i] == col_order[i-1] + 1: + current_group.append(col_order[i]) + else: + contiguous_groups.append(current_group) + current_group = [col_order[i]] + contiguous_groups.append(current_group) + + # === Update contiguous column groups (bulk 2D transfer) === + for col_group in contiguous_groups: + # Build 2D array for this group: rows × columns + data_2d = [] + for item in sorted_data: + row_values = [] + for excel_col_index in col_group: + source_field_path = column_mapping[excel_col_index] + # Get value from source item + value = get_nested_value(item, source_field_path.split(".")) + + # Apply value replacement + if value_replacement: + value = _apply_value_replacement(value, value_replacement) + + row_values.append(value) + data_2d.append(row_values) + + # Transfer entire group to Excel in ONE operation + first_col_in_group = start_col + col_group[0] + first_col_letter = get_column_letter(first_col_in_group) + target_range_start = f"{first_col_letter}{start_row}" + + # Write 2D array at once (xlwings automatically maps rows × columns) + sheet_xw.range(target_range_start).value = data_2d + + # Logging + num_data_rows = len(sorted_data) + logging.info(f"Filled table {target_name} with {num_data_rows} rows " + f"at {sheet_name}!{start_cell.coordinate} " + f"(bulk duplication + {len(contiguous_groups)} contiguous group(s))") + except Exception as e: + logging.error(f"Error filling table data for {target_name}: {e}") + logging.error(f"Traceback: {traceback.format_exc()}") + + +def _process_sheet_xlwings(workbook_xw, sheet_config, inclusions_data, organizations_data, + inclusions_mapping_config, organizations_mapping_config, template_vars, + workbook_context=""): + """ + Process a single sheet using xlwings (hybrid approach). + + Delegates to specialized helpers to maintain clarity and testability. + + Args: + workbook_xw: xlwings Book object + sheet_config: Sheet configuration dict + inclusions_data: List of inclusion dictionaries + organizations_data: List of organization dictionaries + inclusions_mapping_config: Inclusions mapping config (for column mapping) + organizations_mapping_config: Organizations mapping config + template_vars: Dictionary of variables for template evaluation + workbook_context: Context string identifying the workbook (for logging) + + Returns: + Boolean True if successful + """ + source_type = sheet_config.get("source_type") + source = sheet_config.get("source") + target_name = sheet_config.get("target_name") + value_replacement = sheet_config.get("value_replacement") + + # === Variable sources: single cell fill === + if source_type == SOURCE_TYPE_VARIABLE: + return _fill_variable_in_sheet(workbook_xw, target_name, source, template_vars, workbook_context) + + # === Table sources: bulk data filling === + if source_type not in [SOURCE_TYPE_INCLUSIONS, SOURCE_TYPE_ORGANIZATIONS]: + return False + + # Prepare data: filter, sort, get column mapping + sorted_data, column_mapping = _prepare_table_data( + source_type, source, sheet_config, inclusions_data, organizations_data, + inclusions_mapping_config, organizations_mapping_config, target_name + ) + if sorted_data is None or column_mapping is None: + return False + + # Get table/named range dimensions from xlwings + try: + table_dims = _get_table_dimensions_xlwings(workbook_xw, target_name) + if not table_dims: + logging.warning(f"Target '{target_name}' not found (neither table nor named range)") + return False + + sheet_name, start_cell, table_height, table_width, header_row_count, target_type = table_dims + sheet_xw = workbook_xw.sheets[sheet_name] + start_row = start_cell.row + start_col = start_cell.column + max_col = start_col + table_width - 1 + num_data_rows = len(sorted_data) + + # === Bulk operations for data filling === + if sorted_data: + # STEP 0: Resize table/named range to match data dimensions + _resize_table_range(workbook_xw, sheet_xw, target_name, start_cell, max_col, start_row, num_data_rows, header_row_count, target_type) + + # STEP 1: Duplicate template row to all data rows + _duplicate_template_row(sheet_xw, start_cell, max_col, start_row, num_data_rows, target_name, workbook_context) + + # STEP 2-3: Fill with data (grouped contiguous columns) + _fill_table_with_data(sheet_xw, start_cell, start_row, start_col, sorted_data, + column_mapping, value_replacement, target_name, sheet_name) + else: + # No data - template row stays empty + logging.info(f"No data for target '{target_name}' ({target_type}), leaving template row empty") + + return True + + except Exception as e: + logging.warning(f"Error processing target '{target_name}': {e}") + logging.error(f"Traceback: {traceback.format_exc()}") + return False + + +# ============================================================================ +# COMPREHENSIVE EXCEL EXPORT ORCHESTRATION (for main script) +# ============================================================================ + +def prepare_excel_export(inclusions_mapping_config, organizations_mapping_config): + """ + Validate Excel export configuration (no data loading). + + This function has a SINGLE responsibility: validate the Excel export CONFIG. + It does NOT load production data (JSONs) - that is the responsibility of + the execution functions (run_normal_mode_export, export_excel_only). + + IMPORTANT: Mapping configs MUST be provided by the caller. The caller is responsible for: + 1. Loading mapping configs from Excel (e.g., via load_inclusions_mapping_config()) + 2. Passing them to this function for config validation + + This follows the dependency injection pattern: the caller provides dependencies, + this function validates config. This ensures: + - Clear responsibility separation: validation ≠ data loading + - Early CONFIG validation (BEFORE data collection in NORMAL MODE) + - Late DATA loading (AFTER collection, only when needed for execution) + + Args: + inclusions_mapping_config: Loaded inclusions mapping (required, non-empty list/dict) + organizations_mapping_config: Loaded organizations mapping (required, non-empty list/dict) + + Returns: + Tuple of (excel_config, has_critical_errors, error_messages) + - excel_config: Tuple of (workbooks_config, sheets_config) or None if errors + - has_critical_errors: Boolean True if validation found critical errors + - error_messages: List of error message strings + + Note: + JSONs are loaded separately by execution functions: + - NORMAL MODE: run_normal_mode_export() loads JSONs AFTER data collection + - --EXCEL-ONLY: export_excel_only() loads JSONs before execution + """ + + error_messages = [] + excel_config = None + has_critical_errors = False + + # === STEP 1: Validate mapping configurations are provided === + # Caller is responsible for loading these configs before calling this function + if not inclusions_mapping_config or (isinstance(inclusions_mapping_config, (list, dict)) and len(inclusions_mapping_config) == 0): + error_msg = "Inclusions mapping configuration must be provided and non-empty" + error_messages.append(error_msg) + logging.error(error_msg) + if console: + console.print(f"[bold red]✗ {error_msg}[/bold red]") + has_critical_errors = True + return excel_config, has_critical_errors, error_messages + + if not organizations_mapping_config or (isinstance(organizations_mapping_config, (list, dict)) and len(organizations_mapping_config) == 0): + error_msg = "Organizations mapping configuration must be provided and non-empty" + error_messages.append(error_msg) + logging.error(error_msg) + if console: + console.print(f"[bold red]✗ {error_msg}[/bold red]") + has_critical_errors = True + return excel_config, has_critical_errors, error_messages + + # === STEP 2: Load Excel config === + logging.info("Loading Excel export configuration...") + + excel_workbooks_config, excel_sheets_config, has_config_error, config_error_messages = load_excel_export_config(console) + if has_config_error: + error_msg = "Critical errors in Excel Export Config" + error_messages.append(error_msg) + error_messages.extend(config_error_messages) + has_critical_errors = True + logging.warning(error_msg) + if console: + console.print(f"[bold red]✗ {error_msg}[/bold red]") + excel_config = (excel_workbooks_config, excel_sheets_config) + return excel_config, has_critical_errors, error_messages + + if not excel_workbooks_config or not excel_sheets_config: + error_msg = "Excel export configuration is empty" + error_messages.append(error_msg) + logging.warning(error_msg) + if console: + console.print(f"[bold red]✗ {error_msg}[/bold red]") + excel_config = (excel_workbooks_config, excel_sheets_config) + return excel_config, has_critical_errors, error_messages + + # Package config into tuple for downstream functions + excel_config = (excel_workbooks_config, excel_sheets_config) + + # === STEP 3: Validate Excel config === + logging.info("Validating Excel export configuration...") + + has_critical_errors, validation_errors = validate_excel_config( + excel_config, + console, + inclusions_mapping_config or [], + organizations_mapping_config or {} + ) + + if validation_errors: + error_messages.extend(validation_errors) + if has_critical_errors and console: + console.print("[bold red]✗ Critical validation errors found[/bold red]") + else: + logging.info("✓ Excel export configuration validated successfully") + + return excel_config, has_critical_errors, error_messages + + +# ============================================================================ +# HIGH-LEVEL ORCHESTRATION FUNCTIONS (for main script integration) +# ============================================================================ + +def export_excel_only(sys_argv, + inclusions_filename=None, organizations_filename=None, + inclusions_mapping_config=None, organizations_mapping_config=None): + """ + Orchestrates EXCEL_ONLY mode - complete end-to-end Excel export workflow. + + This function completely encapsulates the --excel_only mode: + 1. Validates Excel configuration + 2. Loads JSON data files (must exist) + 3. Executes Excel export with error handling + 4. Displays user-friendly messages and confirmations + + IMPORTANT: The caller (main script) is responsible for loading mapping configs + before calling this function. This ensures consistent config instances across + the application and follows the dependency injection pattern. + + This follows the same pattern as run_check_only_mode() from quality_checks module. + + Args: + sys_argv: sys.argv from main script (for potential future CLI arg parsing) + inclusions_filename: Name of inclusions JSON file (e.g., "endobest_inclusions.json") + organizations_filename: Name of organizations JSON file (e.g., "endobest_organizations.json") + inclusions_mapping_config: Loaded inclusions mapping configuration (REQUIRED - caller must load) + organizations_mapping_config: Loaded organizations mapping configuration (REQUIRED - caller must load) + """ + global console + + if not inclusions_filename: + inclusions_filename = INCLUSIONS_FILE_NAME + if not organizations_filename: + organizations_filename = ORGANIZATIONS_FILE_NAME + + print() + console.print("[bold cyan]═══ EXCEL ONLY MODE ═══[/bold cyan]\n") + + # Step 1: Validate Excel configuration (no data loading) + logging.info("EXCEL ONLY MODE: Validating Excel configuration") + excel_config, has_config_critical, error_messages = \ + prepare_excel_export(inclusions_mapping_config, organizations_mapping_config) + + # Step 2: Handle critical configuration errors + if has_config_critical: + print() + console.print("[bold red]⚠ CRITICAL CONFIGURATION ERROR(S) DETECTED[/bold red]") + console.print("[bold red]────────────────────────────────────[/bold red]") + for idx, error_msg in enumerate(error_messages, 1): + console.print(f"[bold red]Error {idx}: {error_msg}[/bold red]") + console.print("[bold red]────────────────────────────────────[/bold red]") + print() + try: + import questionary + answer = questionary.confirm( + "⚠ Continue anyway?", + default=False + ).ask() + if not answer: + console.print("[bold red]Aborted by user[/bold red]") + logging.warning("EXCEL ONLY MODE: Aborted by user due to critical errors") + return + except ImportError: + console.print("[bold yellow]⚠ questionary not available for confirmation[/bold yellow]") + console.print("[bold yellow]Proceeding with export despite critical errors[/bold yellow]") + + # Step 3: Load JSON data files (must exist in --excel-only mode) + logging.info("EXCEL ONLY MODE: Loading data files") + inclusions_data = _load_json_file_internal(inclusions_filename) + organizations_data = _load_json_file_internal(organizations_filename) + + if inclusions_data is None or organizations_data is None: + console.print("[bold red]✗ Error: Could not load data files for Excel export[/bold red]") + logging.error("EXCEL ONLY MODE: Data file loading failed") + return + + # Step 4: Execute Excel export (direct call to export_to_excel, console is global) + print() + console.print("[bold cyan]═══ Excel Export ═══[/bold cyan]\n") + logging.info("EXCEL ONLY MODE: Executing export") + + if excel_config: + try: + logging.info(f"Starting Excel export: {len(inclusions_data)} inclusions, {len(organizations_data)} organizations") + + success, error_count = export_to_excel( + inclusions_data, + organizations_data, + excel_config, + inclusions_mapping_config=inclusions_mapping_config, + organizations_mapping_config=organizations_mapping_config + ) + + if success: + logging.info("EXCEL ONLY MODE: Export completed successfully") + else: + logging.warning(f"EXCEL ONLY MODE: Export completed with {error_count} error(s)") + except Exception as e: + error_msg = f"Excel export failed: {str(e)}" + logging.error(f"EXCEL ONLY MODE: {error_msg}\n{traceback.format_exc()}") + console.print(f"[bold red]✗ {error_msg}[/bold red]\n") + else: + console.print("[bold red]✗ Could not load Excel configuration[/bold red]\n") + logging.error("EXCEL ONLY MODE: Excel config missing") + + +def run_normal_mode_export(excel_enabled, excel_config, + inclusions_mapping_config=None, organizations_mapping_config=None): + """ + Orchestrates Excel export during normal mode execution. + + This function encapsulates the Excel export step that runs after inclusions and organizations + have been collected and written to JSON files. It handles: + - Loading JSONs from filesystem (ensures fresh data consistency) + - Executing Excel export with comprehensive error handling + - Displaying results to user + + This is called from the normal workflow after data collection completes. + + Args: + excel_enabled: Boolean indicating if Excel export is enabled + excel_config: Tuple of (workbooks_config, sheets_config) or None + inclusions_mapping_config: Loaded inclusions mapping configuration (optional) + organizations_mapping_config: Loaded organizations mapping configuration (optional) + + Note: + This function loads JSON files from the filesystem (which were written + during the data collection phase) to ensure consistency. + + Returns: + Tuple of (export_succeeded, error_message) + - export_succeeded: Boolean True if export completed successfully (or skipped) + - error_message: String with error details (empty if success=True or skipped) + """ + global console + + # Only proceed if export is enabled and config is available + if not excel_enabled or not excel_config: + logging.info("Excel export not enabled or config missing, skipping") + return True, "" # FIX BUG #3: Return True when export is intentionally skipped (not an error) + + print() + console.print("[bold cyan]═══ Excel Export ═══[/bold cyan]\n") + logging.info("NORMAL MODE: Starting Excel export") + + try: + # Load JSONs from filesystem to ensure data consistency with what was written + # Use constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH) + inclusions_from_fs = _load_json_file_internal(INCLUSIONS_FILE_NAME) + organizations_from_fs = _load_json_file_internal(ORGANIZATIONS_FILE_NAME) + + if inclusions_from_fs is None or organizations_from_fs is None: + error_msg = "Could not load data files for Excel export" + logging.error(f"NORMAL MODE: {error_msg}") + console.print(f"[bold red]✗ {error_msg}[/bold red]\n") + return False, error_msg + + # Execute the export (direct call to export_to_excel, console is global) + logging.info(f"Starting Excel export: {len(inclusions_from_fs)} inclusions, {len(organizations_from_fs)} organizations") + + success, error_count = export_to_excel( + inclusions_from_fs, + organizations_from_fs, + excel_config, + inclusions_mapping_config=inclusions_mapping_config, + organizations_mapping_config=organizations_mapping_config + ) + + if success: + logging.info("NORMAL MODE: Excel export completed successfully") + return True, "" + else: + error_msg = f"Excel export completed with {error_count} error(s)" + logging.warning(f"NORMAL MODE: {error_msg}") + return False, error_msg + + except Exception as e: + error_msg = f"Unexpected error during Excel export: {str(e)}" + logging.error(f"NORMAL MODE: {error_msg}\n{traceback.format_exc()}") + console.print(f"[bold red]✗ {error_msg}[/bold red]\n") + return False, error_msg + + +def _load_json_file_internal(filename): + """ + Internal helper to load JSON file. + + Args: + filename: Path to JSON file + + Returns: + Parsed JSON data or None if file doesn't exist or can't be parsed + """ + try: + if not os.path.exists(filename): + logging.warning(f"JSON file not found: {filename}") + return None + + with open(filename, 'r', encoding='utf-8') as f: + data = json.load(f) + logging.info(f"Loaded {filename}: {len(data) if isinstance(data, list) else 'data'}") + return data + + except Exception as e: + logging.error(f"Error loading {filename}: {str(e)}") + return None diff --git a/eb_dashboard_excel_only-exe.bat b/eb_dashboard_excel_only-exe.bat new file mode 100644 index 0000000..e30948a --- /dev/null +++ b/eb_dashboard_excel_only-exe.bat @@ -0,0 +1,3 @@ +@echo off +eb_dashboard.exe --excel-only %* + diff --git a/eb_dashboard_excel_only.bat b/eb_dashboard_excel_only.bat new file mode 100644 index 0000000..5854ac9 --- /dev/null +++ b/eb_dashboard_excel_only.bat @@ -0,0 +1,4 @@ +@echo off +call C:\PythonProjects\.rcvenv\Scripts\activate.bat +python eb_dashboard.py --excel-only %* + diff --git a/eb_dashboard_quality_checks.py b/eb_dashboard_quality_checks.py new file mode 100644 index 0000000..95bbf56 --- /dev/null +++ b/eb_dashboard_quality_checks.py @@ -0,0 +1,1313 @@ +""" +Endobest Dashboard - Quality Checks Module + +This module contains all quality assurance functions: +- JSON file loading and backup utilities +- Coherence checks between organization statistics and detailed inclusion data +- Comprehensive non-regression checks with configurable rules +- Config-driven validation with Warning/Critical thresholds +- Support for special rules (New/Deleted Inclusions, New/Deleted Fields) +- 4-step logic for normal rules (field selection, transition matching, exception application, bloc_scope) +""" + +import json +import logging +import os +import shutil + +import openpyxl +from rich.console import Console +from eb_dashboard_utils import get_nested_value, get_old_filename as _get_old_filename, get_config_path +from eb_dashboard_constants import ( + INCLUSIONS_FILE_NAME, + ORGANIZATIONS_FILE_NAME, + OLD_FILE_SUFFIX, + DASHBOARD_CONFIG_FILE_NAME, + REGRESSION_CHECK_TABLE_NAME +) + + +# ============================================================================ +# MODULE CONFIGURATION +# ============================================================================ + +# Debug mode: Set to True to display detailed changes for each regression check rule +# (Variable globale - mutée au runtime, pas une constante) +debug_mode = False + + +def enable_debug_mode(): + """Enable debug mode to display detailed changes for each regression check rule.""" + global debug_mode + debug_mode = True + if console: + console.print("[dim]DEBUG MODE enabled - detailed changes will be displayed[/dim]") + + +# ============================================================================ +# MODULE DEPENDENCIES (injected from main module) +# ============================================================================ + +# Will be injected by the main module +console = None + +# Regression check config is loaded on-demand via load_regression_check_config() +regression_check_config = [] + +# NOTE: File names and table names are imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH): +# - INCLUSIONS_FILE_NAME +# - ORGANIZATIONS_FILE_NAME +# - OLD_FILE_SUFFIX +# - DASHBOARD_CONFIG_FILE_NAME +# - REGRESSION_CHECK_TABLE_NAME + + +def set_dependencies(console_instance): + """ + Inject console instance from main module. + + Args: + console_instance: Rich Console instance for formatted output + + Note: + - File and table names are imported directly from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH) + - Regression check config is loaded on-demand via load_regression_check_config() + """ + global console + console = console_instance + + +# ============================================================================ +# CONFIGURATION LOADING +# ============================================================================ + +def load_regression_check_config(console_instance=None): + """Loads and validates the regression check configuration from the Excel file. + + Args: + console_instance: Optional Rich Console instance. If not provided, uses global console. + """ + global regression_check_config, console + + # Use provided console or fall back to global + if console_instance: + console = console_instance + + config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME) + + try: + workbook = openpyxl.load_workbook(config_path) + except FileNotFoundError: + error_msg = f"Error: Configuration file not found at: {config_path}" + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + if REGRESSION_CHECK_TABLE_NAME not in workbook.sheetnames: + error_msg = f"Error: Sheet '{REGRESSION_CHECK_TABLE_NAME}' not found in the configuration file." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + sheet = workbook[REGRESSION_CHECK_TABLE_NAME] + headers = [cell.value for cell in sheet[1]] + + temp_config = [] + + for row_index, row in enumerate(sheet.iter_rows(min_row=2, values_only=True), start=2): + rule_config = dict(zip(headers, row)) + + # Skip if ignore column contains "ignore" (case insensitive) + ignore_value = rule_config.get("ignore") + if ignore_value and isinstance(ignore_value, str) and "ignore" in ignore_value.lower(): + continue + + # Skip if all columns are None (empty row) + if all(value is None for value in row): + continue + + # Validate bloc_title and line_label + bloc_title = rule_config.get("bloc_title") + line_label = rule_config.get("line_label") + + if not bloc_title or not isinstance(bloc_title, str): + continue # Skip rows without bloc_title (header separators, etc.) + + if not line_label or not isinstance(line_label, str): + error_msg = f"Error in Regression_Check config, row {row_index}: 'line_label' is mandatory when 'bloc_title' is specified." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + # Validate thresholds + warning_threshold = rule_config.get("warning_threshold") + critical_threshold = rule_config.get("critical_threshold") + + if warning_threshold is None or not isinstance(warning_threshold, (int, float)) or warning_threshold < 0: + error_msg = f"Error in Regression_Check config, row {row_index}: 'warning_threshold' must be a number >= 0." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + if critical_threshold is None or not isinstance(critical_threshold, (int, float)) or critical_threshold < 0: + error_msg = f"Error in Regression_Check config, row {row_index}: 'critical_threshold' must be a number >= 0." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + # Parse JSON fields + for json_field in ["field_selection", "transitions"]: + value = rule_config.get(json_field) + if value and isinstance(value, str): + try: + rule_config[json_field] = json.loads(value) + except json.JSONDecodeError: + error_msg = f"Error in Regression_Check config, row {row_index}, field '{json_field}': Invalid JSON format." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + elif value is None: + rule_config[json_field] = None + + # Validate field_selection format + line_label = rule_config.get("line_label") + field_selection = rule_config.get("field_selection") + + # Special rules that don't use field_selection + special_rules_no_selection = ["New Fields", "Deleted Fields", "Deleted Inclusions"] + + if line_label not in special_rules_no_selection: + # Standard rules and "New Inclusions" MUST have field_selection + if field_selection is None: + error_msg = f"Error in Regression_Check config, row {row_index}: 'field_selection' is mandatory for rule '{line_label}'." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + if not isinstance(field_selection, list): + console.print(f"[yellow]⚠ Row {row_index}: 'field_selection' must be a JSON array of [action, selector] pairs, skipping rule[/yellow]") + rule_config["_config_error"] = True + else: + # Validate each field_selection step + for step_idx, step in enumerate(field_selection): + if not isinstance(step, list) or len(step) != 2: + console.print(f"[yellow]⚠ Row {row_index}: field_selection[{step_idx}] must be array of 2 elements [action, selector], skipping rule[/yellow]") + rule_config["_config_error"] = True + break + + action, field_selector = step + + if action not in ["include", "exclude"]: + console.print(f"[yellow]⚠ Row {row_index}: field_selection[{step_idx}] action must be 'include' or 'exclude', got '{action}', skipping rule[/yellow]") + rule_config["_config_error"] = True + break + + if not isinstance(field_selector, str) or "." not in field_selector: + console.print(f"[yellow]⚠ Row {row_index}: field_selection[{step_idx}] selector must be string with dot notation (e.g., '*.*', 'group.*', 'group.field'), got '{field_selector}', skipping rule[/yellow]") + rule_config["_config_error"] = True + break + else: + # Special rules should have empty field_selection + if field_selection is not None and field_selection != [] and field_selection != "": + console.print(f"[yellow]⚠ Row {row_index}: Special rule '{line_label}' should have empty field_selection, got {field_selection}[/yellow]") + rule_config["_config_error"] = True + + # Validate bloc_scope + bloc_scope = rule_config.get("bloc_scope") + if bloc_scope is not None and bloc_scope not in ["all", "any"]: + error_msg = f"Error in Regression_Check config, row {row_index}: 'bloc_scope' must be 'all' or 'any'." + logging.critical(error_msg) + console.print(f"[bold red]{error_msg}[/bold red]") + raise Exception(error_msg) + + # Validate transitions format (new pipeline format) + # Format: [["include"/"exclude", "field_selector", "from_pattern", "to_pattern"], ...] + transitions = rule_config.get("transitions") + config_error = False + + if transitions is not None: + if not isinstance(transitions, list): + console.print(f"[yellow]⚠ Row {row_index}: 'transitions' must be a JSON array, skipping this rule[/yellow]") + config_error = True + else: + # Validate each transition step + for step_idx, transition_step in enumerate(transitions): + if not isinstance(transition_step, list) or len(transition_step) != 4: + console.print(f"[yellow]⚠ Row {row_index}: transitions[{step_idx}] must be array of 4 elements [action, field_selector, from, to], skipping[/yellow]") + config_error = True + break + + action, field_selector, from_val, to_val = transition_step + + if action not in ["include", "exclude"]: + console.print(f"[yellow]⚠ Row {row_index}: transitions[{step_idx}] action must be 'include' or 'exclude', got '{action}', skipping[/yellow]") + config_error = True + break + + if not isinstance(field_selector, str) or "." not in field_selector: + console.print(f"[yellow]⚠ Row {row_index}: transitions[{step_idx}] field_selector must be string with dot notation (e.g., '*.*', 'group.*', 'group.field'), got '{field_selector}', skipping[/yellow]") + config_error = True + break + + if config_error: + rule_config["_config_error"] = True + + temp_config.append(rule_config) + + regression_check_config = temp_config + console.print(f"Loaded {len(regression_check_config)} regression check rules.", style="green") + + +def run_check_only_mode(sys_argv): + """ + Orchestrates CHECK_ONLY and CHECK_ONLY_COMPARE modes. + + This function handles the complete workflow for both CHECK_ONLY modes: + - CHECK_ONLY: Full validation (coherence + regression) on existing files + - CHECK_ONLY_COMPARE: Regression-only comparison of two specific files + + Args: + sys_argv: sys.argv from main script (to parse command-line arguments) + """ + global console + + # Initialize console if not already set + if console is None: + console = Console() + + print() + + # Detect CHECK_ONLY_COMPARE mode: --check-only + if len(sys_argv) >= 4: + # CHECK_ONLY_COMPARE mode: Compare two specific files + current_file = sys_argv[2] + old_file = sys_argv[3] + + console.print("[bold cyan]═══ CHECK ONLY COMPARE MODE ═══[/bold cyan]") + console.print(f"Comparing two specific files without coherence check:\n") + console.print(f" Current: [bold]{current_file}[/bold]") + console.print(f" Old: [bold]{old_file}[/bold]\n") + + # Load only regression check configuration + print() + load_regression_check_config(console) + + # Run quality checks with coherence check skipped + print() + has_coherence_critical, has_regression_critical = run_quality_checks( + current_inclusions=current_file, + organizations_list=None, + old_inclusions_filename=old_file, + skip_coherence=True + ) + + # Display summary + if has_regression_critical: + console.print("[bold red]✗ CRITICAL issues detected![/bold red]") + else: + console.print("[bold green]✓ All checks passed successfully![/bold green]") + + else: + # Standard CHECK_ONLY mode: Full validation with coherence + regression + console.print("[bold cyan]═══ CHECK ONLY MODE ═══[/bold cyan]") + console.print("Running quality checks on existing data files without collecting new data.\n") + + # Load regression check configuration (coherence check doesn't need extended fields) + print() + load_regression_check_config(console) + + # Run quality checks (will load all files internally) + print() + old_inclusions_file = _get_old_filename(INCLUSIONS_FILE_NAME, OLD_FILE_SUFFIX) + has_coherence_critical, has_regression_critical = run_quality_checks( + current_inclusions=INCLUSIONS_FILE_NAME, + organizations_list=ORGANIZATIONS_FILE_NAME, + old_inclusions_filename=old_inclusions_file + ) + + # Display summary + if has_coherence_critical or has_regression_critical: + console.print("[bold red]✗ CRITICAL issues detected![/bold red]") + else: + console.print("[bold green]✓ All checks passed successfully![/bold green]") + + +# ============================================================================ +# FILE UTILITIES +# ============================================================================ + +def load_json_file(filename): + """ + Loads a JSON file (inclusions, organizations, or any JSON data). + Returns the parsed JSON data or None if file doesn't exist or error occurred. + + Args: + filename: Path to the JSON file to load. + + Returns: + Parsed JSON data (list, dict, etc.) or None if file not found or error occurred. + """ + if os.path.exists(filename): + try: + with open(filename, 'r', encoding='utf-8') as f: + return json.load(f) + except Exception as e: + logging.warning(f"Could not load JSON file '{filename}': {e}") + console.print(f"[yellow]⚠ Warning: Could not load JSON file '{filename}': {e}[/yellow]") + return None + + +def backup_output_files(): + """ + Silently backups current output files before writing new versions. + This is called AFTER all checks pass to avoid losing history on crash. + """ + def _backup_file_silent(source, destination): + """Internal: Silently backup a file if it exists, overwriting destination.""" + if os.path.exists(source): + try: + shutil.copy2(source, destination) + except Exception as e: + logging.warning(f"Could not backup {source}: {e}") + + _backup_file_silent(INCLUSIONS_FILE_NAME, _get_old_filename(INCLUSIONS_FILE_NAME, OLD_FILE_SUFFIX)) + _backup_file_silent(ORGANIZATIONS_FILE_NAME, _get_old_filename(ORGANIZATIONS_FILE_NAME, OLD_FILE_SUFFIX)) + + +# ============================================================================ +# COHERENCE CHECK +# ============================================================================ + +def coherence_check(output_inclusions, organizations_list): + """ + Checks coherence between organization statistics and actual inclusion details. + Displays results with color-coded status. + Returns True if any critical issue was found, False otherwise. + """ + has_critical = False # Track critical status + + def _get_status_and_style(count, warning_threshold=None, critical_threshold=None): + """Internal: Determine status level and visual style.""" + nonlocal has_critical + if critical_threshold is not None and count > critical_threshold: + has_critical = True + return "CRITICAL", "red", "✗" + elif warning_threshold is not None and count > warning_threshold: + return "WARNING", "yellow", "⚠" + else: + return "OK", "green", "✓" + + def _print_check_line(message, count=None, status_tuple=None, indent=0): + """Internal: Print a formatted check line with emoji and color.""" + indent_str = " " * indent + if status_tuple: + status, color, emoji = status_tuple + if count is not None: + console.print(f"{indent_str}{emoji} [{color}]{message}: {count}[/{color}]") + else: + console.print(f"{indent_str}{emoji} [{color}]{message}[/{color}]") + else: + console.print(f"{indent_str}{message}") + + def _calculate_detail_counters_with_ap(inclusions_list, org_id=None): + """Internal: Calculate actual counters from inclusions detail with AP (prematurely terminated) handling. + + Rules: + - If status ends with ' - AP': increment prematurely_terminated + - Else if starts with 'pré-incluse': increment preincluded + - Else if starts with 'incluse': increment included + - Always increment patients + """ + patients = 0 + preincluded = 0 + included = 0 + prematurely_terminated = 0 + + for inclusion in inclusions_list: + # Filter by organization if specified + if org_id: + inc_org_id = get_nested_value(inclusion, ["Patient_Identification", "Organisation_Id"]) + if inc_org_id != org_id: + continue + + patients += 1 + status = get_nested_value(inclusion, ["Inclusion", "Inclusion_Status"], default="") + + if isinstance(status, str): + # Check if status ends with ' - AP' (prematurely terminated) + if status.endswith(" - AP"): + prematurely_terminated += 1 + # Otherwise apply the normal classification + elif status.lower().startswith("pré-incluse"): + preincluded += 1 + elif status.lower().startswith("incluse"): + included += 1 + + return patients, preincluded, included, prematurely_terminated + + # Main coherence check logic + console.print("\n[bold]═══ Coherence Check ═══[/bold]\n") + + # Calculate total counters + total_stats = { + 'patients': sum(org.get('patients_count', 0) for org in organizations_list), + 'preincluded': sum(org.get('preincluded_count', 0) for org in organizations_list), + 'included': sum(org.get('included_count', 0) for org in organizations_list), + 'prematurely_terminated': sum(org.get('prematurely_terminated_count', 0) for org in organizations_list) + } + + total_detail_tuple = _calculate_detail_counters_with_ap(output_inclusions) + total_detail = { + 'patients': total_detail_tuple[0], + 'preincluded': total_detail_tuple[1], + 'included': total_detail_tuple[2], + 'prematurely_terminated': total_detail_tuple[3] + } + + # Check total (4 counters must match) + total_ok = (total_stats['patients'] == total_detail['patients'] and + total_stats['preincluded'] == total_detail['preincluded'] and + total_stats['included'] == total_detail['included'] and + total_stats['prematurely_terminated'] == total_detail['prematurely_terminated']) + + total_status = _get_status_and_style(0 if total_ok else 1, 0, 0) + + message = (f"TOTAL - Stats({total_stats['patients']}/{total_stats['preincluded']}/{total_stats['included']}/{total_stats['prematurely_terminated']}) " + f"vs Detail({total_detail['patients']}/{total_detail['preincluded']}/{total_detail['included']}/{total_detail['prematurely_terminated']})") + _print_check_line(message, status_tuple=total_status, indent=0) + + # Check each organization (only display if not OK) + for org in organizations_list: + org_id = org.get('id') + org_name = org.get('name', 'Unknown') + + org_stats = { + 'patients': org.get('patients_count', 0), + 'preincluded': org.get('preincluded_count', 0), + 'included': org.get('included_count', 0), + 'prematurely_terminated': org.get('prematurely_terminated_count', 0) + } + + org_detail_tuple = _calculate_detail_counters_with_ap(output_inclusions, org_id) + org_detail = { + 'patients': org_detail_tuple[0], + 'preincluded': org_detail_tuple[1], + 'included': org_detail_tuple[2], + 'prematurely_terminated': org_detail_tuple[3] + } + + org_ok = (org_stats['patients'] == org_detail['patients'] and + org_stats['preincluded'] == org_detail['preincluded'] and + org_stats['included'] == org_detail['included'] and + org_stats['prematurely_terminated'] == org_detail['prematurely_terminated']) + + if not org_ok: + org_status = _get_status_and_style(1, 0, 0) + message = (f"{org_name} - Stats({org_stats['patients']}/{org_stats['preincluded']}/{org_stats['included']}/{org_stats['prematurely_terminated']}) " + f"vs Detail({org_detail['patients']}/{org_detail['preincluded']}/{org_detail['included']}/{org_detail['prematurely_terminated']})") + _print_check_line(message, status_tuple=org_status, indent=1) + + return has_critical + + +# ============================================================================ +# QUALITY CHECKS ORCHESTRATION +# ============================================================================ + +def run_quality_checks(current_inclusions, organizations_list, old_inclusions_filename, skip_coherence=False): + """ + Runs coherence and non-regression quality checks on inclusions data. + + Args: + current_inclusions: Either a filename (str) to load inclusions from, + or a list of inclusion dictionaries (already in memory) + organizations_list: Either a filename (str) to load organizations from, + or a list of organization dictionaries (already in memory) + old_inclusions_filename: Filename of old inclusions for regression comparison + Must be a string (filename) + skip_coherence: If True, skip coherence check (default: False) + + Returns: + Tuple of (has_coherence_critical, has_regression_critical) + + Usage: + - Normal mode: + run_quality_checks( + current_inclusions=output_inclusions, # list (in memory) + organizations_list=organizations_list, # list (in memory) + old_inclusions_filename=INCLUSIONS_FILE_NAME # str (current file) + ) + + - Check-only mode: + run_quality_checks( + current_inclusions=INCLUSIONS_FILE_NAME, # str (current file) + organizations_list=ORGANIZATIONS_FILE_NAME, # str (organizations file) + old_inclusions_filename=get_old_filename(INCLUSIONS_FILE_NAME) # str (old file) + ) + """ + global console, regression_check_config + + # Auto-load regression config if not already loaded + if not regression_check_config: + if console is None: + console = Console() + load_regression_check_config(console) + + console.print("[bold cyan]══════════════════════════════════════════════════[/bold cyan]") + + # Load current_inclusions if it's a filename + if isinstance(current_inclusions, str): + current_inclusions_data = load_json_file(current_inclusions) + if current_inclusions_data is None: + console.print(f"[bold red]Error: Could not load current inclusions from '{current_inclusions}'[/bold red]") + return True, True # Return critical errors if can't load + elif isinstance(current_inclusions, list): + current_inclusions_data = current_inclusions + else: + console.print(f"[bold red]Error: current_inclusions must be either a filename (str) or a list of inclusions[/bold red]") + return True, True + + # Load organizations and run coherence check (unless skipped) + has_coherence_critical = False + if not skip_coherence: + # Load organizations_list if it's a filename + if isinstance(organizations_list, str): + organizations_data = load_json_file(organizations_list) + if organizations_data is None: + console.print(f"[bold red]Error: Could not load organizations from '{organizations_list}'[/bold red]") + return True, True # Return critical errors if can't load + elif isinstance(organizations_list, list): + organizations_data = organizations_list + else: + console.print(f"[bold red]Error: organizations_list must be either a filename (str) or a list of organizations[/bold red]") + return True, True + + # Run coherence check + has_coherence_critical = coherence_check(current_inclusions_data, organizations_data) + + # Load and run non-regression check + has_regression_critical = non_regression_check(current_inclusions_data, old_inclusions_filename) + + console.print("[bold cyan]══════════════════════════════════════════════════[/bold cyan]") + print() + + return has_coherence_critical, has_regression_critical + + +# ============================================================================ +# NON-REGRESSION CHECK +# ============================================================================ + +def non_regression_check(output_inclusions, old_inclusions_filename): + """ + Comprehensive config-driven non-regression check comparing current vs old inclusions. + Uses rules from regression_check_config loaded from Excel. + Returns True if any critical issue was found, False otherwise. + + Args: + output_inclusions: Current inclusions data (list) + old_inclusions_filename: Filename of old inclusions JSON file to load + """ + # Display section header first + console.print("\n[bold]═══ Non Regression Check ═══[/bold]\n") + + # Display loading message and load old inclusions file + console.print(f"[dim]Loading old inclusions from: {old_inclusions_filename}[/dim]") + old_inclusions = load_json_file(old_inclusions_filename) + + if old_inclusions is None: + console.print(f"[yellow]⚠ No old inclusions file found at '{old_inclusions_filename}', skipping non-regression check[/yellow]") + return False + + has_critical = False # Track critical status + + # ========== INTERNAL UTILITY FUNCTIONS ========== + + def _is_undefined(value): + """Check if a value is considered undefined.""" + return value in [None, "", "undefined"] + + def _values_are_equal(val1, val2): + """ + Compare two values with special handling for undefined values. + - If both are undefined → considered equal + - Otherwise → strict equality + """ + if _is_undefined(val1) and _is_undefined(val2): + return True + return val1 == val2 + + def _apply_pipeline_step(checked_fields, action, field_selector, from_pattern, to_pattern): + """Apply one pipeline step to checked_fields list IN-PLACE. + + Modifies the is_checked status (5th element) of fields matching the selector + and transition pattern. + + Args: + checked_fields: List of [group_name, field_name, old_val, new_val, is_checked] + MODIFIED IN-PLACE + action: "include" or "exclude" + field_selector: "*.*", "group.*", or "group.field" + from_pattern: "*undefined", "*defined", "*", or literal value + to_pattern: "*undefined", "*defined", "*", or literal value + + Logic: + - For each field in checked_fields: + - If field matches selector AND transition matches: + - if action="include": set is_checked=True + - if action="exclude": set is_checked=False + - Otherwise: leave is_checked unchanged + + Returns: None (modifies list in place) + """ + for i, field_record in enumerate(checked_fields): + group_name, field_name, old_val, new_val, is_checked = field_record + + # Check if this step applies to this field + if not _field_selector_matches_pattern(field_selector, group_name, field_name): + continue + + # Check if transition matches + if _transition_matches(old_val, new_val, from_pattern, to_pattern): + if action == "include": + checked_fields[i][4] = True + elif action == "exclude": + checked_fields[i][4] = False + + def _transition_matches(old_val, new_val, expected_old, expected_new): + """ + Check if a transition matches with support for keywords. + + Keywords supported (start with *): + - "*undefined": matches None, "", "undefined" + - "*defined": matches any defined value (NOT None, "", "undefined") + - "*": matches any value + + All other values are treated as literal values and matched by exact equality. + + Args: + old_val: Actual old value + new_val: Actual new value + expected_old: Expected old value or keyword (if starts with *) + expected_new: Expected new value or keyword (if starts with *) + + Returns: + True if transition matches + """ + # Handle old value matching + if expected_old == "*undefined": + old_matches = old_val in [None, "", "undefined"] + elif expected_old == "*defined": + old_matches = old_val not in [None, "", "undefined"] + elif expected_old == "*": + old_matches = True + else: + # Literal value matching (exact equality) + old_matches = (old_val == expected_old) + + # Handle new value matching + if expected_new == "*undefined": + new_matches = new_val in [None, "", "undefined"] + elif expected_new == "*defined": + new_matches = new_val not in [None, "", "undefined"] + elif expected_new == "*": + new_matches = True + else: + # Literal value matching (exact equality) + new_matches = (new_val == expected_new) + + return old_matches and new_matches + + def _check_field_matches_exception(group_name, field_name, old_val, new_val, exception_spec): + """ + Check if a field matches an exception specification. + + Now supports both single transitions and multiple transitions per exception. + + Args: + group_name: Field group name + field_name: Field name + old_val: Old value + new_val: New value + exception_spec: Exception specification dict with "field" and "transition" + Examples: + Single: {"field": "Status", "transition": [false, true]} + Multiple: {"field": "Status", "transition": [[false, true], [true, false]]} + + Returns: + True if the field and its transition match the exception + """ + if not isinstance(exception_spec, dict): + return False + + exception_field = exception_spec.get("field") + exception_transition = exception_spec.get("transition") + + if not exception_field or not exception_transition: + return False + + # Parse field specification (format: "field_group.field_name" or just "field_name") + if "." in exception_field: + exc_group, exc_name = exception_field.split(".", 1) + # Must match both group and name + if exc_group != group_name or exc_name != field_name: + return False + else: + # Only field name specified, must match field name only + if exception_field != field_name: + return False + + # Check if transition matches (now supports multiple transitions) + if not isinstance(exception_transition, list): + return False + + # Check if this is array of arrays: [[old1, new1], [old2, new2], ...] + if exception_transition and isinstance(exception_transition[0], list): + # Multiple transitions + for trans_pair in exception_transition: + if len(trans_pair) != 2: + continue + expected_old, expected_new = trans_pair + if _transition_matches(old_val, new_val, expected_old, expected_new): + return True + return False + + # Legacy support: single transition [old, new] + elif len(exception_transition) == 2 and not isinstance(exception_transition[0], list): + expected_old, expected_new = exception_transition + return _transition_matches(old_val, new_val, expected_old, expected_new) + + return False + + def _get_status_and_style(count, warning_threshold, critical_threshold): + """Determine status level and visual style.""" + nonlocal has_critical + if count > critical_threshold: + has_critical = True + return "CRITICAL", "red", "✗" + elif count > warning_threshold: + return "WARNING", "yellow", "⚠" + else: + return "OK", "green", "✓" + + def _print_block_header(title, status_tuple, indent=0): + """Print block header with status.""" + indent_str = " " * indent + status, color, emoji = status_tuple + console.print(f"{indent_str}{emoji} [{color}][bold]{title}[/bold][/{color}]") + + def _print_check_line(message, count, status_tuple, indent=1): + """Print a check line.""" + indent_str = " " * indent + status, color, emoji = status_tuple + console.print(f"{indent_str}{emoji} [{color}]{message}: {count}[/{color}]") + + def _calculate_block_status(line_statuses): + """Calculate overall block status from line statuses.""" + if any(s[0] == "CRITICAL" for s in line_statuses): + return ("CRITICAL", "red", "✗") + elif any(s[0] == "WARNING" for s in line_statuses): + return ("WARNING", "yellow", "⚠") + else: + return ("OK", "green", "✓") + + # ========== NEW FIELD SELECTION PIPELINE FUNCTIONS ========== + + def _field_selector_matches_pattern(selector, group_name, field_name): + """ + Check if a field matches a field_selector pattern. + + Patterns: + - "*.*": matches any field + - "group.*": matches any field in specific group + - "group.field": matches specific field + + Args: + selector: Field selector pattern string + group_name: Actual group name + field_name: Actual field name + + Returns: + True if matches, False otherwise + """ + if selector == "*.*": + return True + + sel_group, sel_field = selector.split(".", 1) + + # Check group part + if sel_group != "*" and sel_group != group_name: + return False + + # Check field part + if sel_field == "*": + return True + + return sel_field == field_name + + def _apply_field_selection_pipeline(all_fields, field_selection_config): + """ + Apply field_selection pipeline to build candidate_fields. + + Args: + all_fields: List of (group_name, field_name) tuples available + field_selection_config: List of [action, field_selector] steps + + Returns: + Set of (group_name, field_name) tuples matching pipeline + """ + # Start with empty set + candidate_fields = set() + + # If None or empty, return empty (explicit requirement) + if not field_selection_config: + return candidate_fields + + # Apply each pipeline step + for action, field_selector in field_selection_config: + for group_name, field_name in all_fields: + # Check if this field matches the selector + if _field_selector_matches_pattern(field_selector, group_name, field_name): + if action == "include": + candidate_fields.add((group_name, field_name)) + elif action == "exclude": + candidate_fields.discard((group_name, field_name)) + + return candidate_fields + + def _get_key_field_from_new_inclusions_rule(rule, new_inclusions_list, old_inclusions_list): + """ + Determine key field by applying field_selection to first inclusion sample. + + Logic: + 1. Get first inclusion from new and old data (representative sample) + 2. Apply field_selection pipeline to both (same as any rule) + 3. Return first field that exists with value in BOTH inclusions + + Assumes inclusion structure is stable across all inclusions (reasonable assumption + for database-backed data). + + Args: + rule: "New Inclusions" rule with field_selection config + new_inclusions_list: List of new inclusions + old_inclusions_list: List of old inclusions + + Returns: + (key_field_name, field_group) tuple + + Raises: + ValueError: If lists empty or no valid key field found + """ + # Get first inclusion from each (representative sample of structure) + if not new_inclusions_list or not old_inclusions_list: + raise ValueError("Cannot determine key field: empty inclusion lists") + + new_inc = new_inclusions_list[0] # First new inclusion + old_inc = old_inclusions_list[0] # First old inclusion + + # Apply field_selection pipeline (SAME AS FOR ANY RULE!) + # This respects the full pipeline: include/exclude/wildcards + candidate_fields = _build_candidate_fields(new_inc, old_inc, rule.get("field_selection")) + + if not candidate_fields: + raise ValueError( + f"field_selection produced no candidate fields. " + f"Config: {rule.get('field_selection')}" + ) + + # Try each candidate field in order (sorted for determinism) + # Return first field that has non-null value in both inclusions + for group_name, field_name in sorted(candidate_fields): + new_val = get_nested_value(new_inc, [group_name, field_name]) + old_val = get_nested_value(old_inc, [group_name, field_name]) + + if new_val is not None and old_val is not None: + return field_name, group_name + + # No valid key found + raise ValueError( + f"No field in field_selection has values in both first new and old inclusion. " + f"Candidates from pipeline: {candidate_fields}. " + f"Verify field_selection config or data has proper values." + ) + + def _build_inclusion_dict(inclusions_list, key_field, field_group="Patient_Identification"): + """ + Build dictionary indexed by key field. + + Args: + inclusions_list: List of inclusion dicts + key_field: Field name to use as key (e.g., "Patient_Id", "Pseudo") + field_group: Group containing the key field (default: "Patient_Identification") + + Returns: + Dict with key values as keys, inclusion dicts as values + """ + result = {} + for inclusion in inclusions_list: + key = get_nested_value(inclusion, [field_group, key_field]) + if key: + result[key] = inclusion + return result + + # ========== TRANSITION MATCHING FUNCTIONS ========== + + def _matches_transition(old_val, new_val, transitions_config): + """Check if (old_val, new_val) matches any configured transition. + + Uses the helper function _transition_matches for consistency. + + Supports keywords with asterisk prefix: + - *undefined: matches any undefined value (None, "", "undefined") + - *defined: matches any defined value (not None, "", or "undefined") + - *: wildcard, matches any value + + All other values are treated as literal values and matched by exact equality. + """ + if transitions_config is None: + return False + for transition in transitions_config: + expected_old, expected_new = transition + if _transition_matches(old_val, new_val, expected_old, expected_new): + return True + return False + + # ========== RULE PROCESSING FUNCTIONS ========== + + def _process_special_rule(rule, line_label, new_dict, old_dict): + """ + Process special rules: "New Inclusions" and "Deleted Inclusions". + These rules simply count the number of keys present in one dict but not the other. + + Args: + rule: Rule configuration (unused for counting, but kept for consistency) + line_label: The line label to identify which special rule this is + new_dict: Dictionary of new inclusions + old_dict: Dictionary of old inclusions + + Returns: + Count of new or deleted inclusions + """ + if line_label == "New Inclusions": + return len(set(new_dict.keys()) - set(old_dict.keys())) + elif line_label == "Deleted Inclusions": + return len(set(old_dict.keys()) - set(new_dict.keys())) + else: + # Should not happen, but return 0 for safety + return 0 + + def _process_new_deleted_fields(line_label, new_dict, old_dict): + """ + Process special rules: "New Fields" and "Deleted Fields". + + These rules collect all fields that appear/disappear in inclusions, using + qualified names "group.field" to distinguish fields across different groups. + + Note: field_selection is NOT used for these rules (must be empty). + + Returns a list of tuples: [(field_qualified_name, count_of_inclusions), ...] + where count_of_inclusions is the number of inclusions that have this field added/removed. + + Args: + line_label: "New Fields" or "Deleted Fields" + new_dict: Dictionary of new inclusions + old_dict: Dictionary of old inclusions + + Returns: + List of (qualified_field_name, inclusion_count) tuples + """ + # Collect field changes across all common inclusions + field_counts = {} # qualified_field_name -> count of inclusions + + # Only examine common inclusions (present in both versions) + # Sort for deterministic processing + common_keys = sorted(set(new_dict.keys()) & set(old_dict.keys())) + + for key in common_keys: + new_inc = new_dict[key] + old_inc = old_dict[key] + + # Get all groups from both versions + # Sort for deterministic processing + all_groups = sorted(set(new_inc.keys()) | set(old_inc.keys())) + + for group_name in all_groups: + new_group = new_inc.get(group_name, {}) + old_group = old_inc.get(group_name, {}) + + if not isinstance(new_group, dict): + new_group = {} + if not isinstance(old_group, dict): + old_group = {} + + new_fields = set(new_group.keys()) + old_fields = set(old_group.keys()) + + # Determine which fields to count based on line_label + if line_label == "New Fields": + changed_fields = sorted(new_fields - old_fields) + elif line_label == "Deleted Fields": + changed_fields = sorted(old_fields - new_fields) + else: + changed_fields = [] + + # Count each changed field with qualified name (sorted for determinism) + for field_name in changed_fields: + qualified_name = f"{group_name}.{field_name}" + field_counts[qualified_name] = field_counts.get(qualified_name, 0) + 1 + + # Convert to list of tuples and sort by count (descending) then by name + result = sorted(field_counts.items(), key=lambda x: (-x[1], x[0])) + return result + + def _build_candidate_fields(new_inc, old_inc, field_selection_config): + """ + Helper function to build candidate fields using field_selection pipeline. + + Args: + new_inc: New inclusion dict + old_inc: Old inclusion dict + field_selection_config: List of [action, field_selector] pipeline steps + + Returns: + Sorted list of (group_name, field_name) tuples that exist in both versions + """ + # Step 1: Collect all available fields from both versions + common_groups = sorted(set(new_inc.keys()) & set(old_inc.keys())) + all_available_fields = [] + + for group_name in common_groups: + new_group = new_inc.get(group_name, {}) + old_group = old_inc.get(group_name, {}) + + if not isinstance(new_group, dict): + new_group = {} + if not isinstance(old_group, dict): + old_group = {} + + # Only fields that exist in both versions + common_field_names = sorted(set(new_group.keys()) & set(old_group.keys())) + + for field_name in common_field_names: + all_available_fields.append((group_name, field_name)) + + # Step 2: Apply field_selection pipeline + if not field_selection_config: + return [] + + candidate_fields = _apply_field_selection_pipeline( + all_available_fields, + field_selection_config + ) + + return sorted(candidate_fields, key=lambda x: (x[0], x[1])) + + def _process_rule(rule, new_dict, old_dict): + """ + Process a single regression check rule with correct 4-step logic. + + Logic: + 1. Build candidate fields using field_selection pipeline + 2. For each changed field, check if transition matches → mark as "checked" + 3. Apply transitions pipeline steps → modify "checked" status + 4. Apply bloc_scope (all/any) → count inclusion + + Only processes common_keys (inclusions present in both new and old dicts). + + Args: + rule: Rule configuration dict + new_dict: Dict of new inclusions indexed by key field + old_dict: Dict of old inclusions indexed by key field + + Returns: + Tuple of (count, details_list) where: + - count: Number of matching inclusions + - details_list: List of (inclusion_key, field_changes) tuples for DEBUG_MODE + field_changes is list of (group.field, old_val, new_val) tuples + """ + # Check for config errors first + if rule.get("_config_error"): + return 0, [] + + field_selection_config = rule.get("field_selection") + bloc_scope = rule.get("bloc_scope") or "any" + + # Only process inclusions present in both versions + common_keys = sorted(set(new_dict.keys()) & set(old_dict.keys())) + matching_inclusions_count = 0 + details_list = [] # For DEBUG_MODE + + for key in common_keys: + new_inc = new_dict[key] + old_inc = old_dict[key] + + # Step 1: Build candidate fields using field_selection pipeline + candidate_fields = _build_candidate_fields(new_inc, old_inc, field_selection_config) + + # If no candidate fields, skip this inclusion + if not candidate_fields: + continue + + # Step 2 & 3: Build initial field list and apply transitions pipeline + # Initialize field list with all changed fields + # Format: [group_name, field_name, old_val, new_val, is_checked] + all_fields_list = [] + changed_fields = [] # Track for bloc_scope="all" logic + + for group_name, field_name in candidate_fields: + new_val = get_nested_value(new_inc, [group_name, field_name]) + old_val = get_nested_value(old_inc, [group_name, field_name]) + + # Track if field has changed (for bloc_scope="all" logic) + field_has_changed = not _values_are_equal(old_val, new_val) + if field_has_changed: + changed_fields.append((group_name, field_name)) + # Add to all_fields_list with is_checked=False initially + all_fields_list.append([group_name, field_name, old_val, new_val, False]) + + # Apply transitions pipeline: each step modifies is_checked in-place + transitions_config = rule.get("transitions", []) + if transitions_config and isinstance(transitions_config, list): + for action, field_selector, from_val, to_val in transitions_config: + _apply_pipeline_step(all_fields_list, action, field_selector, from_val, to_val) + + # Extract final checked fields + checked_fields = [(f[0], f[1], f[2], f[3]) for f in all_fields_list if f[4]] + + # Step 4: Apply bloc_scope logic + inclusion_matches = False + if bloc_scope == "all": + # ALL fields that CHANGED must match the transition pattern + # (unchanged fields don't block the rule) + if len(changed_fields) > 0 and len(checked_fields) == len(changed_fields): + inclusion_matches = True + else: # bloc_scope == "any" + # AT LEAST ONE field must be checked + if len(checked_fields) > 0: + inclusion_matches = True + + if inclusion_matches: + matching_inclusions_count += 1 + # Collect details for debug_mode + if debug_mode and checked_fields: + field_changes = [(f"{gn}.{fn}", ov, nv) for gn, fn, ov, nv in checked_fields] + details_list.append((key, field_changes)) + + return matching_inclusions_count, details_list + + # ========== MAIN LOGIC ========== + + # Determine key field from "New Inclusions" rule config + key_field = None + field_group = None + + for rule in regression_check_config: + if rule.get("line_label") == "New Inclusions": + try: + key_field, field_group = _get_key_field_from_new_inclusions_rule( + rule, + output_inclusions, + old_inclusions + ) + break + except ValueError as e: + console.print(f"[bold red]Error determining key field: {e}[/bold red]") + return True # Critical error, trigger user confirmation + + if not key_field: + console.print("[bold red]Error: 'New Inclusions' rule not found or has no valid field_selection[/bold red]") + return True # Critical error, trigger user confirmation + + console.print(f"[dim]Using key field: {field_group}.{key_field}[/dim]\n") + + new_dict = _build_inclusion_dict(output_inclusions, key_field, field_group) + old_dict = _build_inclusion_dict(old_inclusions, key_field, field_group) + + # Group rules by bloc_title, preserving order of first appearance in regression_check_config + blocs = {} + bloc_order = [] # Track order of first appearance + for rule in regression_check_config: + bloc_title = rule["bloc_title"] + if bloc_title not in blocs: + blocs[bloc_title] = [] + bloc_order.append(bloc_title) + blocs[bloc_title].append(rule) + + # Process each bloc in order of first appearance + for bloc_title in bloc_order: + rules = blocs[bloc_title] + line_results = [] + + for rule in rules: + line_label = rule["line_label"] + warning_threshold = rule["warning_threshold"] + critical_threshold = rule["critical_threshold"] + + # Detect special rules and route to appropriate processing function + if line_label in ["New Inclusions", "Deleted Inclusions"]: + # Special rules: just count new/deleted keys + count = _process_special_rule(rule, line_label, new_dict, old_dict) + line_results.append((line_label, count, None, "simple")) # type: simple count + + elif line_label in ["New Fields", "Deleted Fields"]: + # Special rules: collect field-by-field details + field_list = _process_new_deleted_fields(line_label, new_dict, old_dict) + # Count is the number of fields detected + count = len(field_list) + line_results.append((line_label, count, field_list, "fields")) # type: field list + + else: + # Normal rules: apply 4-step logic + count, details = _process_rule(rule, new_dict, old_dict) + line_results.append((line_label, count, details, "details")) # type: inclusion details + + # Calculate status for each line now that we have counts + line_results_with_status = [] + for line_label, count, data, result_type in line_results: + # Find the rule to get thresholds + rule = next(r for r in rules if r["line_label"] == line_label) + warning_threshold = rule["warning_threshold"] + critical_threshold = rule["critical_threshold"] + status_tuple = _get_status_and_style(count, warning_threshold, critical_threshold) + line_results_with_status.append((line_label, count, data, result_type, status_tuple)) + + # Calculate bloc status + bloc_status = _calculate_block_status([result[4] for result in line_results_with_status]) + + # Display bloc header + _print_block_header(bloc_title, bloc_status, indent=0) + + # Display lines based on bloc and status + for line_label, count, data, result_type, status_tuple in line_results_with_status: + # Structure bloc shows everything, others only show non-OK lines + should_display = (bloc_title == "Structure") or (status_tuple[0] != "OK") + + if should_display: + if result_type == "fields": + # Display field list with title and sub-items + _print_check_line(line_label, count, status_tuple, indent=1) + # Display each field as a sub-item + for field_name, inclusion_count in data: + console.print(f" {field_name} ({inclusion_count} inclusions)") + + elif result_type == "details": + # Display count + _print_check_line(line_label, count, status_tuple, indent=1) + + # Display detailed changes if debug_mode is enabled and data exists + if debug_mode and data and len(data) > 0: + for inclusion_key, field_changes in data: + console.print(f" [dim]{key_field}: {inclusion_key}[/dim]") + for qualified_field, old_val, new_val in field_changes: + # Format values for display + old_display = f"'{old_val}'" if isinstance(old_val, str) else str(old_val) + new_display = f"'{new_val}'" if isinstance(new_val, str) else str(new_val) + console.print(f" - {qualified_field}: {old_display} → {new_display}") + + else: + # Simple count display + _print_check_line(line_label, count, status_tuple, indent=1) + + console.print() + + return has_critical diff --git a/eb_dashboard_utils.py b/eb_dashboard_utils.py new file mode 100644 index 0000000..2aab906 --- /dev/null +++ b/eb_dashboard_utils.py @@ -0,0 +1,220 @@ +""" +Endobest Dashboard - Utility Functions Module + +This module contains generic utility functions used throughout the Endobest Dashboard: +- HTTP client management (thread-safe) +- Nested data structure navigation with wildcard support +- Configuration path resolution (script vs PyInstaller) +- Thread position management for progress bars +- Filename generation utilities +""" + +import os +import sys +import threading + +import httpx + +from eb_dashboard_constants import CONFIG_FOLDER_NAME + + +# ============================================================================ +# GLOBAL VARIABLES (managed by main module) +# ============================================================================ +thread_local_storage = threading.local() + + +def run_with_context(func, context, *args, **kwargs): + """ + Wrapper to set thread-local context before running a function in a new thread. + Useful for ThreadPoolExecutor where context is lost. + """ + thread_local_storage.current_patient_context = context + return func(*args, **kwargs) + + +# These will be set/accessed from the main module +httpx_clients = {} +_clients_lock = threading.Lock() +threads_list = [] +_threads_list_lock = threading.Lock() + + +# ============================================================================ +# HTTP CLIENT MANAGEMENT +# ============================================================================ + +def get_httpx_client() -> httpx.Client: + """ + Get or create thread-local HTTP client. + Keep-alive is disabled to avoid stale connections with load balancers. + """ + global httpx_clients + thread_id = threading.get_ident() + + with _clients_lock: + if thread_id not in httpx_clients: + # Create client with keep-alive disabled + httpx_clients[thread_id] = httpx.Client( + headers={"Connection": "close"}, # Explicitly request closing + limits=httpx.Limits(max_keepalive_connections=0, max_connections=100) + ) + return httpx_clients[thread_id] + + +def clear_httpx_client(): + """ + Removes the current thread's client from the cache. + Ensures a fresh client (and socket pool) will be created on the next call. + """ + global httpx_clients + thread_id = threading.get_ident() + with _clients_lock: + if thread_id in httpx_clients: + try: + # Close the client before removing it + httpx_clients[thread_id].close() + except: + pass + del httpx_clients[thread_id] + + +def get_thread_position(): + """ + Get the position of the current thread in the threads list. + Used for managing progress bar positions in multithreaded environment. + """ + global threads_list + thread_id = threading.get_ident() + with _threads_list_lock: + if thread_id not in threads_list: + threads_list.append(thread_id) + return len(threads_list) - 1 + else: + return threads_list.index(thread_id) + + +# ============================================================================ +# NESTED DATA NAVIGATION +# ============================================================================ + +def get_nested_value(data_structure, path, default=None): + """ + Extracts a value from a nested structure of dictionaries and lists. + Supports a wildcard '*' in the path to retrieve all elements from a list. + + Args: + data_structure: The nested dict/list structure to navigate + path: List of keys/indices to follow. Use '*' for list wildcard. + default: Value to return if path not found + + Returns: + The value at the end of the path, or default if not found + + Examples: + get_nested_value({"a": {"b": 1}}, ["a", "b"]) -> 1 + get_nested_value({"items": [{"x": 1}, {"x": 2}]}, ["items", "*", "x"]) -> [1, 2] + """ + if data_structure is None: + return "$$$$ No Data" + if not path: + return default + + if "*" in path: + wildcard_index = path.index("*") + path_before = path[:wildcard_index] + path_after = path[wildcard_index+1:] + + # Create a temporary function for non-wildcard path resolution + def _get_simple_nested_value(ds, p, d): + cl = ds + for k in p: + if isinstance(cl, dict): + cl = cl.get(k) + elif isinstance(cl, list): + try: + if isinstance(k, int) and -len(cl) <= k < len(cl): + cl = cl[k] + else: return d + except (IndexError, TypeError): return d + else: return d + if cl is None: return d + return cl + + base_level = _get_simple_nested_value(data_structure, path_before, default) + + if not isinstance(base_level, list): + return default + + results = [] + for item in base_level: + # For each item, recursively call to resolve the rest of the path + value = get_nested_value(item, path_after, default) + if value is not default and value != "$$$$ No Data": + results.append(value) + + # Flatten the results by one level to handle multiple wildcards + final_results = [] + for res in results: + if isinstance(res, list): + final_results.extend(res) + else: + final_results.append(res) + + return final_results + + # No wildcard, original logic (iterative) + current_level = data_structure + for key_or_index in path: + if isinstance(current_level, dict): + current_level = current_level.get(key_or_index) + if current_level is None: + return default + elif isinstance(current_level, list): + try: + if isinstance(key_or_index, int) and -len(current_level) <= key_or_index < len(current_level): + current_level = current_level[key_or_index] + else: + return default + except (IndexError, TypeError): + return default + else: + return default + return current_level + + +# ============================================================================ +# CONFIGURATION UTILITIES +# ============================================================================ + +def get_config_path(): + """ + Gets the correct path to the config folder. + Works for both script execution and PyInstaller executable. + + Returns: + Path to config folder + """ + if getattr(sys, 'frozen', False): + # Running as a PyInstaller bundle + config_folder = CONFIG_FOLDER_NAME + return os.path.join(sys._MEIPASS, config_folder) + else: + # Running as a script + return CONFIG_FOLDER_NAME + + +def get_old_filename(current_filename, old_suffix="_old"): + """Generate old backup filename from current filename. + + Example: "endobest_inclusions.json" → "endobest_inclusions_old.json" + + Args: + current_filename: Current file name (e.g., "endobest_inclusions.json") + old_suffix: Suffix to append before file extension (default: "_old") + + Returns: + Old backup filename with suffix before extension + """ + name, ext = os.path.splitext(current_filename) + return f"{name}{old_suffix}{ext}" diff --git a/eb_org_center_mapping.xlsx b/eb_org_center_mapping.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..83b00327e9eb445bb6ab027dbae360eaf2f7200c GIT binary patch literal 18804 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 JiTrW*e*kA^d>jA( literal 0 HcmV?d00001