Compare commits

..

6 Commits

Author SHA1 Message Date
13e794eb34 ignore logs 2026-02-09 18:43:34 +01:00
e58f867bd0 ignore exe 2026-02-09 18:42:39 +01:00
57540d5159 ignore all jsons 2026-02-09 18:34:51 +01:00
d6943faf59 exclude log from git 2026-02-09 17:16:53 +01:00
8562c45f05 executable 2026-02-09 17:08:59 +01:00
1cc2c754b7 No-6-Month-Visit 2026-02-09 17:01:55 +01:00
28 changed files with 478081 additions and 515 deletions

9
.gitignore vendored
View File

@@ -195,12 +195,7 @@ Endobest Reporting/
jsons history/ jsons history/
nul nul
# Ignore all json, exe, log, txt, csv and xlsx files dashboard.log
*.json *.json
*.exe *.exe
*.log *.log
*.txt
*.csv
/*.xlsx
!eb_org_center_mapping.xlsx
/pyproject.toml

Binary file not shown.

Binary file not shown.

40
dashboard.log Normal file
View File

@@ -0,0 +1,40 @@
2026-02-09 17:08:47,130 - WARNING - Error in get_record_by_patient_id (Attempt 1): Server error '502 Bad Gateway' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
2026-02-09 17:08:47,130 - WARNING - Error in get_record_by_patient_id (Attempt 1): Server error '502 Bad Gateway' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
2026-02-09 17:08:47,130 - WARNING - Error in get_record_by_patient_id (Attempt 1): Server error '502 Bad Gateway' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
2026-02-09 17:09:45,882 - WARNING - Error in get_record_by_patient_id (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,883 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,882 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,883 - WARNING - Error in get_record_by_patient_id (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,889 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,890 - WARNING - Error in get_record_by_patient_id (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,890 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,892 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,890 - WARNING - Error in get_record_by_patient_id (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,892 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,891 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:09:45,892 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): [WinError 10054] Une connexion existante a d<> <20>tre ferm<72>e par l<>h<EFBFBD>te distant
2026-02-09 17:17:16,591 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): Server error '502 Bad Gateway' for url 'https://api-hcp.ziwig-connect.com/api/surveys/filter/with-answers'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
2026-02-09 17:17:16,591 - WARNING - Error in get_record_by_patient_id (Attempt 1): Server error '502 Bad Gateway' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
2026-02-09 17:17:16,592 - WARNING - Error in get_record_by_patient_id (Attempt 1): Server error '502 Bad Gateway' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/502
2026-02-09 17:21:04,034 - WARNING - Error in get_request_by_tube_id (Attempt 1): Client error '401 Unauthorized' for url 'https://api-lab.ziwig-connect.com/api/requests/by-tube-id/55241110000385?isAdmin=true&organization=undefined'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:04,070 - WARNING - Error in get_request_by_tube_id (Attempt 1): Client error '401 Unauthorized' for url 'https://api-lab.ziwig-connect.com/api/requests/by-tube-id/55241110001327?isAdmin=true&organization=undefined'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:05,040 - WARNING - Error in get_record_by_patient_id (Attempt 1): Client error '401 Unauthorized' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:05,043 - WARNING - Error in get_record_by_patient_id (Attempt 1): Client error '401 Unauthorized' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:05,045 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): Client error '401 Unauthorized' for url 'https://api-hcp.ziwig-connect.com/api/surveys/filter/with-answers'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:05,045 - WARNING - Error in get_all_questionnaires_by_patient (Attempt 1): Client error '401 Unauthorized' for url 'https://api-hcp.ziwig-connect.com/api/surveys/filter/with-answers'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:05,331 - WARNING - Error in get_record_by_patient_id (Attempt 1): Client error '401 Unauthorized' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401
2026-02-09 17:21:06,089 - WARNING - Error in get_record_by_patient_id (Attempt 1): Client error '401 Unauthorized' for url 'https://api-hcp.ziwig-connect.com/api/records/byPatient'
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401

Binary file not shown.

View File

@@ -21,7 +21,7 @@
# identification, and support for complex data extraction using JSON path expressions. # identification, and support for complex data extraction using JSON path expressions.
import json import json
import logging import logging
import msvcrt
import os import os
import re import re
import sys import sys
@@ -86,10 +86,6 @@ from eb_dashboard_utils import (
clear_httpx_client, clear_httpx_client,
get_thread_position, get_thread_position,
get_config_path, get_config_path,
set_dashboard_config_path_override,
get_dashboard_config_path,
set_output_file_suffix,
get_output_filename,
thread_local_storage, thread_local_storage,
run_with_context run_with_context
) )
@@ -107,23 +103,8 @@ from eb_dashboard_excel_export import (
set_dependencies as excel_set_dependencies set_dependencies as excel_set_dependencies
) )
def _fatal_cli_error(message): logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s', filename=LOG_FILE_NAME,
"""Print a CLI error then wait for Enter before exiting (keeps console open on Windows Explorer launch).""" filemode='w')
print(message)
input("Press Enter to exit...")
sys.exit(1)
# Handle --add-suffix <value> early: must precede logging.basicConfig (affects log filename)
if "--add-suffix" in sys.argv:
_idx = sys.argv.index("--add-suffix")
if _idx + 1 >= len(sys.argv):
_fatal_cli_error("Error: --add-suffix requires a value")
set_output_file_suffix(sys.argv[_idx + 1])
del sys.argv[_idx:_idx + 2]
logging.basicConfig(level=logging.WARNING, format='%(asctime)s - %(levelname)s - %(message)s',
filename=get_output_filename(LOG_FILE_NAME), filemode='w')
# ============================================================================ # ============================================================================
@@ -137,10 +118,6 @@ access_token = ""
refresh_token = "" refresh_token = ""
threads_list = [] threads_list = []
_token_refresh_lock = threading.Lock() _token_refresh_lock = threading.Lock()
on_retry_exhausted = "ask" # "ask" | "ignore" | "abort" — set at startup
fetch_six_month_visit = False # Whether to fetch 6-month visit data (slow, ~5s per patient)
_stored_username = "" # Credentials stored at login for automatic re-login
_stored_password = ""
_threads_list_lock = threading.Lock() _threads_list_lock = threading.Lock()
global_pbar = None global_pbar = None
_global_pbar_lock = threading.Lock() _global_pbar_lock = threading.Lock()
@@ -149,7 +126,6 @@ _user_interaction_lock = threading.Lock()
# Global variables (mutable, set at runtime - not constants) # Global variables (mutable, set at runtime - not constants)
inclusions_mapping_config = [] inclusions_mapping_config = []
organizations_mapping_config = [] organizations_mapping_config = []
_include_drafts = False # Set by --include-drafts CLI arg if provided
excel_export_config = None excel_export_config = None
excel_export_enabled = False excel_export_enabled = False
@@ -172,23 +148,6 @@ if "--debug" in sys.argv:
sys.argv.remove("--debug") sys.argv.remove("--debug")
enable_debug_mode() enable_debug_mode()
# Handle --config <path> override (remove from sys.argv to preserve positional args)
if "--config" in sys.argv:
_idx = sys.argv.index("--config")
if _idx + 1 >= len(sys.argv):
_fatal_cli_error("Error: --config requires a file path argument")
_raw_config_path = sys.argv[_idx + 1]
del sys.argv[_idx:_idx + 2]
if os.path.isabs(_raw_config_path):
set_dashboard_config_path_override(_raw_config_path)
else:
set_dashboard_config_path_override(os.path.join(get_config_path(), _raw_config_path))
# Handle --include-drafts flag (remove from sys.argv to preserve positional args)
_include_drafts = "--include-drafts" in sys.argv
if _include_drafts:
sys.argv.remove("--include-drafts")
# --- Progress Bar Configuration --- # --- Progress Bar Configuration ---
# NOTE: BAR_N_FMT_WIDTH, BAR_TOTAL_FMT_WIDTH, BAR_TIME_WIDTH, BAR_RATE_WIDTH # NOTE: BAR_N_FMT_WIDTH, BAR_TOTAL_FMT_WIDTH, BAR_TIME_WIDTH, BAR_RATE_WIDTH
# are imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH) # are imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH)
@@ -227,10 +186,8 @@ def new_token():
finally: finally:
if attempt < ERROR_MAX_RETRY - 1: if attempt < ERROR_MAX_RETRY - 1:
sleep(WAIT_BEFORE_RETRY) sleep(WAIT_BEFORE_RETRY)
# Refresh token exhausted — attempt full re-login with stored credentials logging.critical("Persistent error in refresh_token")
logging.warning("Refresh token exhausted. Attempting re-login with stored credentials.") raise httpx.RequestError(message="Persistent error in refresh_token")
_do_login(_stored_username, _stored_password)
logging.info("Re-login successful. New tokens acquired.")
def api_call_with_retry(func): def api_call_with_retry(func):
@@ -255,10 +212,7 @@ def api_call_with_retry(func):
if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 401: if isinstance(exc, httpx.HTTPStatusError) and exc.response.status_code == 401:
logging.info(f"Token expired for {func_name}. Refreshing token.") logging.info(f"Token expired for {func_name}. Refreshing token.")
try: new_token()
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: if attempt < ERROR_MAX_RETRY - 1:
sleep(WAIT_BEFORE_RETRY) sleep(WAIT_BEFORE_RETRY)
@@ -271,41 +225,32 @@ def api_call_with_retry(func):
sleep(WAIT_BEFORE_NEW_BATCH_OF_RETRIES) sleep(WAIT_BEFORE_NEW_BATCH_OF_RETRIES)
break # Exit for loop to restart batch in while True break # Exit for loop to restart batch in while True
else: else:
# All automatic batches exhausted — apply on_retry_exhausted policy # All automatic batches exhausted, ask the user
with _user_interaction_lock: with _user_interaction_lock:
if on_retry_exhausted == "ignore": 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)":
# Retrieve context if available
ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"}) 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}") logging.warning(f"[IGNORE] User opted to skip {func_name} for Patient {ctx['id']} ({ctx['pseudo']}). Error: {exc}")
return None return None
else:
elif on_retry_exhausted == "abort": logging.critical(f"User chose to stop script after persistent error in {func_name}.")
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} (stopped by user)")
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 return wrapper
@@ -314,37 +259,8 @@ def api_call_with_retry(func):
# BLOCK 3: AUTHENTICATION # 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=60)
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(): def login():
global _stored_username, _stored_password global access_token, refresh_token
user_name = (questionary.text("login :", default=DEFAULT_USER_NAME).ask()) user_name = (questionary.text("login :", default=DEFAULT_USER_NAME).ask())
password = (questionary.password("password :", default=DEFAULT_PASSWORD).ask()) password = (questionary.password("password :", default=DEFAULT_PASSWORD).ask())
@@ -352,18 +268,42 @@ def login():
return "Exit" return "Exit"
try: try:
_do_login(user_name, password) client = get_httpx_client()
client.base_url = IAM_URL
response = client.post(API_AUTH_LOGIN_ENDPOINT, json={"username": user_name, "password": password},
timeout=20)
response.raise_for_status()
master_token = response.json()["access_token"]
user_id = response.json()["userId"]
except httpx.RequestError as exc: except httpx.RequestError as exc:
print(f"Login Error : {exc}") print(f"Login Error : {exc}")
logging.warning(f"Login Error : {exc}") logging.warning(f"Login Error : {exc}")
return "Error" return "Error"
except httpx.HTTPStatusError as exc: except httpx.HTTPStatusError as exc:
print(f"Login Error : {exc.response.status_code} for Url {exc.request.url}") 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}") logging.warning(
f"Login Error : {exc.response.status_code} for Url {exc.request.url}")
return "Error"
try:
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"]
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}")
return "Error" return "Error"
_stored_username = user_name
_stored_password = password
print() print()
print("Login Success") print("Login Success")
return "Success" return "Success"
@@ -373,100 +313,6 @@ def login():
# BLOCK 3B: FILE UTILITIES # 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): def load_json_file(filename):
""" """
Load a JSON file from disk. Load a JSON file from disk.
@@ -494,7 +340,7 @@ def load_json_file(filename):
def load_inclusions_mapping_config(): def load_inclusions_mapping_config():
"""Loads and validates the inclusions mapping configuration from the Excel file.""" """Loads and validates the inclusions mapping configuration from the Excel file."""
global inclusions_mapping_config global inclusions_mapping_config
config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME) config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME)
try: try:
# Load with data_only=True to read calculated values instead of formulas # Load with data_only=True to read calculated values instead of formulas
@@ -596,7 +442,7 @@ def load_inclusions_mapping_config():
def load_organizations_mapping_config(): def load_organizations_mapping_config():
"""Loads and validates the organizations mapping configuration from the Excel file.""" """Loads and validates the organizations mapping configuration from the Excel file."""
global organizations_mapping_config global organizations_mapping_config
config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME) config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME)
try: try:
# Load with data_only=True to read calculated values instead of formulas # Load with data_only=True to read calculated values instead of formulas
@@ -654,12 +500,6 @@ def _find_questionnaire_by_id(qcm_dict, qcm_id):
if not isinstance(qcm_dict, dict): if not isinstance(qcm_dict, dict):
return None return None
qcm_data = qcm_dict.get(qcm_id) qcm_data = qcm_dict.get(qcm_id)
if qcm_data and qcm_data.get("_count", 1) > 1:
ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"})
logging.error(
f"[DUPLICATE QCM] Patient {ctx['id']} ({ctx['pseudo']}): "
f"Questionnaire id='{qcm_id}' appeared {qcm_data['_count']} times in API response — using last received copy"
)
return qcm_data.get("answers") if qcm_data else None return qcm_data.get("answers") if qcm_data else None
@@ -667,32 +507,20 @@ def _find_questionnaire_by_name(qcm_dict, name):
"""Finds a questionnaire by name (sequential search, returns first match).""" """Finds a questionnaire by name (sequential search, returns first match)."""
if not isinstance(qcm_dict, dict): if not isinstance(qcm_dict, dict):
return None return None
matches = [qcm for qcm in qcm_dict.values() for qcm in qcm_dict.values():
if get_nested_value(qcm, ["questionnaire", "name"]) == name] if get_nested_value(qcm, ["questionnaire", "name"]) == name:
if len(matches) > 1: return qcm.get("answers")
ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"}) return None
ids = [get_nested_value(q, ["questionnaire", "id"]) for q in matches]
logging.error(
f"[DUPLICATE QCM] Patient {ctx['id']} ({ctx['pseudo']}): "
f"Questionnaire name='{name}' matches {len(matches)} entries (ids: {ids}) — returning first match"
)
return matches[0].get("answers") if matches else None
def _find_questionnaire_by_category(qcm_dict, category): def _find_questionnaire_by_category(qcm_dict, category):
"""Finds a questionnaire by category (sequential search, returns first match).""" """Finds a questionnaire by category (sequential search, returns first match)."""
if not isinstance(qcm_dict, dict): if not isinstance(qcm_dict, dict):
return None return None
matches = [qcm for qcm in qcm_dict.values() for qcm in qcm_dict.values():
if get_nested_value(qcm, ["questionnaire", "category"]) == category] if get_nested_value(qcm, ["questionnaire", "category"]) == category:
if len(matches) > 1: return qcm.get("answers")
ctx = getattr(thread_local_storage, "current_patient_context", {"id": "Unknown", "pseudo": "Unknown"}) return None
ids = [get_nested_value(q, ["questionnaire", "id"]) for q in matches]
logging.error(
f"[DUPLICATE QCM] Patient {ctx['id']} ({ctx['pseudo']}): "
f"Questionnaire category='{category}' matches {len(matches)} entries (ids: {ids}) — returning first match"
)
return matches[0].get("answers") if matches else None
def _get_field_value_from_questionnaire(all_questionnaires, field_config): def _get_field_value_from_questionnaire(all_questionnaires, field_config):
@@ -729,13 +557,6 @@ def get_value_from_inclusion(inclusion_dict, key):
def _execute_custom_function(function_name, args, output_inclusion): def _execute_custom_function(function_name, args, output_inclusion):
"""Executes a custom function for a calculated field.""" """Executes a custom function for a calculated field."""
def dominant_no_value(has_undef, has_na):
"""Returns the dominant sentinel: 'undefined' > 'N/A' > None (real value present)."""
if has_undef: return "undefined"
if has_na: return "N/A"
return None
if function_name == "search_in_fields_using_regex": if function_name == "search_in_fields_using_regex":
if not args or len(args) < 2: if not args or len(args) < 2:
return "$$$$ Argument Error: search_in_fields_using_regex requires at least 2 arguments" return "$$$$ Argument Error: search_in_fields_using_regex requires at least 2 arguments"
@@ -744,19 +565,16 @@ def _execute_custom_function(function_name, args, output_inclusion):
field_names = args[1:] field_names = args[1:]
field_values = [] field_values = []
has_undefined = False all_undefined = True
has_na = False
for field_name in field_names: for field_name in field_names:
value = get_value_from_inclusion(output_inclusion, field_name) value = get_value_from_inclusion(output_inclusion, field_name)
field_values.append(value) field_values.append(value)
if value is None or value == "undefined": if value is not None and value != "undefined":
has_undefined = True all_undefined = False
elif value == "N/A":
has_na = True
if not any(v not in (None, "undefined", "N/A") for v in field_values): if all_undefined:
return dominant_no_value(has_undefined, has_na) return "undefined"
try: try:
for value in field_values: for value in field_values:
@@ -777,8 +595,6 @@ def _execute_custom_function(function_name, args, output_inclusion):
if value is None or value == "undefined": if value is None or value == "undefined":
return "undefined" return "undefined"
if value == "N/A":
return "N/A"
match = re.search(r'\((.*?)\)', str(value)) match = re.search(r'\((.*?)\)', str(value))
return match.group(1) if match else "undefined" return match.group(1) if match else "undefined"
@@ -792,8 +608,6 @@ def _execute_custom_function(function_name, args, output_inclusion):
if status is None or status == "undefined": if status is None or status == "undefined":
return "undefined" return "undefined"
if status == "N/A":
return "N/A"
if not isinstance(is_terminated, bool) or not is_terminated: if not isinstance(is_terminated, bool) or not is_terminated:
return status return status
@@ -803,8 +617,7 @@ def _execute_custom_function(function_name, args, output_inclusion):
elif function_name == "if_then_else": elif function_name == "if_then_else":
# Unified conditional function # Unified conditional function
# Syntax: ["operator", arg1, arg2_optional, result_if_true, result_if_false] # Syntax: ["operator", arg1, arg2_optional, result_if_true, result_if_false]
# Operators: "is_true", "is_false", "all_true", "any_true", "is_defined", "is_undefined", "all_defined", "==", "!=", ">", ">=", "<", "<=" # Operators: "is_true", "is_false", "all_true", "is_defined", "is_undefined", "all_defined", "==", "!="
# Sentinel propagation: "undefined" (unknown) > "N/A" (not applicable) > real value.
if not args or len(args) < 4: if not args or len(args) < 4:
return "$$$$ Argument Error: if_then_else requires at least 4 arguments" return "$$$$ Argument Error: if_then_else requires at least 4 arguments"
@@ -832,8 +645,6 @@ def _execute_custom_function(function_name, args, output_inclusion):
value = resolve_value(args[1]) value = resolve_value(args[1])
if value is None or value == "undefined": if value is None or value == "undefined":
return "undefined" return "undefined"
if value == "N/A":
return "N/A"
condition = (value is True) condition = (value is True)
result_if_true = resolve_value(args[2]) result_if_true = resolve_value(args[2])
result_if_false = resolve_value(args[3]) result_if_false = resolve_value(args[3])
@@ -844,8 +655,6 @@ def _execute_custom_function(function_name, args, output_inclusion):
value = resolve_value(args[1]) value = resolve_value(args[1])
if value is None or value == "undefined": if value is None or value == "undefined":
return "undefined" return "undefined"
if value == "N/A":
return "N/A"
condition = (value is False) condition = (value is False)
result_if_true = resolve_value(args[2]) result_if_true = resolve_value(args[2])
result_if_false = resolve_value(args[3]) result_if_false = resolve_value(args[3])
@@ -857,58 +666,22 @@ def _execute_custom_function(function_name, args, output_inclusion):
if not isinstance(fields_arg, list): if not isinstance(fields_arg, list):
return "$$$$ Argument Error: all_true requires arg1 to be a list of field names" return "$$$$ Argument Error: all_true requires arg1 to be a list of field names"
has_undefined = False
has_na = False
conditions = [] conditions = []
for field_name in fields_arg: for field_name in fields_arg:
field_value = get_value_from_inclusion(output_inclusion, field_name) field_value = get_value_from_inclusion(output_inclusion, field_name)
if field_value is None or field_value == "undefined": if field_value is None or field_value == "undefined":
has_undefined = True return "undefined"
elif field_value == "N/A": conditions.append(field_value)
has_na = True
else:
conditions.append(field_value)
status = dominant_no_value(has_undefined, has_na)
if status:
return status
condition = all(conditions) condition = all(conditions)
result_if_true = resolve_value(args[2]) result_if_true = resolve_value(args[2])
result_if_false = resolve_value(args[3]) result_if_false = resolve_value(args[3])
elif operator == "any_true":
# OR semantics: returns "undefined" only if ALL operands are undefined
if len(args) != 4:
return "$$$$ Argument Error: any_true requires 4 arguments"
fields_arg = args[1]
if not isinstance(fields_arg, list):
return "$$$$ Argument Error: any_true requires arg1 to be a list of field names"
has_undefined = False
has_na = False
resolved = []
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":
has_undefined = True
elif field_value == "N/A":
has_na = True
else:
resolved.append(field_value)
if not resolved:
return dominant_no_value(has_undefined, has_na)
condition = any(resolved)
result_if_true = resolve_value(args[2])
result_if_false = resolve_value(args[3])
elif operator == "is_defined": elif operator == "is_defined":
if len(args) != 4: if len(args) != 4:
return "$$$$ Argument Error: is_defined requires 4 arguments" return "$$$$ Argument Error: is_defined requires 4 arguments"
value = resolve_value(args[1]) value = resolve_value(args[1])
condition = (value is not None and value != "undefined" and value != "N/A") condition = (value is not None and value != "undefined")
result_if_true = resolve_value(args[2]) result_if_true = resolve_value(args[2])
result_if_false = resolve_value(args[3]) result_if_false = resolve_value(args[3])
@@ -929,7 +702,7 @@ def _execute_custom_function(function_name, args, output_inclusion):
for field_name in fields_arg: for field_name in fields_arg:
field_value = get_value_from_inclusion(output_inclusion, field_name) field_value = get_value_from_inclusion(output_inclusion, field_name)
if field_value is None or field_value == "undefined" or field_value == "N/A": if field_value is None or field_value == "undefined":
condition = False condition = False
break break
else: else:
@@ -944,12 +717,8 @@ def _execute_custom_function(function_name, args, output_inclusion):
value1 = resolve_value(args[1]) value1 = resolve_value(args[1])
value2 = resolve_value(args[2]) value2 = resolve_value(args[2])
v1_undef = value1 is None or value1 == "undefined" if value1 is None or value1 == "undefined" or value2 is None or value2 == "undefined":
v2_undef = value2 is None or value2 == "undefined" return "undefined"
status = dominant_no_value(v1_undef or v2_undef,
value1 == "N/A" or value2 == "N/A")
if status:
return status
condition = (value1 == value2) condition = (value1 == value2)
result_if_true = resolve_value(args[3]) result_if_true = resolve_value(args[3])
@@ -961,58 +730,13 @@ def _execute_custom_function(function_name, args, output_inclusion):
value1 = resolve_value(args[1]) value1 = resolve_value(args[1])
value2 = resolve_value(args[2]) value2 = resolve_value(args[2])
v1_undef = value1 is None or value1 == "undefined" if value1 is None or value1 == "undefined" or value2 is None or value2 == "undefined":
v2_undef = value2 is None or value2 == "undefined" return "undefined"
status = dominant_no_value(v1_undef or v2_undef,
value1 == "N/A" or value2 == "N/A")
if status:
return status
condition = (value1 != value2) condition = (value1 != value2)
result_if_true = resolve_value(args[3]) result_if_true = resolve_value(args[3])
result_if_false = resolve_value(args[4]) result_if_false = resolve_value(args[4])
elif operator in (">", ">=", "<", "<="):
if len(args) != 5:
return f"$$$$ Argument Error: {operator} requires 5 arguments"
value1 = resolve_value(args[1])
value2 = resolve_value(args[2])
v1_undef = value1 is None or value1 == "undefined"
v2_undef = value2 is None or value2 == "undefined"
status = dominant_no_value(v1_undef or v2_undef,
value1 == "N/A" or value2 == "N/A")
if status:
return status
# Si l'un est numérique, tenter de convertir l'autre ; sinon comparer en string
cmp1, cmp2 = value1, value2
if isinstance(value1, (int, float)) and not isinstance(value2, (int, float)):
try:
cmp2 = float(value2)
except (ValueError, TypeError):
cmp1 = str(value1)
elif isinstance(value2, (int, float)) and not isinstance(value1, (int, float)):
try:
cmp1 = float(value1)
except (ValueError, TypeError):
cmp2 = str(value2)
try:
if operator == ">":
condition = (cmp1 > cmp2)
elif operator == ">=":
condition = (cmp1 >= cmp2)
elif operator == "<":
condition = (cmp1 < cmp2)
else:
condition = (cmp1 <= cmp2)
except TypeError:
return f"$$$$ Comparison Error: cannot compare {type(cmp1).__name__} and {type(cmp2).__name__}"
result_if_true = resolve_value(args[3])
result_if_false = resolve_value(args[4])
else: else:
return f"$$$$ Unknown Operator: {operator}" return f"$$$$ Unknown Operator: {operator}"
@@ -1035,8 +759,6 @@ def process_inclusions_mapping(output_inclusion, inclusion_data, record_data, re
if condition_value is None or condition_value == "undefined": if condition_value is None or condition_value == "undefined":
final_value = "undefined" final_value = "undefined"
elif condition_value == "N/A":
final_value = "N/A"
elif not isinstance(condition_value, bool): elif not isinstance(condition_value, bool):
final_value = "$$$$ Condition Field Error" final_value = "$$$$ Condition Field Error"
elif not condition_value: elif not condition_value:
@@ -1240,13 +962,6 @@ def get_all_questionnaires_by_patient(patient_id, record_data):
response.raise_for_status() response.raise_for_status()
response_data = response.json() response_data = response.json()
# First pass: count occurrences of each q_id to detect duplicates at lookup time
q_id_counts = {}
for item in response_data:
q_id = get_nested_value(item, path=["questionnaire", "id"])
if q_id:
q_id_counts[q_id] = q_id_counts.get(q_id, 0) + 1
# Build dictionary with questionnaire metadata for searching # Build dictionary with questionnaire metadata for searching
results = {} results = {}
for item in response_data: for item in response_data:
@@ -1254,17 +969,6 @@ def get_all_questionnaires_by_patient(patient_id, record_data):
q_name = get_nested_value(item, path=["questionnaire", "name"]) q_name = get_nested_value(item, path=["questionnaire", "name"])
q_category = get_nested_value(item, path=["questionnaire", "category"]) q_category = get_nested_value(item, path=["questionnaire", "category"])
answers = get_nested_value(item, path=["answers"], default={}) answers = get_nested_value(item, path=["answers"], default={})
# ── DRAFT ANSWERS FILTER ────────────────────────────────────────────────
# "Draft" answers have a missing or mismatched subject (patient id):
# they are not yet submitted and should not be treated as valid patient data.
# Active by default; use --include-drafts CLI flag to disable.
if not _include_drafts:
answers_patient = answers.get("subject") if isinstance(answers, dict) else None
if not answers_patient or answers_patient != patient_id:
answers = {}
# ────────────────────────────────────────────────────────────────────────
if q_id: if q_id:
results[q_id] = { results[q_id] = {
"questionnaire": { "questionnaire": {
@@ -1272,8 +976,7 @@ def get_all_questionnaires_by_patient(patient_id, record_data):
"name": q_name, "name": q_name,
"category": q_category "category": q_category
}, },
"answers": answers, "answers": answers
"_count": q_id_counts[q_id]
} }
return results return results
@@ -1432,12 +1135,8 @@ def _process_inclusion_data(inclusion, organization):
output_inclusion = {} output_inclusion = {}
# --- Prepare all data sources --- # --- Prepare all data sources ---
# 1. Launch Visit Search asynchronously (it's slow, ~5s) — only if enabled by user # 1. 6-month visit loading disabled on this branch (No-6-Month-Visit)
# We use run_with_context to pass the patient identity to the new thread # visit_future = subtasks_thread_pool.submit(run_with_context, search_visit_by_pseudo_and_order, ctx, pseudo, 2)
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 # 2. Prepare inclusion_data: enrich inclusion with organization info
inclusion_data = dict(inclusion) inclusion_data = dict(inclusion)
@@ -1461,11 +1160,8 @@ def _process_inclusion_data(inclusion, organization):
logging.error(f"Error fetching request data for patient {patient_id}: {e}") logging.error(f"Error fetching request data for patient {patient_id}: {e}")
request_data = None request_data = None
try: # 6-month visit loading disabled on this branch (No-6-Month-Visit)
six_month_visit_data = visit_future.result() if visit_future is not None else {} six_month_visit_data = None
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 all fields from configuration ---
process_inclusions_mapping(output_inclusion, inclusion_data, record_data, request_data, all_questionnaires, six_month_visit_data) process_inclusions_mapping(output_inclusion, inclusion_data, record_data, request_data, all_questionnaires, six_month_visit_data)
@@ -1519,7 +1215,7 @@ def main():
load_organizations_mapping_config() load_organizations_mapping_config()
# Completely externalized Excel-only workflow # Completely externalized Excel-only workflow
export_excel_only(sys.argv, get_output_filename(INCLUSIONS_FILE_NAME), get_output_filename(ORGANIZATIONS_FILE_NAME), export_excel_only(sys.argv, INCLUSIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME,
inclusions_mapping_config, organizations_mapping_config) inclusions_mapping_config, organizations_mapping_config)
return return
@@ -1532,19 +1228,10 @@ def main():
if login_status == "Exit": if login_status == "Exit":
return return
print()
ask_fetch_six_month_visit()
print() print()
number_of_threads = int((questionary.text("Number of threads :", default="12", number_of_threads = int((questionary.text("Number of threads :", default="12",
validate=lambda x: x.isdigit() and 0 < int(x) <= MAX_THREADS).ask())) validate=lambda x: x.isdigit() and 0 < int(x) <= MAX_THREADS).ask()))
print()
ask_on_retry_exhausted()
print()
wait_for_scheduled_launch()
print() print()
load_inclusions_mapping_config() load_inclusions_mapping_config()
load_organizations_mapping_config() load_organizations_mapping_config()
@@ -1603,14 +1290,6 @@ def main():
inclusions_total_count = sum(org.get('patients_count', 0) for org in organizations_list) 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', ''))) organizations_list.sort(key=lambda org: (-org.get('patients_count', 0), org.get('name', '')))
# ╔══════════════════════════════════════════════════════════════════╗
# ║ TEST INSTRUMENTATION — REMOVE BEFORE PRODUCTION ║
# ║ Limits the table to organizations at ranks 33 and 34 ║
# ║ (indices 32 and 33, after descending sort by patients_count) ║
# ╚══════════════════════════════════════════════════════════════════╝
# organizations_list = [org for i, org in enumerate(organizations_list) if i in (32, 33)]
# ══════════════════════════════════════════════════════════════════
number_of_organizations = len(organizations_list) number_of_organizations = len(organizations_list)
print(f"{inclusions_total_count} Inclusions in {number_of_organizations} Organizations...") print(f"{inclusions_total_count} Inclusions in {number_of_organizations} Organizations...")
print() print()
@@ -1664,7 +1343,7 @@ def main():
has_coherence_critical, has_regression_critical = run_quality_checks( has_coherence_critical, has_regression_critical = run_quality_checks(
current_inclusions=output_inclusions, # list: données en mémoire (nouvellement collectées) 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 organizations_list=organizations_list, # list: données en mémoire avec compteurs
old_inclusions_filename=get_output_filename(INCLUSIONS_FILE_NAME) # str: current file on disk (with suffix if any) old_inclusions_filename=INCLUSIONS_FILE_NAME # str: "endobest_inclusions.json" (version courante sur disque)
) )
# === CHECK FOR CRITICAL ISSUES AND ASK USER CONFIRMATION === # === CHECK FOR CRITICAL ISSUES AND ASK USER CONFIRMATION ===
@@ -1689,9 +1368,9 @@ def main():
# === WRITE NEW FILES === # === WRITE NEW FILES ===
print("Writing files...") print("Writing files...")
with open(get_output_filename(INCLUSIONS_FILE_NAME), 'w', encoding='utf-8') as f_json: with open(INCLUSIONS_FILE_NAME, 'w', encoding='utf-8') as f_json:
json.dump(output_inclusions, f_json, indent=4, ensure_ascii=False) json.dump(output_inclusions, f_json, indent=4, ensure_ascii=False)
with open(get_output_filename(ORGANIZATIONS_FILE_NAME), 'w', encoding='utf-8') as f_json: with open(ORGANIZATIONS_FILE_NAME, 'w', encoding='utf-8') as f_json:
json.dump(organizations_list, f_json, indent=4, ensure_ascii=False) json.dump(organizations_list, f_json, indent=4, ensure_ascii=False)
console.print("[green]✓ Data saved to JSON files[/green]") console.print("[green]✓ Data saved to JSON files[/green]")

View File

@@ -1,2 +0,0 @@
@echo off
eb_dashboard.exe --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix %*

View File

@@ -1,3 +0,0 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix %*

View File

@@ -34,7 +34,7 @@ try:
except ImportError: except ImportError:
xw = None xw = None
from eb_dashboard_utils import get_nested_value, get_config_path, get_dashboard_config_path, get_output_filename from eb_dashboard_utils import get_nested_value, get_config_path
from eb_dashboard_constants import ( from eb_dashboard_constants import (
INCLUSIONS_FILE_NAME, INCLUSIONS_FILE_NAME,
ORGANIZATIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME,
@@ -117,7 +117,7 @@ def load_excel_export_config(console_instance=None):
if console_instance: if console_instance:
console = console_instance console = console_instance
config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME) config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME)
error_messages = [] error_messages = []
try: try:
@@ -399,7 +399,7 @@ def export_to_excel(inclusions_data, organizations_data, excel_config,
output_template = workbook_config.get("output_file_name_template") output_template = workbook_config.get("output_file_name_template")
if_output_exists = workbook_config.get("if_output_exists", OUTPUT_ACTION_OVERWRITE) if_output_exists = workbook_config.get("if_output_exists", OUTPUT_ACTION_OVERWRITE)
# Resolve output filename, then apply --add-suffix if provided # Resolve output filename
try: try:
output_filename = output_template.format(**template_vars) output_filename = output_template.format(**template_vars)
except KeyError as e: except KeyError as e:
@@ -407,7 +407,6 @@ def export_to_excel(inclusions_data, organizations_data, excel_config,
error_count += 1 error_count += 1
continue continue
output_filename = get_output_filename(output_filename)
output_path = os.path.join(EXCEL_OUTPUT_FOLDER, output_filename) output_path = os.path.join(EXCEL_OUTPUT_FOLDER, output_filename)
# Log workbook processing start # Log workbook processing start
@@ -557,7 +556,7 @@ def _prepare_template_variables():
""" """
# Get UTC timestamp from inclusions file # Get UTC timestamp from inclusions file
# Use constant from eb_dashboard_constants (SINGLE SOURCE OF TRUTH) # Use constant from eb_dashboard_constants (SINGLE SOURCE OF TRUTH)
inclusions_file = get_output_filename(INCLUSIONS_FILE_NAME) inclusions_file = INCLUSIONS_FILE_NAME
if os.path.exists(inclusions_file): if os.path.exists(inclusions_file):
file_mtime = os.path.getmtime(inclusions_file) file_mtime = os.path.getmtime(inclusions_file)
extract_date_time_utc = datetime.fromtimestamp(file_mtime, tz=timezone.utc) extract_date_time_utc = datetime.fromtimestamp(file_mtime, tz=timezone.utc)
@@ -755,11 +754,7 @@ def _apply_value_replacement(value, replacements):
return value return value
for value_before, value_after in replacements: for value_before, value_after in replacements:
# bool is a subclass of int in Python (True == 1, False == 0): if value == value_before: # Strict equality
# reject the match if one side is bool and the other is not.
if isinstance(value, bool) != isinstance(value_before, bool):
continue
if value == value_before:
return value_after return value_after
return value # No match, return original return value # No match, return original
@@ -1922,9 +1917,9 @@ def export_excel_only(sys_argv,
global console global console
if not inclusions_filename: if not inclusions_filename:
inclusions_filename = get_output_filename(INCLUSIONS_FILE_NAME) inclusions_filename = INCLUSIONS_FILE_NAME
if not organizations_filename: if not organizations_filename:
organizations_filename = get_output_filename(ORGANIZATIONS_FILE_NAME) organizations_filename = ORGANIZATIONS_FILE_NAME
print() print()
console.print("[bold cyan]═══ EXCEL ONLY MODE ═══[/bold cyan]\n") console.print("[bold cyan]═══ EXCEL ONLY MODE ═══[/bold cyan]\n")
@@ -2039,8 +2034,8 @@ def run_normal_mode_export(excel_enabled, excel_config,
try: try:
# Load JSONs from filesystem to ensure data consistency with what was written # Load JSONs from filesystem to ensure data consistency with what was written
# Use constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH) # Use constants imported from eb_dashboard_constants.py (SINGLE SOURCE OF TRUTH)
inclusions_from_fs = _load_json_file_internal(get_output_filename(INCLUSIONS_FILE_NAME)) inclusions_from_fs = _load_json_file_internal(INCLUSIONS_FILE_NAME)
organizations_from_fs = _load_json_file_internal(get_output_filename(ORGANIZATIONS_FILE_NAME)) organizations_from_fs = _load_json_file_internal(ORGANIZATIONS_FILE_NAME)
if inclusions_from_fs is None or organizations_from_fs is None: if inclusions_from_fs is None or organizations_from_fs is None:
error_msg = "Could not load data files for Excel export" error_msg = "Could not load data files for Excel export"

View File

@@ -1,3 +0,0 @@
@echo off
eb_dashboard.exe --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix debug %*

View File

@@ -1,4 +0,0 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix debug %*

View File

@@ -17,7 +17,7 @@ import shutil
import openpyxl import openpyxl
from rich.console import Console from rich.console import Console
from eb_dashboard_utils import get_nested_value, get_old_filename as _get_old_filename, get_config_path, get_dashboard_config_path, get_output_filename from eb_dashboard_utils import get_nested_value, get_old_filename as _get_old_filename, get_config_path
from eb_dashboard_constants import ( from eb_dashboard_constants import (
INCLUSIONS_FILE_NAME, INCLUSIONS_FILE_NAME,
ORGANIZATIONS_FILE_NAME, ORGANIZATIONS_FILE_NAME,
@@ -93,7 +93,7 @@ def load_regression_check_config(console_instance=None):
if console_instance: if console_instance:
console = console_instance console = console_instance
config_path = get_dashboard_config_path(DASHBOARD_CONFIG_FILE_NAME) config_path = os.path.join(get_config_path(), DASHBOARD_CONFIG_FILE_NAME)
try: try:
workbook = openpyxl.load_workbook(config_path) workbook = openpyxl.load_workbook(config_path)
@@ -318,10 +318,10 @@ def run_check_only_mode(sys_argv):
# Run quality checks (will load all files internally) # Run quality checks (will load all files internally)
print() print()
old_inclusions_file = _get_old_filename(get_output_filename(INCLUSIONS_FILE_NAME), OLD_FILE_SUFFIX) old_inclusions_file = _get_old_filename(INCLUSIONS_FILE_NAME, OLD_FILE_SUFFIX)
has_coherence_critical, has_regression_critical = run_quality_checks( has_coherence_critical, has_regression_critical = run_quality_checks(
current_inclusions=get_output_filename(INCLUSIONS_FILE_NAME), current_inclusions=INCLUSIONS_FILE_NAME,
organizations_list=get_output_filename(ORGANIZATIONS_FILE_NAME), organizations_list=ORGANIZATIONS_FILE_NAME,
old_inclusions_filename=old_inclusions_file old_inclusions_filename=old_inclusions_file
) )
@@ -370,8 +370,8 @@ def backup_output_files():
except Exception as e: except Exception as e:
logging.warning(f"Could not backup {source}: {e}") logging.warning(f"Could not backup {source}: {e}")
_backup_file_silent(get_output_filename(INCLUSIONS_FILE_NAME), _get_old_filename(get_output_filename(INCLUSIONS_FILE_NAME), OLD_FILE_SUFFIX)) _backup_file_silent(INCLUSIONS_FILE_NAME, _get_old_filename(INCLUSIONS_FILE_NAME, OLD_FILE_SUFFIX))
_backup_file_silent(get_output_filename(ORGANIZATIONS_FILE_NAME), _get_old_filename(get_output_filename(ORGANIZATIONS_FILE_NAME), OLD_FILE_SUFFIX)) _backup_file_silent(ORGANIZATIONS_FILE_NAME, _get_old_filename(ORGANIZATIONS_FILE_NAME, OLD_FILE_SUFFIX))
# ============================================================================ # ============================================================================
@@ -431,11 +431,6 @@ def coherence_check(output_inclusions, organizations_list):
continue continue
patients += 1 patients += 1
# API statistics exclude non-consented from status sub-counters only
if get_nested_value(inclusion, ["Inclusion", "Consent_Signed"]) is not True:
continue
status = get_nested_value(inclusion, ["Inclusion", "Inclusion_Status"], default="") status = get_nested_value(inclusion, ["Inclusion", "Inclusion_Status"], default="")
if isinstance(status, str): if isinstance(status, str):

View File

@@ -204,43 +204,6 @@ def get_config_path():
return CONFIG_FOLDER_NAME return CONFIG_FOLDER_NAME
_dashboard_config_path_override = None
def set_dashboard_config_path_override(path):
"""Sets a global override for the dashboard config file path (used by --config CLI arg)."""
global _dashboard_config_path_override
_dashboard_config_path_override = path
def get_dashboard_config_path(config_file_name):
"""Returns the dashboard config file path, respecting any --config CLI override."""
if _dashboard_config_path_override:
return _dashboard_config_path_override
return os.path.join(get_config_path(), config_file_name)
_output_file_suffix = None
def set_output_file_suffix(suffix):
"""Sets the global output file suffix (used by --add-suffix CLI arg)."""
global _output_file_suffix
_output_file_suffix = suffix
def get_output_filename(base_name):
"""Returns the output filename with the suffix inserted before the extension.
Example: get_output_filename("endobest_inclusions.json") with suffix "A"
"endobest_inclusions_A.json"
"""
if not _output_file_suffix:
return base_name
name, ext = os.path.splitext(base_name)
return f"{name}_{_output_file_suffix}{ext}"
def get_old_filename(current_filename, old_suffix="_old"): def get_old_filename(current_filename, old_suffix="_old"):
"""Generate old backup filename from current filename. """Generate old backup filename from current filename.

View File

@@ -1,3 +0,0 @@
@echo off
eb_dashboard.exe --include-drafts --add-suffix with_drafts %*

View File

@@ -1,4 +0,0 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --include-drafts --add-suffix with_drafts %*

View File

@@ -1,3 +0,0 @@
@echo off
eb_dashboard.exe --include-drafts --excel-only --add-suffix with_drafts %*

View File

@@ -1,4 +0,0 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --include-drafts --excel-only --add-suffix with_drafts %*

View File

@@ -1,3 +0,0 @@
@echo off
eb_dashboard.exe --include-drafts --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix with_drafts_debug %*

View File

@@ -1,4 +0,0 @@
@echo off
call C:\PythonProjects\.rcvenv\Scripts\activate.bat
python eb_dashboard.py --include-drafts --excel-only --config "Endobest_Dashboard_Config - Debug.xlsx" --add-suffix with_drafts_debug %*

Binary file not shown.

228230
endobest_inclusions.json Normal file

File diff suppressed because it is too large Load Diff

248294
endobest_inclusions_old.json Normal file

File diff suppressed because it is too large Load Diff

704
endobest_organizations.json Normal file
View File

@@ -0,0 +1,704 @@
[
{
"id": "5703b3d6-e415-40b0-90ea-b168835fd720",
"name": "HOPITAL PRIVE NATECIA",
"Center_Name": "Hôpital Privé Natécia",
"patients_count": 169,
"preincluded_count": 0,
"included_count": 167,
"prematurely_terminated_count": 2
},
{
"id": "1de71a30-840b-4c4b-84fc-281ce1b4a5e1",
"name": "SANTE ATLANTIQUE",
"Center_Name": "Clinique Santé Atlantique",
"patients_count": 159,
"preincluded_count": 1,
"included_count": 157,
"prematurely_terminated_count": 1
},
{
"id": "5e6d7afa-6532-495f-a84a-34f644aeaa0f",
"name": "CLINIQUE BELLEDONNE",
"Center_Name": "Clinique Belledonne",
"patients_count": 158,
"preincluded_count": 4,
"included_count": 153,
"prematurely_terminated_count": 1
},
{
"id": "026a6d39-552f-44b9-8a2d-1ecd705f9e08",
"name": "HOPITAL AMERICAIN",
"Center_Name": "Hôpital Américain de Paris",
"patients_count": 158,
"preincluded_count": 1,
"included_count": 157,
"prematurely_terminated_count": 0
},
{
"id": "bf0f96c1-8bbc-4f2c-b360-4a5b27995a12",
"name": "SA CLINIQUE TIVOLI-DUCOS",
"Center_Name": "Clinique TIVOLI",
"patients_count": 155,
"preincluded_count": 1,
"included_count": 154,
"prematurely_terminated_count": 0
},
{
"id": "aba63d11-0dd7-40e8-a652-384c311bb358",
"name": "HOPITAL LYON SUD - HOSPICES CIVILS DE LYON",
"Center_Name": "Hôpital Lyon Sud - Hospices Civils de Lyon",
"patients_count": 127,
"preincluded_count": 0,
"included_count": 127,
"prematurely_terminated_count": 0
},
{
"id": "8488229d-f426-4bdd-923c-f638041b223c",
"name": "HOPITAL JEANNE DE FLANDRE DU CHU DE LILLE",
"Center_Name": "CHU de Lille - Hôpital Jeanne de Flandre",
"patients_count": 91,
"preincluded_count": 1,
"included_count": 90,
"prematurely_terminated_count": 0
},
{
"id": "31665f8d-0f46-44dc-931f-e3aed8879965",
"name": "HOPITAL MAISON BLANCHE CHU REIMS",
"Center_Name": "CHU de Reims - Hôpital Maison Blanche",
"patients_count": 88,
"preincluded_count": 0,
"included_count": 88,
"prematurely_terminated_count": 0
},
{
"id": "b77b301c-10fa-4eca-bb5b-51506766e158",
"name": "HOPITAL CHARLES NICOLLE CHU ROUEN",
"Center_Name": "CHU de Rouen Normandie - Hôpital Charles-Nicolle",
"patients_count": 84,
"preincluded_count": 1,
"included_count": 83,
"prematurely_terminated_count": 0
},
{
"id": "743d0a1a-7edf-4fe6-9d65-0c14363f2153",
"name": "CENTRE HOSPITALIER UNIVERSITAIRE JEAN MINJOZ BESANCON",
"Center_Name": "CHU Jean Minjoz Besançon - Pôle Mère-Femme",
"patients_count": 81,
"preincluded_count": 0,
"included_count": 81,
"prematurely_terminated_count": 0
},
{
"id": "855d7aab-9736-40b7-b9ff-dcc6438890ef",
"name": "HOPITAL CROIX-ROUSSE - HOSPICES CIVILS DE LYON",
"Center_Name": "Hôpital Croix-Rousse - Hospices Civils de Lyon",
"patients_count": 68,
"preincluded_count": 5,
"included_count": 62,
"prematurely_terminated_count": 1
},
{
"id": "74a05936-8fb6-4662-8e95-fd1c91621bf2",
"name": "GHU APHP SORBONNE UNIVERSITE SITE TENON",
"Center_Name": "GHU APHP - Hôpital Tenon",
"patients_count": 62,
"preincluded_count": 5,
"included_count": 56,
"prematurely_terminated_count": 1
},
{
"id": "76d2ebcd-5482-4c5d-8221-c94d1de7261e",
"name": "CENTRE HOSPITALIER ANNECY-GENEVOIS SITE ANNECY",
"Center_Name": "CH Annecy Genevois Site dAnnecy",
"patients_count": 60,
"preincluded_count": 0,
"included_count": 60,
"prematurely_terminated_count": 0
},
{
"id": "006740fa-696c-4ffb-8b77-7a60d0e23617",
"name": "HOPITAL PRIVÉ LE BOIS",
"Center_Name": "Hôpital Privé le Bois - Lille",
"patients_count": 60,
"preincluded_count": 1,
"included_count": 59,
"prematurely_terminated_count": 0
},
{
"id": "a4b3dfea-1220-4987-b287-f58fde1d0ee5",
"name": "CENTRE HOSPITALIER UNIVERSITAIRE COTE DE NACRE",
"Center_Name": "CHU de Caen",
"patients_count": 59,
"preincluded_count": 0,
"included_count": 59,
"prematurely_terminated_count": 0
},
{
"id": "4aef30cb-778b-409a-b6e2-898bfd91c850",
"name": "CENTRE HOSPITALIER GENERAL DE VALENCIENNES",
"Center_Name": "CH de Valenciennes",
"patients_count": 57,
"preincluded_count": 3,
"included_count": 54,
"prematurely_terminated_count": 0
},
{
"id": "f7d3526b-18d6-43af-b52c-5ca23bac798e",
"name": "CENTRE HOSPITALIER REGIONAL D ANGERS",
"Center_Name": "CHR dAngers",
"patients_count": 50,
"preincluded_count": 0,
"included_count": 50,
"prematurely_terminated_count": 0
},
{
"id": "1fd6c63b-d909-45d8-8c70-ecb12501c1d1",
"name": "HOPITAL DE HAUTEPIERRE",
"Center_Name": "CHRU Strasbourg - Hôpital de Hautepierre",
"patients_count": 44,
"preincluded_count": 2,
"included_count": 42,
"prematurely_terminated_count": 0
},
{
"id": "d17ddcc4-64db-4cf5-83c5-146f9a060254",
"name": "CENTRE HOSPITALIER DE CALAIS.",
"Center_Name": "CH de Calais",
"patients_count": 42,
"preincluded_count": 0,
"included_count": 42,
"prematurely_terminated_count": 0
},
{
"id": "f6665185-b1ae-4847-a378-652d35158cee",
"name": "CLINIQUE BELHARRA",
"Center_Name": "Clinique Belharra",
"patients_count": 40,
"preincluded_count": 0,
"included_count": 40,
"prematurely_terminated_count": 0
},
{
"id": "fe25553c-4894-4291-b303-c19d2a1c6f0f",
"name": "CHU SITE FELIX GUYON (SAINT DENIS)",
"Center_Name": "CHU la Réunion - Site Félix Guyon (SAINT DENIS)",
"patients_count": 39,
"preincluded_count": 6,
"included_count": 33,
"prematurely_terminated_count": 0
},
{
"id": "12daafee-970b-4781-9121-994d06e3a766",
"name": "CHU DE NICE HOPITAL DE L'ARCHET",
"Center_Name": "CHU Nice Archet",
"patients_count": 38,
"preincluded_count": 0,
"included_count": 38,
"prematurely_terminated_count": 0
},
{
"id": "f07a7374-d731-4fcf-86e9-2f36e5faf342",
"name": "CHU MONTPELLIER HOPITAL ARNAUD DE VILLENEUVE",
"Center_Name": "CHU Montpellier - Hôpital Arnaud de Villeneuve",
"patients_count": 38,
"preincluded_count": 0,
"included_count": 38,
"prematurely_terminated_count": 0
},
{
"id": "9c64545a-b622-4ef9-be0f-f5cc21b9cd66",
"name": "HOPITAL NORD - CHU DE GRENOBLE ALPES",
"Center_Name": "CHU de Grenoble - Hôpital Nord",
"patients_count": 36,
"preincluded_count": 0,
"included_count": 35,
"prematurely_terminated_count": 1
},
{
"id": "c03b88b5-3cd2-4336-9048-19c239baf5ec",
"name": "CHRU DE RENNES SITE HOPITAL SUD",
"Center_Name": "CHU Rennes",
"patients_count": 34,
"preincluded_count": 0,
"included_count": 34,
"prematurely_terminated_count": 0
},
{
"id": "b0ba921e-e24a-41c2-b769-a2aa108cdd58",
"name": "CENTRE HOSPITALIER LES ESCARTONS A BRIANCON",
"Center_Name": "CH des Escartons de Briançon",
"patients_count": 32,
"preincluded_count": 1,
"included_count": 31,
"prematurely_terminated_count": 0
},
{
"id": "b5f30dc5-da3f-4f8b-9a39-33f0fefb7196",
"name": "SCM RX TOULOUSE CLINIQUE PASTEUR",
"Center_Name": "Clinique Pasteur Toulouse",
"patients_count": 32,
"preincluded_count": 2,
"included_count": 30,
"prematurely_terminated_count": 0
},
{
"id": "817429dd-0d7c-4df9-ad70-2f19c205e05a",
"name": "CENTRE HOSPITALIER - FALCONAJA - BASTIA",
"Center_Name": "CH Bastia",
"patients_count": 29,
"preincluded_count": 4,
"included_count": 25,
"prematurely_terminated_count": 0
},
{
"id": "4f943b00-9306-418a-a853-1d97dff71172",
"name": "HOPITAL EUROPEEN",
"Center_Name": "Hôpital Européen Marseille",
"patients_count": 28,
"preincluded_count": 0,
"included_count": 28,
"prematurely_terminated_count": 0
},
{
"id": "002312ec-69fb-4582-9e8f-7c266e5479d2",
"name": "HOPITAL NORD - CHU DE SAINT-ETIENNE",
"Center_Name": "CHU de Saint-Étienne - Hôpital Nord",
"patients_count": 27,
"preincluded_count": 2,
"included_count": 25,
"prematurely_terminated_count": 0
},
{
"id": "0ce39aac-6b5a-418b-8430-c4432e6cd78f",
"name": "HOPITAL DE RANGUEIL CHU TOULOUSE",
"Center_Name": "CHU de Toulouse - Hôpital Rangueil",
"patients_count": 24,
"preincluded_count": 1,
"included_count": 23,
"prematurely_terminated_count": 0
},
{
"id": "8ef8564e-836c-4c03-b4ec-8e28fa76c3c0",
"name": "HOPITAL ESTAING - CHU CLERMONT-FERRAND",
"Center_Name": "CHU Clermont-Ferrand - Site Estaing",
"patients_count": 23,
"preincluded_count": 1,
"included_count": 22,
"prematurely_terminated_count": 0
},
{
"id": "546f981f-6f7d-40c8-9b97-a1a3c46e6674",
"name": "GROUPE HOSPITALIER DE LA REGION DE MULHOUSE ET SUD ALSACE",
"Center_Name": "GHR Mulhouse Sud Alsace",
"patients_count": 20,
"preincluded_count": 0,
"included_count": 20,
"prematurely_terminated_count": 0
},
{
"id": "eb9c561d-37b6-485c-b351-9e19515cb98d",
"name": "GROUPE HOSPITALIER PELLEGRIN - CHU",
"Center_Name": "CHU de Bordeaux - GH Pellegrin",
"patients_count": 20,
"preincluded_count": 0,
"included_count": 20,
"prematurely_terminated_count": 0
},
{
"id": "d1ef2ced-0206-4725-a08c-454d71e823ec",
"name": "HOPITAL DE LA MERE ET DE L'ENFANT",
"Center_Name": "CHU Limoges - Hôpital de la Mère et de l'Enfant",
"patients_count": 20,
"preincluded_count": 0,
"included_count": 20,
"prematurely_terminated_count": 0
},
{
"id": "7f43a297-b156-4fec-8ecf-d8d008b1a1d2",
"name": "HOPITAL PRIVE DIJON BOURGOGNE",
"Center_Name": "Centre Evidens - Hôpital privé Dijon Bourgogne",
"patients_count": 19,
"preincluded_count": 4,
"included_count": 15,
"prematurely_terminated_count": 0
},
{
"id": "d767fcd0-3f4c-4cdf-9cda-b829156d2f95",
"name": "HOPITAL PRIVE SUD CORSE",
"Center_Name": "Hôpital Privé Sud Corse",
"patients_count": 18,
"preincluded_count": 0,
"included_count": 18,
"prematurely_terminated_count": 0
},
{
"id": "58d5b536-b326-4713-9137-bb6f852df0be",
"name": "CENTRE HOSPITALIER METROPOLE SAVOIE - CHAMBERY NH",
"Center_Name": "CH Métropole Savoie - Site Chambéry",
"patients_count": 16,
"preincluded_count": 0,
"included_count": 16,
"prematurely_terminated_count": 0
},
{
"id": "b4b35661-87c8-4333-bfb1-3ce4f1c77565",
"name": "CENTRE HOSPITALIER REGIONAL UNIVERSITAIRE BRETONNEAU",
"Center_Name": "CHRU Bretonneau",
"patients_count": 16,
"preincluded_count": 2,
"included_count": 14,
"prematurely_terminated_count": 0
},
{
"id": "454313d9-c624-43de-a8e3-84989d526403",
"name": "CLINIQUE MUTUALISTE LA SAGESSE RENNES",
"Center_Name": "Clinique la Sagesse - Rennes",
"patients_count": 15,
"preincluded_count": 0,
"included_count": 15,
"prematurely_terminated_count": 0
},
{
"id": "d981c078-189e-4831-a063-778733f7e582",
"name": "HOPITAL FOCH",
"Center_Name": "Hôpital Foch",
"patients_count": 15,
"preincluded_count": 1,
"included_count": 12,
"prematurely_terminated_count": 0
},
{
"id": "940c8425-fe53-45ac-a750-3e195df990e3",
"name": "HOPITAL PRIVE D'EURE ET LOIR",
"Center_Name": "Hôpital Privé d'Eure et Loir",
"patients_count": 14,
"preincluded_count": 1,
"included_count": 13,
"prematurely_terminated_count": 0
},
{
"id": "98cbbefe-920a-495a-981d-02093bd2253d",
"name": "CHU LA MILETRIE",
"Center_Name": "CHU de Poitiers La Miletrie",
"patients_count": 13,
"preincluded_count": 0,
"included_count": 13,
"prematurely_terminated_count": 0
},
{
"id": "38e9ef8a-5073-471a-a3df-14d834db2d12",
"name": "GHBS-SITE HÔPITAL DU SCORFF",
"Center_Name": "Groupe Hospitalier Bretagne Sud Lorient - GHBS - Lorient (SCORFF)",
"patients_count": 13,
"preincluded_count": 0,
"included_count": 13,
"prematurely_terminated_count": 0
},
{
"id": "6316db1a-4a90-4e45-a518-0095d63f7c36",
"name": "HOPITAL LE BOCAGE CHRU DIJON",
"Center_Name": "CHU Dijon",
"patients_count": 12,
"preincluded_count": 1,
"included_count": 10,
"prematurely_terminated_count": 1
},
{
"id": "f790c171-9523-48b6-a246-fdb26628eb6c",
"name": "CHRU NANCY - MATERNITE",
"Center_Name": "CHRU de Nancy - Maternité",
"patients_count": 11,
"preincluded_count": 1,
"included_count": 10,
"prematurely_terminated_count": 0
},
{
"id": "7b8c5362-7c7a-4d93-8b12-58b8e56020d1",
"name": "HOPITAL JACQUES MONOD CH LE HAVRE",
"Center_Name": "GH du Havre - Hôpital Jacques Monod",
"patients_count": 11,
"preincluded_count": 0,
"included_count": 11,
"prematurely_terminated_count": 0
},
{
"id": "6953af68-e30e-437f-93cf-19ed0932c350",
"name": "CHRU D'ORLEANS - HOPITAL DE LA SOURCE",
"Center_Name": "CHU d'Orléans",
"patients_count": 10,
"preincluded_count": 0,
"included_count": 10,
"prematurely_terminated_count": 0
},
{
"id": "f13560a8-3bf6-4322-bdae-332ee3bf0366",
"name": "POLYCLINIQUE JEAN VILLAR",
"Center_Name": "Polyclinique Jean Villar",
"patients_count": 10,
"preincluded_count": 0,
"included_count": 10,
"prematurely_terminated_count": 0
},
{
"id": "da67fb6e-2ccc-4994-8fde-cc2ee2a8e9ca",
"name": "GHU AP-HP UNIVERSITE PARIS SACLAY SITE KREMLIN BICETRE",
"Center_Name": "GHU APHP - Hôpital Bicêtre",
"patients_count": 9,
"preincluded_count": 0,
"included_count": 7,
"prematurely_terminated_count": 0
},
{
"id": "56f427f0-be35-493b-8049-46b747c0105e",
"name": "CHU AMIENS SUD",
"Center_Name": "CHU Amiens Sud",
"patients_count": 7,
"preincluded_count": 3,
"included_count": 4,
"prematurely_terminated_count": 0
},
{
"id": "f784ee6f-27b5-4afe-ba23-4f839d96535c",
"name": "APHM HOPITAL NORD",
"Center_Name": "APHM Hôpital Nord",
"patients_count": 5,
"preincluded_count": 0,
"included_count": 5,
"prematurely_terminated_count": 0
},
{
"id": "1ac60b10-08de-4639-9a49-67167f85844e",
"name": "CENTRE HOSPITALIER DE LENS",
"Center_Name": "CH de Lens",
"patients_count": 5,
"preincluded_count": 2,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "9ba321be-65d0-4767-9865-a547905e647e",
"name": "HOPITAL DE LA CROIX SAINT SIMON",
"Center_Name": "GH Diaconesses Croix Saint-Simon",
"patients_count": 5,
"preincluded_count": 1,
"included_count": 4,
"prematurely_terminated_count": 0
},
{
"id": "cab5b1e9-d93a-46eb-ae35-a449ea719c65",
"name": "GRAND HOSP EST FRANCILIEN MARNE LA VALLEE SITE JOSSIGNY",
"Center_Name": "Grand Hôpital de l'Est Francilien - Site de Marne-la-Vallée",
"patients_count": 4,
"preincluded_count": 0,
"included_count": 4,
"prematurely_terminated_count": 0
},
{
"id": "96519108-b0b2-4a5f-999f-cccc4e4059c9",
"name": "CHU DE NANTES SITE HOTEL DIEU HOPITAL MERE ENFANT",
"Center_Name": "CHU de Nantes - Site Hôtel Dieu Hôpital Mère Enfant",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "fe7548b7-75a1-4e13-97a9-6dc865b04b25",
"name": "CHU SITE SUD ( SAINT PIERRE)",
"Center_Name": "CHU la Réunion - Site Sud (SAINT PIERRE)",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "e68c76de-33e8-49e3-978d-c5fb408e7fd5",
"name": "CLINIQUE AXIUM",
"Center_Name": "Clinique Axium",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "af09eaa0-7e08-48e3-990c-24ca2fbc3fdc",
"name": "SCP GYNECOLOGIE RIVE GAUCHE",
"Center_Name": "SCP Gynécologie Clinique Rive Gauche",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "0466558d-077c-4a51-9d25-2e15e00fabaf",
"name": "CENTRE HOSPITALIER DE CANNES SIMONE VEIL",
"Center_Name": "CH de Cannes Simone Veil",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "aa3e8684-07e0-45d9-9cee-144df2e4b430",
"name": "CENTRE HOSPITALIER DE PAU",
"Center_Name": "CH de Pau",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "13306b61-7ea6-4d60-b89c-b0df2d9d1605",
"name": "CENTRE HOSPITALIER VICTOR PROVO ROUBAIX",
"Center_Name": "CH Victor Provo Roubaix",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "8c27716f-edfe-4a4e-9189-09f39cc64395",
"name": "CHI DE MONT DE MARSAN ET DU PAYS DES SOURCES",
"Center_Name": "CHI Mont de Marsan",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "b74c365a-fc63-4763-b57d-abb609debd3b",
"name": "CTRE HOSPITALIER INTERCOMMUNAL POISSY ST GERMAIN SITE POISSY",
"Center_Name": "CHI Poissy St Germain en Laye - Site Poissy",
"patients_count": 2,
"preincluded_count": 1,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "4d271322-7945-42e8-801b-a3b25fc570be",
"name": "GROUPE HOSPITALIER PARIS SAINT JOSEPH",
"Center_Name": "Groupe Hospitalier Paris Saint-Joseph",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "d82d64eb-a1cf-4635-a99f-23def12c7cb5",
"name": "CHRU BREST SITE HOPITAL MORVAN",
"Center_Name": "CHRU Brest - Site Hôpital Morvan",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "a47dc8c0-dfea-4a4c-9bcc-1370616ff9c8",
"name": "CHU DE MARTINIQUE SITE MFME",
"Center_Name": "CHU de Martinique - Site MFME",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "384d3b28-f33e-43a6-9043-394aa88aead7",
"name": "CLINIQUE BOUCHARD",
"Center_Name": "Clinique Bouchard - Marseille",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "0f78459f-b598-4faa-81a1-085a439e8c0e",
"name": "HOPITAUX PRIVES ROUENNAIS MATHILDE",
"Center_Name": "Hôpitaux Privés Rouennais - Mathilde",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "02e42e01-56ab-418d-95df-cb53a7ef17b3",
"name": "APHM HOPITAL DE LA CONCEPTION",
"Center_Name": "APHM Hôpital de la Conception",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "2ba1f179-425e-4d88-97a7-88a097e639ab",
"name": "CENTRE HOSPITALIER DE CAYENNE",
"Center_Name": "CHU de Guyane - Site de Cayenne",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "3e8887e1-7608-40a6-b6f5-87c6fa5a0bd0",
"name": "CENTRE HOSPITALIER DE VERSAILLES ANDRE MIGNOT",
"Center_Name": "CH de Versailles",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "6af3784d-5d57-47c3-b924-0d13008ebd88",
"name": "CENTRE HOSPITALIER UNIVERSITAIRE DE POINTE-A-PITRE",
"Center_Name": "CHU de Guadeloupe - Pointe-à-Pitre",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "aa6af654-2cf2-4714-885d-a0e40b6f6f0a",
"name": "CHR METZ - THIONVILLE - HOPITAL DE MERCY",
"Center_Name": "CHR Metz - Thionville - Hôpital de Mercy",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "8441f339-4dd3-4c8c-bd2d-7751dc7fe43b",
"name": "CLINIQUE DU TERTRE ROUGE - POLE SANTE SUD",
"Center_Name": "Clinique du Tertre Rouge - Pôle Santé Sud",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "09c785f2-3deb-4a4a-a1a8-adbca5b767d7",
"name": "GHU APHP CENTRE-UNIVERSITE PARIS CITE SITE COCHIN PORT ROYAL",
"Center_Name": "APHP Site Cochin",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "5847c4fa-4025-4303-80eb-deaa07b80c0b",
"name": "GHU APHP SORBONNE UNIVERSITE SITE PITIE SALPETRIERE",
"Center_Name": "APHP Site Pitié Salpêtriere",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "dacc94bd-89b0-4cc4-b334-f684360abbe5",
"name": "HOPITAL PRIVE D ANTONY",
"Center_Name": "Hôpital Privé d'Antony",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
}
]

View File

@@ -0,0 +1,704 @@
[
{
"id": "5703b3d6-e415-40b0-90ea-b168835fd720",
"name": "HOPITAL PRIVE NATECIA",
"Center_Name": "Hôpital Privé Natécia",
"patients_count": 169,
"preincluded_count": 0,
"included_count": 167,
"prematurely_terminated_count": 2
},
{
"id": "1de71a30-840b-4c4b-84fc-281ce1b4a5e1",
"name": "SANTE ATLANTIQUE",
"Center_Name": "Clinique Santé Atlantique",
"patients_count": 159,
"preincluded_count": 1,
"included_count": 157,
"prematurely_terminated_count": 1
},
{
"id": "5e6d7afa-6532-495f-a84a-34f644aeaa0f",
"name": "CLINIQUE BELLEDONNE",
"Center_Name": "Clinique Belledonne",
"patients_count": 158,
"preincluded_count": 4,
"included_count": 153,
"prematurely_terminated_count": 1
},
{
"id": "026a6d39-552f-44b9-8a2d-1ecd705f9e08",
"name": "HOPITAL AMERICAIN",
"Center_Name": "Hôpital Américain de Paris",
"patients_count": 158,
"preincluded_count": 1,
"included_count": 157,
"prematurely_terminated_count": 0
},
{
"id": "bf0f96c1-8bbc-4f2c-b360-4a5b27995a12",
"name": "SA CLINIQUE TIVOLI-DUCOS",
"Center_Name": "Clinique TIVOLI",
"patients_count": 155,
"preincluded_count": 1,
"included_count": 154,
"prematurely_terminated_count": 0
},
{
"id": "aba63d11-0dd7-40e8-a652-384c311bb358",
"name": "HOPITAL LYON SUD - HOSPICES CIVILS DE LYON",
"Center_Name": "Hôpital Lyon Sud - Hospices Civils de Lyon",
"patients_count": 127,
"preincluded_count": 0,
"included_count": 127,
"prematurely_terminated_count": 0
},
{
"id": "8488229d-f426-4bdd-923c-f638041b223c",
"name": "HOPITAL JEANNE DE FLANDRE DU CHU DE LILLE",
"Center_Name": "CHU de Lille - Hôpital Jeanne de Flandre",
"patients_count": 91,
"preincluded_count": 1,
"included_count": 90,
"prematurely_terminated_count": 0
},
{
"id": "31665f8d-0f46-44dc-931f-e3aed8879965",
"name": "HOPITAL MAISON BLANCHE CHU REIMS",
"Center_Name": "CHU de Reims - Hôpital Maison Blanche",
"patients_count": 88,
"preincluded_count": 0,
"included_count": 88,
"prematurely_terminated_count": 0
},
{
"id": "b77b301c-10fa-4eca-bb5b-51506766e158",
"name": "HOPITAL CHARLES NICOLLE CHU ROUEN",
"Center_Name": "CHU de Rouen Normandie - Hôpital Charles-Nicolle",
"patients_count": 84,
"preincluded_count": 1,
"included_count": 83,
"prematurely_terminated_count": 0
},
{
"id": "743d0a1a-7edf-4fe6-9d65-0c14363f2153",
"name": "CENTRE HOSPITALIER UNIVERSITAIRE JEAN MINJOZ BESANCON",
"Center_Name": "CHU Jean Minjoz Besançon - Pôle Mère-Femme",
"patients_count": 81,
"preincluded_count": 0,
"included_count": 81,
"prematurely_terminated_count": 0
},
{
"id": "855d7aab-9736-40b7-b9ff-dcc6438890ef",
"name": "HOPITAL CROIX-ROUSSE - HOSPICES CIVILS DE LYON",
"Center_Name": "Hôpital Croix-Rousse - Hospices Civils de Lyon",
"patients_count": 68,
"preincluded_count": 5,
"included_count": 62,
"prematurely_terminated_count": 1
},
{
"id": "74a05936-8fb6-4662-8e95-fd1c91621bf2",
"name": "GHU APHP SORBONNE UNIVERSITE SITE TENON",
"Center_Name": "GHU APHP - Hôpital Tenon",
"patients_count": 62,
"preincluded_count": 5,
"included_count": 56,
"prematurely_terminated_count": 1
},
{
"id": "76d2ebcd-5482-4c5d-8221-c94d1de7261e",
"name": "CENTRE HOSPITALIER ANNECY-GENEVOIS SITE ANNECY",
"Center_Name": "CH Annecy Genevois Site dAnnecy",
"patients_count": 60,
"preincluded_count": 0,
"included_count": 60,
"prematurely_terminated_count": 0
},
{
"id": "006740fa-696c-4ffb-8b77-7a60d0e23617",
"name": "HOPITAL PRIVÉ LE BOIS",
"Center_Name": "Hôpital Privé le Bois - Lille",
"patients_count": 60,
"preincluded_count": 1,
"included_count": 59,
"prematurely_terminated_count": 0
},
{
"id": "a4b3dfea-1220-4987-b287-f58fde1d0ee5",
"name": "CENTRE HOSPITALIER UNIVERSITAIRE COTE DE NACRE",
"Center_Name": "CHU de Caen",
"patients_count": 59,
"preincluded_count": 0,
"included_count": 59,
"prematurely_terminated_count": 0
},
{
"id": "4aef30cb-778b-409a-b6e2-898bfd91c850",
"name": "CENTRE HOSPITALIER GENERAL DE VALENCIENNES",
"Center_Name": "CH de Valenciennes",
"patients_count": 57,
"preincluded_count": 3,
"included_count": 54,
"prematurely_terminated_count": 0
},
{
"id": "f7d3526b-18d6-43af-b52c-5ca23bac798e",
"name": "CENTRE HOSPITALIER REGIONAL D ANGERS",
"Center_Name": "CHR dAngers",
"patients_count": 50,
"preincluded_count": 0,
"included_count": 50,
"prematurely_terminated_count": 0
},
{
"id": "1fd6c63b-d909-45d8-8c70-ecb12501c1d1",
"name": "HOPITAL DE HAUTEPIERRE",
"Center_Name": "CHRU Strasbourg - Hôpital de Hautepierre",
"patients_count": 44,
"preincluded_count": 2,
"included_count": 42,
"prematurely_terminated_count": 0
},
{
"id": "d17ddcc4-64db-4cf5-83c5-146f9a060254",
"name": "CENTRE HOSPITALIER DE CALAIS.",
"Center_Name": "CH de Calais",
"patients_count": 42,
"preincluded_count": 0,
"included_count": 42,
"prematurely_terminated_count": 0
},
{
"id": "f6665185-b1ae-4847-a378-652d35158cee",
"name": "CLINIQUE BELHARRA",
"Center_Name": "Clinique Belharra",
"patients_count": 40,
"preincluded_count": 0,
"included_count": 40,
"prematurely_terminated_count": 0
},
{
"id": "fe25553c-4894-4291-b303-c19d2a1c6f0f",
"name": "CHU SITE FELIX GUYON (SAINT DENIS)",
"Center_Name": "CHU la Réunion - Site Félix Guyon (SAINT DENIS)",
"patients_count": 39,
"preincluded_count": 6,
"included_count": 33,
"prematurely_terminated_count": 0
},
{
"id": "12daafee-970b-4781-9121-994d06e3a766",
"name": "CHU DE NICE HOPITAL DE L'ARCHET",
"Center_Name": "CHU Nice Archet",
"patients_count": 38,
"preincluded_count": 0,
"included_count": 38,
"prematurely_terminated_count": 0
},
{
"id": "f07a7374-d731-4fcf-86e9-2f36e5faf342",
"name": "CHU MONTPELLIER HOPITAL ARNAUD DE VILLENEUVE",
"Center_Name": "CHU Montpellier - Hôpital Arnaud de Villeneuve",
"patients_count": 38,
"preincluded_count": 0,
"included_count": 38,
"prematurely_terminated_count": 0
},
{
"id": "9c64545a-b622-4ef9-be0f-f5cc21b9cd66",
"name": "HOPITAL NORD - CHU DE GRENOBLE ALPES",
"Center_Name": "CHU de Grenoble - Hôpital Nord",
"patients_count": 36,
"preincluded_count": 0,
"included_count": 35,
"prematurely_terminated_count": 1
},
{
"id": "c03b88b5-3cd2-4336-9048-19c239baf5ec",
"name": "CHRU DE RENNES SITE HOPITAL SUD",
"Center_Name": "CHU Rennes",
"patients_count": 34,
"preincluded_count": 0,
"included_count": 34,
"prematurely_terminated_count": 0
},
{
"id": "b0ba921e-e24a-41c2-b769-a2aa108cdd58",
"name": "CENTRE HOSPITALIER LES ESCARTONS A BRIANCON",
"Center_Name": "CH des Escartons de Briançon",
"patients_count": 32,
"preincluded_count": 1,
"included_count": 31,
"prematurely_terminated_count": 0
},
{
"id": "b5f30dc5-da3f-4f8b-9a39-33f0fefb7196",
"name": "SCM RX TOULOUSE CLINIQUE PASTEUR",
"Center_Name": "Clinique Pasteur Toulouse",
"patients_count": 32,
"preincluded_count": 2,
"included_count": 30,
"prematurely_terminated_count": 0
},
{
"id": "817429dd-0d7c-4df9-ad70-2f19c205e05a",
"name": "CENTRE HOSPITALIER - FALCONAJA - BASTIA",
"Center_Name": "CH Bastia",
"patients_count": 29,
"preincluded_count": 4,
"included_count": 25,
"prematurely_terminated_count": 0
},
{
"id": "4f943b00-9306-418a-a853-1d97dff71172",
"name": "HOPITAL EUROPEEN",
"Center_Name": "Hôpital Européen Marseille",
"patients_count": 28,
"preincluded_count": 0,
"included_count": 28,
"prematurely_terminated_count": 0
},
{
"id": "002312ec-69fb-4582-9e8f-7c266e5479d2",
"name": "HOPITAL NORD - CHU DE SAINT-ETIENNE",
"Center_Name": "CHU de Saint-Étienne - Hôpital Nord",
"patients_count": 27,
"preincluded_count": 2,
"included_count": 25,
"prematurely_terminated_count": 0
},
{
"id": "0ce39aac-6b5a-418b-8430-c4432e6cd78f",
"name": "HOPITAL DE RANGUEIL CHU TOULOUSE",
"Center_Name": "CHU de Toulouse - Hôpital Rangueil",
"patients_count": 24,
"preincluded_count": 1,
"included_count": 23,
"prematurely_terminated_count": 0
},
{
"id": "8ef8564e-836c-4c03-b4ec-8e28fa76c3c0",
"name": "HOPITAL ESTAING - CHU CLERMONT-FERRAND",
"Center_Name": "CHU Clermont-Ferrand - Site Estaing",
"patients_count": 23,
"preincluded_count": 1,
"included_count": 22,
"prematurely_terminated_count": 0
},
{
"id": "546f981f-6f7d-40c8-9b97-a1a3c46e6674",
"name": "GROUPE HOSPITALIER DE LA REGION DE MULHOUSE ET SUD ALSACE",
"Center_Name": "GHR Mulhouse Sud Alsace",
"patients_count": 20,
"preincluded_count": 0,
"included_count": 20,
"prematurely_terminated_count": 0
},
{
"id": "eb9c561d-37b6-485c-b351-9e19515cb98d",
"name": "GROUPE HOSPITALIER PELLEGRIN - CHU",
"Center_Name": "CHU de Bordeaux - GH Pellegrin",
"patients_count": 20,
"preincluded_count": 0,
"included_count": 20,
"prematurely_terminated_count": 0
},
{
"id": "d1ef2ced-0206-4725-a08c-454d71e823ec",
"name": "HOPITAL DE LA MERE ET DE L'ENFANT",
"Center_Name": "CHU Limoges - Hôpital de la Mère et de l'Enfant",
"patients_count": 20,
"preincluded_count": 0,
"included_count": 20,
"prematurely_terminated_count": 0
},
{
"id": "7f43a297-b156-4fec-8ecf-d8d008b1a1d2",
"name": "HOPITAL PRIVE DIJON BOURGOGNE",
"Center_Name": "Centre Evidens - Hôpital privé Dijon Bourgogne",
"patients_count": 19,
"preincluded_count": 4,
"included_count": 15,
"prematurely_terminated_count": 0
},
{
"id": "d767fcd0-3f4c-4cdf-9cda-b829156d2f95",
"name": "HOPITAL PRIVE SUD CORSE",
"Center_Name": "Hôpital Privé Sud Corse",
"patients_count": 18,
"preincluded_count": 0,
"included_count": 18,
"prematurely_terminated_count": 0
},
{
"id": "58d5b536-b326-4713-9137-bb6f852df0be",
"name": "CENTRE HOSPITALIER METROPOLE SAVOIE - CHAMBERY NH",
"Center_Name": "CH Métropole Savoie - Site Chambéry",
"patients_count": 16,
"preincluded_count": 0,
"included_count": 16,
"prematurely_terminated_count": 0
},
{
"id": "b4b35661-87c8-4333-bfb1-3ce4f1c77565",
"name": "CENTRE HOSPITALIER REGIONAL UNIVERSITAIRE BRETONNEAU",
"Center_Name": "CHRU Bretonneau",
"patients_count": 16,
"preincluded_count": 2,
"included_count": 14,
"prematurely_terminated_count": 0
},
{
"id": "454313d9-c624-43de-a8e3-84989d526403",
"name": "CLINIQUE MUTUALISTE LA SAGESSE RENNES",
"Center_Name": "Clinique la Sagesse - Rennes",
"patients_count": 15,
"preincluded_count": 0,
"included_count": 15,
"prematurely_terminated_count": 0
},
{
"id": "d981c078-189e-4831-a063-778733f7e582",
"name": "HOPITAL FOCH",
"Center_Name": "Hôpital Foch",
"patients_count": 15,
"preincluded_count": 1,
"included_count": 12,
"prematurely_terminated_count": 0
},
{
"id": "940c8425-fe53-45ac-a750-3e195df990e3",
"name": "HOPITAL PRIVE D'EURE ET LOIR",
"Center_Name": "Hôpital Privé d'Eure et Loir",
"patients_count": 14,
"preincluded_count": 1,
"included_count": 13,
"prematurely_terminated_count": 0
},
{
"id": "98cbbefe-920a-495a-981d-02093bd2253d",
"name": "CHU LA MILETRIE",
"Center_Name": "CHU de Poitiers La Miletrie",
"patients_count": 13,
"preincluded_count": 0,
"included_count": 13,
"prematurely_terminated_count": 0
},
{
"id": "38e9ef8a-5073-471a-a3df-14d834db2d12",
"name": "GHBS-SITE HÔPITAL DU SCORFF",
"Center_Name": "Groupe Hospitalier Bretagne Sud Lorient - GHBS - Lorient (SCORFF)",
"patients_count": 13,
"preincluded_count": 0,
"included_count": 13,
"prematurely_terminated_count": 0
},
{
"id": "6316db1a-4a90-4e45-a518-0095d63f7c36",
"name": "HOPITAL LE BOCAGE CHRU DIJON",
"Center_Name": "CHU Dijon",
"patients_count": 12,
"preincluded_count": 1,
"included_count": 10,
"prematurely_terminated_count": 1
},
{
"id": "f790c171-9523-48b6-a246-fdb26628eb6c",
"name": "CHRU NANCY - MATERNITE",
"Center_Name": "CHRU de Nancy - Maternité",
"patients_count": 11,
"preincluded_count": 1,
"included_count": 10,
"prematurely_terminated_count": 0
},
{
"id": "7b8c5362-7c7a-4d93-8b12-58b8e56020d1",
"name": "HOPITAL JACQUES MONOD CH LE HAVRE",
"Center_Name": "GH du Havre - Hôpital Jacques Monod",
"patients_count": 11,
"preincluded_count": 0,
"included_count": 11,
"prematurely_terminated_count": 0
},
{
"id": "6953af68-e30e-437f-93cf-19ed0932c350",
"name": "CHRU D'ORLEANS - HOPITAL DE LA SOURCE",
"Center_Name": "CHU d'Orléans",
"patients_count": 10,
"preincluded_count": 0,
"included_count": 10,
"prematurely_terminated_count": 0
},
{
"id": "f13560a8-3bf6-4322-bdae-332ee3bf0366",
"name": "POLYCLINIQUE JEAN VILLAR",
"Center_Name": "Polyclinique Jean Villar",
"patients_count": 10,
"preincluded_count": 0,
"included_count": 10,
"prematurely_terminated_count": 0
},
{
"id": "da67fb6e-2ccc-4994-8fde-cc2ee2a8e9ca",
"name": "GHU AP-HP UNIVERSITE PARIS SACLAY SITE KREMLIN BICETRE",
"Center_Name": "GHU APHP - Hôpital Bicêtre",
"patients_count": 9,
"preincluded_count": 0,
"included_count": 7,
"prematurely_terminated_count": 0
},
{
"id": "56f427f0-be35-493b-8049-46b747c0105e",
"name": "CHU AMIENS SUD",
"Center_Name": "CHU Amiens Sud",
"patients_count": 7,
"preincluded_count": 3,
"included_count": 4,
"prematurely_terminated_count": 0
},
{
"id": "f784ee6f-27b5-4afe-ba23-4f839d96535c",
"name": "APHM HOPITAL NORD",
"Center_Name": "APHM Hôpital Nord",
"patients_count": 5,
"preincluded_count": 0,
"included_count": 5,
"prematurely_terminated_count": 0
},
{
"id": "1ac60b10-08de-4639-9a49-67167f85844e",
"name": "CENTRE HOSPITALIER DE LENS",
"Center_Name": "CH de Lens",
"patients_count": 5,
"preincluded_count": 2,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "9ba321be-65d0-4767-9865-a547905e647e",
"name": "HOPITAL DE LA CROIX SAINT SIMON",
"Center_Name": "GH Diaconesses Croix Saint-Simon",
"patients_count": 5,
"preincluded_count": 1,
"included_count": 4,
"prematurely_terminated_count": 0
},
{
"id": "cab5b1e9-d93a-46eb-ae35-a449ea719c65",
"name": "GRAND HOSP EST FRANCILIEN MARNE LA VALLEE SITE JOSSIGNY",
"Center_Name": "Grand Hôpital de l'Est Francilien - Site de Marne-la-Vallée",
"patients_count": 4,
"preincluded_count": 0,
"included_count": 4,
"prematurely_terminated_count": 0
},
{
"id": "96519108-b0b2-4a5f-999f-cccc4e4059c9",
"name": "CHU DE NANTES SITE HOTEL DIEU HOPITAL MERE ENFANT",
"Center_Name": "CHU de Nantes - Site Hôtel Dieu Hôpital Mère Enfant",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "fe7548b7-75a1-4e13-97a9-6dc865b04b25",
"name": "CHU SITE SUD ( SAINT PIERRE)",
"Center_Name": "CHU la Réunion - Site Sud (SAINT PIERRE)",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "e68c76de-33e8-49e3-978d-c5fb408e7fd5",
"name": "CLINIQUE AXIUM",
"Center_Name": "Clinique Axium",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "af09eaa0-7e08-48e3-990c-24ca2fbc3fdc",
"name": "SCP GYNECOLOGIE RIVE GAUCHE",
"Center_Name": "SCP Gynécologie Clinique Rive Gauche",
"patients_count": 3,
"preincluded_count": 0,
"included_count": 3,
"prematurely_terminated_count": 0
},
{
"id": "0466558d-077c-4a51-9d25-2e15e00fabaf",
"name": "CENTRE HOSPITALIER DE CANNES SIMONE VEIL",
"Center_Name": "CH de Cannes Simone Veil",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "aa3e8684-07e0-45d9-9cee-144df2e4b430",
"name": "CENTRE HOSPITALIER DE PAU",
"Center_Name": "CH de Pau",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "13306b61-7ea6-4d60-b89c-b0df2d9d1605",
"name": "CENTRE HOSPITALIER VICTOR PROVO ROUBAIX",
"Center_Name": "CH Victor Provo Roubaix",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "8c27716f-edfe-4a4e-9189-09f39cc64395",
"name": "CHI DE MONT DE MARSAN ET DU PAYS DES SOURCES",
"Center_Name": "CHI Mont de Marsan",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "b74c365a-fc63-4763-b57d-abb609debd3b",
"name": "CTRE HOSPITALIER INTERCOMMUNAL POISSY ST GERMAIN SITE POISSY",
"Center_Name": "CHI Poissy St Germain en Laye - Site Poissy",
"patients_count": 2,
"preincluded_count": 1,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "4d271322-7945-42e8-801b-a3b25fc570be",
"name": "GROUPE HOSPITALIER PARIS SAINT JOSEPH",
"Center_Name": "Groupe Hospitalier Paris Saint-Joseph",
"patients_count": 2,
"preincluded_count": 0,
"included_count": 2,
"prematurely_terminated_count": 0
},
{
"id": "d82d64eb-a1cf-4635-a99f-23def12c7cb5",
"name": "CHRU BREST SITE HOPITAL MORVAN",
"Center_Name": "CHRU Brest - Site Hôpital Morvan",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "a47dc8c0-dfea-4a4c-9bcc-1370616ff9c8",
"name": "CHU DE MARTINIQUE SITE MFME",
"Center_Name": "CHU de Martinique - Site MFME",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "384d3b28-f33e-43a6-9043-394aa88aead7",
"name": "CLINIQUE BOUCHARD",
"Center_Name": "Clinique Bouchard - Marseille",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "0f78459f-b598-4faa-81a1-085a439e8c0e",
"name": "HOPITAUX PRIVES ROUENNAIS MATHILDE",
"Center_Name": "Hôpitaux Privés Rouennais - Mathilde",
"patients_count": 1,
"preincluded_count": 0,
"included_count": 1,
"prematurely_terminated_count": 0
},
{
"id": "02e42e01-56ab-418d-95df-cb53a7ef17b3",
"name": "APHM HOPITAL DE LA CONCEPTION",
"Center_Name": "APHM Hôpital de la Conception",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "2ba1f179-425e-4d88-97a7-88a097e639ab",
"name": "CENTRE HOSPITALIER DE CAYENNE",
"Center_Name": "CHU de Guyane - Site de Cayenne",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "3e8887e1-7608-40a6-b6f5-87c6fa5a0bd0",
"name": "CENTRE HOSPITALIER DE VERSAILLES ANDRE MIGNOT",
"Center_Name": "CH de Versailles",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "6af3784d-5d57-47c3-b924-0d13008ebd88",
"name": "CENTRE HOSPITALIER UNIVERSITAIRE DE POINTE-A-PITRE",
"Center_Name": "CHU de Guadeloupe - Pointe-à-Pitre",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "aa6af654-2cf2-4714-885d-a0e40b6f6f0a",
"name": "CHR METZ - THIONVILLE - HOPITAL DE MERCY",
"Center_Name": "CHR Metz - Thionville - Hôpital de Mercy",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "8441f339-4dd3-4c8c-bd2d-7751dc7fe43b",
"name": "CLINIQUE DU TERTRE ROUGE - POLE SANTE SUD",
"Center_Name": "Clinique du Tertre Rouge - Pôle Santé Sud",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "09c785f2-3deb-4a4a-a1a8-adbca5b767d7",
"name": "GHU APHP CENTRE-UNIVERSITE PARIS CITE SITE COCHIN PORT ROYAL",
"Center_Name": "APHP Site Cochin",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "5847c4fa-4025-4303-80eb-deaa07b80c0b",
"name": "GHU APHP SORBONNE UNIVERSITE SITE PITIE SALPETRIERE",
"Center_Name": "APHP Site Pitié Salpêtriere",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
},
{
"id": "dacc94bd-89b0-4cc4-b334-f684360abbe5",
"name": "HOPITAL PRIVE D ANTONY",
"Center_Name": "Hôpital Privé d'Antony",
"patients_count": 0,
"preincluded_count": 0,
"included_count": 0,
"prematurely_terminated_count": 0
}
]