Spaces:
Build error
Build error
Update baserow_storage.py
Browse files- baserow_storage.py +491 -583
baserow_storage.py
CHANGED
|
@@ -1,584 +1,492 @@
|
|
| 1 |
-
# baserow_storage.py
|
| 2 |
-
|
| 3 |
-
import os
|
| 4 |
-
import requests
|
| 5 |
-
import json
|
| 6 |
-
import sys
|
| 7 |
-
from datetime import datetime
|
| 8 |
-
from typing import Optional, Dict
|
| 9 |
-
import logging
|
| 10 |
-
# Configuration du logger (ajoutez ceci en haut du fichier)
|
| 11 |
-
logger = logging.getLogger(__name__)
|
| 12 |
-
# --- Configuration Baserow (Doit être défini dans les secrets) ---
|
| 13 |
-
HEALTH_CHECK_URL = "https://api.baserow.io/api/database/rows/table/"
|
| 14 |
-
|
| 15 |
-
# 2. URL de BASE CORRECTE pour la construction des requêtes de données (connexion, inscription, etc.)
|
| 16 |
-
DATA_BASE_URL = "https://api.baserow.io/api/database/rows/"
|
| 17 |
-
API_TOKEN = os.environ.get("BASEROW_API_TOKEN")
|
| 18 |
-
|
| 19 |
-
# Les IDs de table seront récupérés depuis les variables d'environnement
|
| 20 |
-
PRIMARY_USERS_TABLE_ID = os.environ.get("PRIMARY_USERS_TABLE_ID")
|
| 21 |
-
END_USERS_TABLE_ID = os.environ.get("END_USERS_TABLE_ID")
|
| 22 |
-
|
| 23 |
-
# Headers pour l'authentification
|
| 24 |
-
HEADERS = {
|
| 25 |
-
"Authorization": f"Token {API_TOKEN}",
|
| 26 |
-
"Content-Type": "application/json"
|
| 27 |
-
}
|
| 28 |
-
|
| 29 |
-
# ----------------------------------------------------------------------
|
| 30 |
-
# --- Noms de Colonnes pour la Table des Utilisateurs Principaux (Primary Users) ---
|
| 31 |
-
# ----------------------------------------------------------------------
|
| 32 |
-
FIELD_ID = 'ID' # Correspond à 'user_id' dans le code
|
| 33 |
-
FIELD_EMAIL = 'Email' # Correspond à 'email'
|
| 34 |
-
FIELD_USERNAME = 'Nom d\'utilisateur' # Correspond à 'username'
|
| 35 |
-
FIELD_PASSWORD_HASH = 'Hachage Mot de Passe' # Correspond à 'password_hash'
|
| 36 |
-
FIELD_API_KEY = 'Clé API' # Correspond à 'api_key'
|
| 37 |
-
FIELD_API_KEY_2 = 'Clé API 2'
|
| 38 |
-
FIELD_API_KEY_3 = 'Clé API 3'
|
| 39 |
-
FIELD_API_KEY_4 = 'Clé API 4'
|
| 40 |
-
FIELD_API_KEY_5 = 'Clé API 5'
|
| 41 |
-
FIELD_SECURITY_Q = 'Question de Sécurité'
|
| 42 |
-
FIELD_SECURITY_A_HASH = 'Hachage Réponse Secrète'
|
| 43 |
-
FIELD_PLAN_ID = 'Plan ID'
|
| 44 |
-
FIELD_STRIPE_SUB_ID = 'ID Abonnement Stripe'
|
| 45 |
-
FIELD_DATE_CREATION = 'Date Création'
|
| 46 |
-
FIELD_DATE_PLAN_START = 'Date Plan Start'
|
| 47 |
-
FIELD_API_CALLS_MONTH = 'API Calls Month' # À vérifier avec votre nom exact dans Baserow!
|
| 48 |
-
FIELD_STATUS = 'Status'
|
| 49 |
-
FIELD_END_USER_ID = 'ID Utilisateur Final' # Correspond à 'end_user_id'
|
| 50 |
-
FIELD_END_USER_IDENTIFIER = 'Identifiant' # Correspond à 'identifier'
|
| 51 |
-
FIELD_END_USER_METADATA = 'Métadonnées' # Correspond à 'metadata'
|
| 52 |
-
FIELD_CLIENT_ID_LINK = 'ID Client Principal' # Lien vers Primary_Users
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
# ----------------------------------------------------------------------
|
| 56 |
-
# --- Noms de Colonnes pour la Table des Utilisateurs Finaux (End Users) ---
|
| 57 |
-
# ----------------------------------------------------------------------
|
| 58 |
-
# Ces champs sont spécifiques à la table END_USERS
|
| 59 |
-
FIELD_END_USER_ID = 'ID Utilisateur Final'
|
| 60 |
-
FIELD_END_USER_IDENTIFIER = 'Identifiant' # Champ pour compatibilité ou recherche
|
| 61 |
-
FIELD_END_USER_EMAIL = 'Email' # NOUVEAU
|
| 62 |
-
FIELD_END_USER_USERNAME = 'Nom d\'utilisateur' # NOUVEAU
|
| 63 |
-
FIELD_END_USER_SECURITY_Q = 'Question de Sécurité' # NOUVEAU (Peut être différent de Primary)
|
| 64 |
-
FIELD_END_USER_SECURITY_A_HASH = 'Hachage Réponse Secrète' # NOUVEAU
|
| 65 |
-
FIELD_END_USER_STATUS = 'Statut' # NOUVEAU
|
| 66 |
-
FIELD_END_USER_METADATA = 'Métadonnées'
|
| 67 |
-
FIELD_PASSWORD_HASH_END_USER = 'Hachage Mot de Passe End User' # Renommer pour éviter le conflit si possible
|
| 68 |
-
FIELD_CLIENT_ID_LINK = 'ID Client Principal'
|
| 69 |
-
|
| 70 |
-
def _get_table_url(table_id: str) -> str:
|
| 71 |
-
"""Construit l'URL d'API pour une table donnée (avec le bon endpoint)."""
|
| 72 |
-
return f"{DATA_BASE_URL}table/{table_id}/"
|
| 73 |
-
|
| 74 |
-
def _baserow_record_to_user(record: Dict, is_end_user: bool) -> Dict:
|
| 75 |
-
"""
|
| 76 |
-
Convertit un enregistrement Baserow (avec noms de champs utilisateur)
|
| 77 |
-
en format de dictionnaire Python attendu par le backend.
|
| 78 |
-
"""
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
'
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
'
|
| 117 |
-
'
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
#
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
#
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
#
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
def
|
| 405 |
-
"""
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
print(f"DEBUG: Baserow
|
| 447 |
-
return
|
| 448 |
-
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
|
| 467 |
-
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
|
| 474 |
-
|
| 475 |
-
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
# Si toutes les tables critiques ont été vérifiées avec succès
|
| 493 |
-
return "operational"
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
def get_health_status() -> Dict:
|
| 497 |
-
"""
|
| 498 |
-
Collecte l'état de santé de tous les services pour la page /statut.
|
| 499 |
-
"""
|
| 500 |
-
|
| 501 |
-
db_status = check_baserow_connection()
|
| 502 |
-
|
| 503 |
-
# L'état de l'authentification et de l'API principale sont
|
| 504 |
-
# généralement liés à l'état de la DB pour une application simple.
|
| 505 |
-
# Si la DB est HS, l'auth est HS. Sinon, ils sont OK.
|
| 506 |
-
|
| 507 |
-
auth_status = db_status # Lié à la DB (pour charger les utilisateurs)
|
| 508 |
-
api_endpoint_status = "operational" # L'endpoint Flask lui-même est considéré comme OK s'il tourne
|
| 509 |
-
|
| 510 |
-
# Version du service (pour information)
|
| 511 |
-
service_version = os.environ.get("SERVICE_VERSION", "1.0.0 (Baserow)")
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
return {
|
| 515 |
-
# Ces valeurs correspondent aux attributs 'data-status' dans statut.html
|
| 516 |
-
"auth": auth_status,
|
| 517 |
-
"data_storage": db_status,
|
| 518 |
-
"api_endpoint": api_endpoint_status,
|
| 519 |
-
"version": service_version,
|
| 520 |
-
"last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
| 521 |
-
}
|
| 522 |
-
|
| 523 |
-
def is_baserow_up() -> bool:
|
| 524 |
-
"""
|
| 525 |
-
Vérifie l'état de Baserow en utilisant l'URL qui garantit un statut 100% fonctionnel
|
| 526 |
-
sur Hugging Face, SANS utiliser la fonction de construction d'URL de table.
|
| 527 |
-
"""
|
| 528 |
-
try:
|
| 529 |
-
# Envoie une requête GET à l'URL qui répond positivement pour le health check.
|
| 530 |
-
response = requests.get(
|
| 531 |
-
HEALTH_CHECK_URL,
|
| 532 |
-
headers=HEADERS,
|
| 533 |
-
timeout=5
|
| 534 |
-
)
|
| 535 |
-
# On vérifie si la réponse est un succès (code 200).
|
| 536 |
-
return response.status_code == 200
|
| 537 |
-
except requests.exceptions.RequestException as e:
|
| 538 |
-
print(f"DEBUG: Baserow health check failed: {e}")
|
| 539 |
-
return False
|
| 540 |
-
|
| 541 |
-
def log_baserow_api_call(method: str, url: str, headers: Dict, data: Optional[Dict] = None, log_response: bool = True):
|
| 542 |
-
"""
|
| 543 |
-
Fonction utilitaire pour effectuer des appels API à Baserow et journaliser
|
| 544 |
-
les requêtes et les réponses dans les logs du Space Hugging Face.
|
| 545 |
-
"""
|
| 546 |
-
|
| 547 |
-
# 1. Journalisation de la requête
|
| 548 |
-
logger.info(f"BASEROW REQUEST: {method} {url}")
|
| 549 |
-
# ATTENTION: Ne pas logger le token API complet!
|
| 550 |
-
logged_headers = {k: v.replace(API_TOKEN, '[TOKEN_MASKED]') if k == 'Authorization' else v for k, v in headers.items()}
|
| 551 |
-
logger.debug(f"BASEROW REQUEST Headers: {logged_headers}")
|
| 552 |
-
if data:
|
| 553 |
-
# Pour les requêtes POST/PUT, logger les données (sans le hash du mot de passe si possible)
|
| 554 |
-
logged_data = data.copy() if isinstance(data, dict) else data
|
| 555 |
-
if isinstance(logged_data, dict) and 'Hachage du mot de passe' in logged_data:
|
| 556 |
-
logged_data['Hachage du mot de passe'] = '[PASSWORD_HASH_MASKED]'
|
| 557 |
-
logger.debug(f"BASEROW REQUEST Body: {logged_data}")
|
| 558 |
-
|
| 559 |
-
# 2. Exécution de la requête
|
| 560 |
-
try:
|
| 561 |
-
if method == "GET":
|
| 562 |
-
response = requests.get(url, headers=headers)
|
| 563 |
-
elif method == "POST":
|
| 564 |
-
response = requests.post(url, headers=headers, json=data)
|
| 565 |
-
elif method == "PUT":
|
| 566 |
-
response = requests.put(url, headers=headers, json=data)
|
| 567 |
-
elif method == "DELETE":
|
| 568 |
-
response = requests.delete(url, headers=headers)
|
| 569 |
-
else:
|
| 570 |
-
raise ValueError(f"Méthode HTTP non supportée: {method}")
|
| 571 |
-
|
| 572 |
-
# 3. Journalisation de la réponse
|
| 573 |
-
if log_response:
|
| 574 |
-
logger.info(f"BASEROW RESPONSE: Status {response.status_code}")
|
| 575 |
-
# Journaliser le contenu pour les erreurs
|
| 576 |
-
if response.status_code >= 400:
|
| 577 |
-
logger.error(f"BASEROW ERROR RESPONSE Body: {response.text}")
|
| 578 |
-
|
| 579 |
-
# 4. Retourner la réponse
|
| 580 |
-
return response
|
| 581 |
-
|
| 582 |
-
except requests.exceptions.RequestException as e:
|
| 583 |
-
logger.error(f"BASEROW CONNECTION ERROR: {e}")
|
| 584 |
return None # Retourne None en cas d'erreur de connexion non gérée par le statut HTTP
|
|
|
|
| 1 |
+
# baserow_storage.py
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
import requests
|
| 5 |
+
import json
|
| 6 |
+
import sys
|
| 7 |
+
from datetime import datetime
|
| 8 |
+
from typing import Optional, Dict
|
| 9 |
+
import logging
|
| 10 |
+
# Configuration du logger (ajoutez ceci en haut du fichier)
|
| 11 |
+
logger = logging.getLogger(__name__)
|
| 12 |
+
# --- Configuration Baserow (Doit être défini dans les secrets) ---
|
| 13 |
+
HEALTH_CHECK_URL = "https://api.baserow.io/api/database/rows/table/"
|
| 14 |
+
|
| 15 |
+
# 2. URL de BASE CORRECTE pour la construction des requêtes de données (connexion, inscription, etc.)
|
| 16 |
+
DATA_BASE_URL = "https://api.baserow.io/api/database/rows/"
|
| 17 |
+
API_TOKEN = os.environ.get("BASEROW_API_TOKEN")
|
| 18 |
+
|
| 19 |
+
# Les IDs de table seront récupérés depuis les variables d'environnement
|
| 20 |
+
PRIMARY_USERS_TABLE_ID = os.environ.get("PRIMARY_USERS_TABLE_ID")
|
| 21 |
+
END_USERS_TABLE_ID = os.environ.get("END_USERS_TABLE_ID")
|
| 22 |
+
|
| 23 |
+
# Headers pour l'authentification
|
| 24 |
+
HEADERS = {
|
| 25 |
+
"Authorization": f"Token {API_TOKEN}",
|
| 26 |
+
"Content-Type": "application/json"
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
# ----------------------------------------------------------------------
|
| 30 |
+
# --- Noms de Colonnes pour la Table des Utilisateurs Principaux (Primary Users) ---
|
| 31 |
+
# ----------------------------------------------------------------------
|
| 32 |
+
FIELD_ID = 'ID' # Correspond à 'user_id' dans le code
|
| 33 |
+
FIELD_EMAIL = 'Email' # Correspond à 'email'
|
| 34 |
+
FIELD_USERNAME = 'Nom d\'utilisateur' # Correspond à 'username'
|
| 35 |
+
FIELD_PASSWORD_HASH = 'Hachage Mot de Passe' # Correspond à 'password_hash'
|
| 36 |
+
FIELD_API_KEY = 'Clé API' # Correspond à 'api_key'
|
| 37 |
+
FIELD_API_KEY_2 = 'Clé API 2'
|
| 38 |
+
FIELD_API_KEY_3 = 'Clé API 3'
|
| 39 |
+
FIELD_API_KEY_4 = 'Clé API 4'
|
| 40 |
+
FIELD_API_KEY_5 = 'Clé API 5'
|
| 41 |
+
FIELD_SECURITY_Q = 'Question de Sécurité'
|
| 42 |
+
FIELD_SECURITY_A_HASH = 'Hachage Réponse Secrète'
|
| 43 |
+
FIELD_PLAN_ID = 'Plan ID'
|
| 44 |
+
FIELD_STRIPE_SUB_ID = 'ID Abonnement Stripe'
|
| 45 |
+
FIELD_DATE_CREATION = 'Date Création'
|
| 46 |
+
FIELD_DATE_PLAN_START = 'Date Plan Start'
|
| 47 |
+
FIELD_API_CALLS_MONTH = 'API Calls Month' # À vérifier avec votre nom exact dans Baserow!
|
| 48 |
+
FIELD_STATUS = 'Status'
|
| 49 |
+
FIELD_END_USER_ID = 'ID Utilisateur Final' # Correspond à 'end_user_id'
|
| 50 |
+
FIELD_END_USER_IDENTIFIER = 'Identifiant' # Correspond à 'identifier'
|
| 51 |
+
FIELD_END_USER_METADATA = 'Métadonnées' # Correspond à 'metadata'
|
| 52 |
+
FIELD_CLIENT_ID_LINK = 'ID Client Principal' # Lien vers Primary_Users
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# ----------------------------------------------------------------------
|
| 56 |
+
# --- Noms de Colonnes pour la Table des Utilisateurs Finaux (End Users) ---
|
| 57 |
+
# ----------------------------------------------------------------------
|
| 58 |
+
# Ces champs sont spécifiques à la table END_USERS
|
| 59 |
+
FIELD_END_USER_ID = 'ID Utilisateur Final'
|
| 60 |
+
FIELD_END_USER_IDENTIFIER = 'Identifiant' # Champ pour compatibilité ou recherche
|
| 61 |
+
FIELD_END_USER_EMAIL = 'Email' # NOUVEAU
|
| 62 |
+
FIELD_END_USER_USERNAME = 'Nom d\'utilisateur' # NOUVEAU
|
| 63 |
+
FIELD_END_USER_SECURITY_Q = 'Question de Sécurité' # NOUVEAU (Peut être différent de Primary)
|
| 64 |
+
FIELD_END_USER_SECURITY_A_HASH = 'Hachage Réponse Secrète' # NOUVEAU
|
| 65 |
+
FIELD_END_USER_STATUS = 'Statut' # NOUVEAU
|
| 66 |
+
FIELD_END_USER_METADATA = 'Métadonnées'
|
| 67 |
+
FIELD_PASSWORD_HASH_END_USER = 'Hachage Mot de Passe End User' # Renommer pour éviter le conflit si possible
|
| 68 |
+
FIELD_CLIENT_ID_LINK = 'ID Client Principal'
|
| 69 |
+
|
| 70 |
+
def _get_table_url(table_id: str) -> str:
|
| 71 |
+
"""Construit l'URL d'API pour une table donnée (avec le bon endpoint)."""
|
| 72 |
+
return f"{DATA_BASE_URL}table/{table_id}/"
|
| 73 |
+
|
| 74 |
+
def _baserow_record_to_user(record: Dict, is_end_user: bool) -> Dict:
|
| 75 |
+
"""
|
| 76 |
+
Convertit un enregistrement Baserow (avec noms de champs utilisateur)
|
| 77 |
+
en format de dictionnaire Python attendu par le backend.
|
| 78 |
+
"""
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
# LOGIQUE POUR L'UTILISATEUR PRINCIPAL (PRIMARY USER)
|
| 83 |
+
|
| 84 |
+
# 1. Récupération des champs individuels
|
| 85 |
+
user_data = {
|
| 86 |
+
# Champs communs / Primary Users
|
| 87 |
+
'baserow_row_id': record['id'], # ID interne de la ligne Baserow (pour les mises à jour)
|
| 88 |
+
'date_creation': record.get(FIELD_DATE_CREATION),
|
| 89 |
+
|
| 90 |
+
# Primary User specific fields
|
| 91 |
+
'user_id': record.get(FIELD_ID),
|
| 92 |
+
'email': record.get(FIELD_EMAIL),
|
| 93 |
+
'username': record.get(FIELD_USERNAME),
|
| 94 |
+
'password_hash': record.get(FIELD_PASSWORD_HASH),
|
| 95 |
+
|
| 96 |
+
# Récupération des 5 clés individuelles (pour l'authentification par clé)
|
| 97 |
+
'api_key': record.get(FIELD_API_KEY),
|
| 98 |
+
'api_key_2': record.get(FIELD_API_KEY_2),
|
| 99 |
+
'api_key_3': record.get(FIELD_API_KEY_3),
|
| 100 |
+
'api_key_4': record.get(FIELD_API_KEY_4),
|
| 101 |
+
'api_key_5': record.get(FIELD_API_KEY_5),
|
| 102 |
+
|
| 103 |
+
'security_question': record.get(FIELD_SECURITY_Q),
|
| 104 |
+
'security_answer_hash': record.get(FIELD_SECURITY_A_HASH),
|
| 105 |
+
'plan_id': record.get(FIELD_PLAN_ID),
|
| 106 |
+
'stripe_subscription_id': record.get(FIELD_STRIPE_SUB_ID),
|
| 107 |
+
'date_plan_start': record.get(FIELD_DATE_PLAN_START),
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
# 2. ÉTAPE CRUCIALE AJOUTÉE : Création de la liste 'api_keys' pour l'affichage
|
| 111 |
+
# Cette liste est nécessaire pour que la boucle dans api_key.html fonctionne correctement.
|
| 112 |
+
user_data['api_keys'] = [
|
| 113 |
+
user_data['api_key'],
|
| 114 |
+
user_data['api_key_2'],
|
| 115 |
+
user_data['api_key_3'],
|
| 116 |
+
user_data['api_key_4'],
|
| 117 |
+
user_data['api_key_5'],
|
| 118 |
+
]
|
| 119 |
+
|
| 120 |
+
# Nettoyage des clés None ou non-pertinentes
|
| 121 |
+
return {k: v for k, v in user_data.items() if v is not None}
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def _user_to_baserow_data(user_data: Dict, is_end_user: bool) -> Dict:
|
| 125 |
+
"""
|
| 126 |
+
Convertit le format de dictionnaire Python du backend en format
|
| 127 |
+
JSON attendu par l'API Baserow (avec noms de champs utilisateur).
|
| 128 |
+
"""
|
| 129 |
+
if is_end_user:
|
| 130 |
+
# End User fields (Ajout des NOUVEAUX champs)
|
| 131 |
+
baserow_data = {
|
| 132 |
+
FIELD_END_USER_ID: user_data.get('end_user_id'),
|
| 133 |
+
FIELD_END_USER_IDENTIFIER: user_data.get('identifier'),
|
| 134 |
+
FIELD_END_USER_EMAIL: user_data.get('email'), # NOUVEAU
|
| 135 |
+
FIELD_END_USER_USERNAME: user_data.get('username'), # NOUVEAU
|
| 136 |
+
FIELD_END_USER_SECURITY_Q: user_data.get('security_question'), # NOUVEAU
|
| 137 |
+
FIELD_END_USER_SECURITY_A_HASH: user_data.get('security_answer_hash'), # NOUVEAU
|
| 138 |
+
FIELD_END_USER_STATUS: user_data.get('status'), # NOUVEAU
|
| 139 |
+
|
| 140 |
+
# CORRECTION CRUCIALE : Utilisation du nom de champ correct pour l'End User
|
| 141 |
+
FIELD_PASSWORD_HASH_END_USER: user_data.get('password_hash'),
|
| 142 |
+
|
| 143 |
+
FIELD_END_USER_METADATA: user_data.get('metadata'),
|
| 144 |
+
FIELD_DATE_CREATION: user_data.get('date_creation'),
|
| 145 |
+
# Le lien vers Primary_Users est géré dans save_end_user_data
|
| 146 |
+
}
|
| 147 |
+
else:
|
| 148 |
+
# Primary User fields
|
| 149 |
+
baserow_data = {
|
| 150 |
+
FIELD_ID: user_data.get('user_id'),
|
| 151 |
+
FIELD_EMAIL: user_data.get('email'),
|
| 152 |
+
FIELD_USERNAME: user_data.get('username'),
|
| 153 |
+
FIELD_PASSWORD_HASH: user_data.get('password_hash'),
|
| 154 |
+
FIELD_API_KEY: user_data.get('api_key'),
|
| 155 |
+
FIELD_API_KEY_2: user_data.get('api_key_2'),
|
| 156 |
+
FIELD_API_KEY_3: user_data.get('api_key_3'),
|
| 157 |
+
FIELD_API_KEY_4: user_data.get('api_key_4'),
|
| 158 |
+
FIELD_API_KEY_5: user_data.get('api_key_5'),
|
| 159 |
+
FIELD_SECURITY_Q: user_data.get('security_question'),
|
| 160 |
+
FIELD_SECURITY_A_HASH: user_data.get('security_answer_hash'),
|
| 161 |
+
FIELD_PLAN_ID: user_data.get('plan_id'),
|
| 162 |
+
FIELD_STRIPE_SUB_ID: user_data.get('stripe_subscription_id'),
|
| 163 |
+
FIELD_DATE_CREATION: user_data.get('date_creation'),
|
| 164 |
+
FIELD_DATE_PLAN_START: user_data.get('date_plan_start'),
|
| 165 |
+
FIELD_API_CALLS_MONTH: user_data.get('api_calls_month', 0),
|
| 166 |
+
FIELD_STATUS: user_data.get('status', 'Active') # Assurez-vous que 'Active' est une option valide dans Baserow
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
# Suppression des clés non-valorisées (None)
|
| 170 |
+
return {k: v for k, v in baserow_data.items() if v is not None}
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
def _get_single_user_record(table_id: str, field_name: str, value: str, is_end_user: bool) -> Optional[Dict]:
|
| 174 |
+
"""Fonction générique pour rechercher un seul enregistrement par un champ (filtrage Baserow)."""
|
| 175 |
+
url = _get_table_url(table_id)
|
| 176 |
+
# Utilisation du paramètre de filtre de Baserow pour une recherche indexée (plus rapide)
|
| 177 |
+
filter_param = f"filter__{field_name}__equal={value}"
|
| 178 |
+
|
| 179 |
+
try:
|
| 180 |
+
response = requests.get(
|
| 181 |
+
f"{url}?user_field_names=true&{filter_param}",
|
| 182 |
+
headers=HEADERS
|
| 183 |
+
)
|
| 184 |
+
response.raise_for_status()
|
| 185 |
+
|
| 186 |
+
data = response.json()
|
| 187 |
+
if data and data.get('results'):
|
| 188 |
+
# On ne prend que le premier résultat (car ID/Email/API Key sont uniques)
|
| 189 |
+
return _baserow_record_to_user(data['results'][0], is_end_user)
|
| 190 |
+
return None
|
| 191 |
+
|
| 192 |
+
except requests.exceptions.RequestException as e:
|
| 193 |
+
print(f"Erreur de Baserow lors de la recherche par filtre {field_name}: {e}", file=sys.stderr)
|
| 194 |
+
return None
|
| 195 |
+
|
| 196 |
+
# ----------------------------------------------------------------------
|
| 197 |
+
# --- Fonctions CRUD Primary_Users (Nouveau et Remplacement) ---
|
| 198 |
+
# ----------------------------------------------------------------------
|
| 199 |
+
|
| 200 |
+
def get_user_by_email(email: str) -> Optional[Dict]:
|
| 201 |
+
"""Recherche un utilisateur principal par son adresse Email."""
|
| 202 |
+
return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_EMAIL, email, is_end_user=False)
|
| 203 |
+
|
| 204 |
+
def get_client_user_by_api_key(api_key: str) -> Optional[Dict]:
|
| 205 |
+
"""
|
| 206 |
+
Recherche un utilisateur principal par L'UNE de ses 5 clés API.
|
| 207 |
+
Utilise le filtre 'OR' de Baserow pour vérifier les 5 champs.
|
| 208 |
+
"""
|
| 209 |
+
url = _get_table_url(PRIMARY_USERS_TABLE_ID)
|
| 210 |
+
|
| 211 |
+
# 1. Définition du filtre OR sur les 5 champs de clé API
|
| 212 |
+
filters = {
|
| 213 |
+
"filter_type": "OR",
|
| 214 |
+
"filters": [
|
| 215 |
+
# Filtre exact pour Clé API
|
| 216 |
+
{"field": FIELD_API_KEY, "type": "equal", "value": api_key},
|
| 217 |
+
{"field": FIELD_API_KEY_2, "type": "equal", "value": api_key},
|
| 218 |
+
{"field": FIELD_API_KEY_3, "type": "equal", "value": api_key},
|
| 219 |
+
{"field": FIELD_API_KEY_4, "type": "equal", "value": api_key},
|
| 220 |
+
{"field": FIELD_API_KEY_5, "type": "equal", "value": api_key},
|
| 221 |
+
]
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
# 2. Construction de la requête Baserow.
|
| 225 |
+
# On utilise le paramètre 'filters' pour passer notre objet de filtre.
|
| 226 |
+
params = {
|
| 227 |
+
"user_field_names": "true",
|
| 228 |
+
"filters": json.dumps(filters) # Baserow a besoin du filtre en JSON string
|
| 229 |
+
}
|
| 230 |
+
|
| 231 |
+
# 3. Exécution de la requête (utilisez votre fonction _make_baserow_request ou équivalent pour GET)
|
| 232 |
+
# Assurez-vous que votre fonction de requête supporte le paramètre 'params'
|
| 233 |
+
try:
|
| 234 |
+
response = _make_baserow_request("GET", url, params=params) # Adaptation à votre structure de fonction d'appel
|
| 235 |
+
|
| 236 |
+
if response.status_code == 200:
|
| 237 |
+
data = response.json()
|
| 238 |
+
# La réponse contient 'results' (une liste)
|
| 239 |
+
if data and data['results']:
|
| 240 |
+
# Nous nous attendons à un seul résultat (la clé API est censée être unique)
|
| 241 |
+
record = data['results'][0]
|
| 242 |
+
# Convertir l'enregistrement Baserow en objet utilisateur (Primary User)
|
| 243 |
+
return _baserow_record_to_user(record) # Assurez-vous d'avoir retiré le paramètre is_end_user ici
|
| 244 |
+
|
| 245 |
+
except Exception as e:
|
| 246 |
+
logger.error(f"Erreur lors de la recherche par clé API: {e}", file=sys.stderr)
|
| 247 |
+
|
| 248 |
+
return None
|
| 249 |
+
|
| 250 |
+
# Remplacement de l'ancien load_primary_user_data(user_id)
|
| 251 |
+
def load_primary_user_data(user_id: str) -> Optional[Dict]:
|
| 252 |
+
"""Recherche un utilisateur principal par son ID (user_id)."""
|
| 253 |
+
return _get_single_user_record(PRIMARY_USERS_TABLE_ID, FIELD_ID, user_id, is_end_user=False)
|
| 254 |
+
|
| 255 |
+
|
| 256 |
+
def save_primary_user_data(user_data: Dict, commit_msg: str = "") -> bool:
|
| 257 |
+
"""Crée ou met à jour un utilisateur principal, avec détection d'erreur ultra-précise."""
|
| 258 |
+
row_id = user_data.get('baserow_row_id')
|
| 259 |
+
|
| 260 |
+
# Définition de l'URL de base pour la table des utilisateurs principaux
|
| 261 |
+
url = _get_table_url(PRIMARY_USERS_TABLE_ID)
|
| 262 |
+
|
| 263 |
+
# 1. Conversion des données
|
| 264 |
+
baserow_data = _user_to_baserow_data(user_data, is_end_user=False)
|
| 265 |
+
|
| 266 |
+
# 2. Suppression des champs en lecture seule (comme dans la correction précédente)
|
| 267 |
+
if baserow_data.pop(FIELD_ID, None):
|
| 268 |
+
print(f"DEBUG: Suppression du champ '{FIELD_ID}' (UUID auto) avant l'envoi { 'POST' if not row_id else 'PATCH'}.", file=sys.stderr)
|
| 269 |
+
|
| 270 |
+
try:
|
| 271 |
+
# Détermination de l'action (PATCH ou POST)
|
| 272 |
+
if row_id:
|
| 273 |
+
action = "PATCH" # ⬅️ CORRECTION: Définition de 'action'
|
| 274 |
+
# MISE À JOUR (PATCH)
|
| 275 |
+
response = requests.patch(
|
| 276 |
+
f"{url}{row_id}/?user_field_names=true",
|
| 277 |
+
headers=HEADERS,
|
| 278 |
+
json=baserow_data
|
| 279 |
+
)
|
| 280 |
+
else:
|
| 281 |
+
action = "POST" # ⬅️ CORRECTION: Définition de 'action'
|
| 282 |
+
# CRÉATION (POST)
|
| 283 |
+
response = requests.post(
|
| 284 |
+
f"{url}?user_field_names=true", # ⬅️ CORRECTION: Utilise l'URL de table 'url'
|
| 285 |
+
headers=HEADERS,
|
| 286 |
+
json=baserow_data
|
| 287 |
+
)
|
| 288 |
+
|
| 289 |
+
# Déclenche une exception requests.exceptions.HTTPError pour les statuts 4xx/5xx
|
| 290 |
+
response.raise_for_status()
|
| 291 |
+
|
| 292 |
+
# Succès
|
| 293 |
+
if not row_id:
|
| 294 |
+
new_record = response.json()
|
| 295 |
+
# 1. Mettre à jour l'ID de ligne Baserow
|
| 296 |
+
user_data['baserow_row_id'] = new_record.get('id')
|
| 297 |
+
# 2. Mettre à jour l'UUID de l'utilisateur (généré par Baserow)
|
| 298 |
+
user_data['user_id'] = new_record.get(FIELD_ID)
|
| 299 |
+
|
| 300 |
+
print(f"DEBUG: UUID de l'utilisateur généré par Baserow et enregistré: {user_data['user_id']}", file=sys.stderr)
|
| 301 |
+
|
| 302 |
+
print(f"DEBUG: Baserow Primary User action '{action}' réussie. Row ID: {user_data.get('baserow_row_id')}. Message: {commit_msg}", file=sys.stderr)
|
| 303 |
+
return True
|
| 304 |
+
|
| 305 |
+
except requests.exceptions.RequestException as e:
|
| 306 |
+
# --- BLOC DE DÉTECTION D'ERREUR PRÉCISE (Ultra-Complet) ---
|
| 307 |
+
|
| 308 |
+
# Note: 'action' est définie dans le bloc try/except, mais si l'erreur survient
|
| 309 |
+
# AVANT la définition de 'action', nous devons la gérer.
|
| 310 |
+
# Pour être sûr, nous allons la définir ici par défaut si elle n'existe pas.
|
| 311 |
+
if 'action' not in locals():
|
| 312 |
+
action = "INCONNU"
|
| 313 |
+
|
| 314 |
+
error_message = f"🚨 ÉCHEC: Erreur lors de la sauvegarde/mise à jour du Primary User dans Baserow. Requête: {action}"
|
| 315 |
+
error_details = ""
|
| 316 |
+
|
| 317 |
+
if hasattr(e, 'response') and e.response is not None:
|
| 318 |
+
# 1. Statut HTTP et URL
|
| 319 |
+
error_details += f"\n -> STATUT HTTP: {e.response.status_code} ({e.response.reason})"
|
| 320 |
+
error_details += f"\n -> URL de la requête: {e.response.url}"
|
| 321 |
+
|
| 322 |
+
# 2. Tenter de décoder le corps de la réponse en JSON (contient les erreurs Baserow)
|
| 323 |
+
try:
|
| 324 |
+
response_json = e.response.json()
|
| 325 |
+
error_details += f"\n\n -> ERREUR BASEROW DÉTAILLÉE (JSON):\n{json.dumps(response_json, indent=4)}"
|
| 326 |
+
|
| 327 |
+
# Optionnel: Synthèse des erreurs de validation de champ
|
| 328 |
+
if isinstance(response_json, dict):
|
| 329 |
+
validation_errors = {k: v for k, v in response_json.items() if isinstance(v, list) and k != 'detail'}
|
| 330 |
+
if validation_errors:
|
| 331 |
+
error_details += "\n -> SYNTHÈSE DES CHAMPS INVALIDES (Vérifiez les noms de colonnes/IDs de table!):"
|
| 332 |
+
for field_name, errors in validation_errors.items():
|
| 333 |
+
error_details += f"\n - Champ '{field_name}': {', '.join([err.get('error', 'Erreur inconnue') for err in errors])}"
|
| 334 |
+
|
| 335 |
+
except json.JSONDecodeError:
|
| 336 |
+
# 3. Si le corps de la réponse n'est pas du JSON
|
| 337 |
+
error_details += f"\n\n -> ERREUR BRUTE (Réponse non-JSON):\n{e.response.text[:500]}..."
|
| 338 |
+
|
| 339 |
+
# 4. Afficher les données que nous avons tenté d'envoyer (après la suppression de l'ID si c'était une création)
|
| 340 |
+
error_details += f"\n\n -> DONNÉES ENVOYÉES À BASEROW:\n{json.dumps(baserow_data, indent=4)}"
|
| 341 |
+
|
| 342 |
+
# Log complet de l'erreur
|
| 343 |
+
print(error_message + error_details, file=sys.stderr)
|
| 344 |
+
|
| 345 |
+
return False
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
# ----------------------------------------------------------------------
|
| 349 |
+
# --- Fonctions CRUD End_Users (Remplacement) ---
|
| 350 |
+
# ----------------------------------------------------------------------
|
| 351 |
+
|
| 352 |
+
# baserow_storage.py : Dans la section CRUD End_Users
|
| 353 |
+
|
| 354 |
+
|
| 355 |
+
def _get_client_baserow_row_id(client_user_id: str) -> Optional[int]:
|
| 356 |
+
"""Récupère l'ID de ligne interne Baserow du client principal pour le lien."""
|
| 357 |
+
client_user = load_primary_user_data(client_user_id) # utilise la fonction déjà créée
|
| 358 |
+
return client_user.get('baserow_row_id') if client_user else None
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def check_baserow_connection() -> str:
|
| 362 |
+
"""
|
| 363 |
+
Vérifie l'état de connexion de la base de données Baserow.
|
| 364 |
+
Retourne 'operational' ou 'outage'.
|
| 365 |
+
"""
|
| 366 |
+
# Liste des IDs de tables critiques à vérifier
|
| 367 |
+
CRITICAL_TABLE_IDS = [
|
| 368 |
+
PRIMARY_USERS_TABLE_ID,
|
| 369 |
+
END_USERS_TABLE_ID
|
| 370 |
+
]
|
| 371 |
+
|
| 372 |
+
if not API_TOKEN:
|
| 373 |
+
# Si le token API n'est pas défini, échec immédiat
|
| 374 |
+
print("DEBUG: BASEROW_API_TOKEN manquant.", file=sys.stderr)
|
| 375 |
+
return "outage"
|
| 376 |
+
|
| 377 |
+
for table_id in CRITICAL_TABLE_IDS:
|
| 378 |
+
if not table_id:
|
| 379 |
+
# Si un des IDs de table critiques n'est pas défini, échec
|
| 380 |
+
print(f"DEBUG: Un ID de table critique Baserow est manquant (ID: {table_id}).", file=sys.stderr)
|
| 381 |
+
return "outage"
|
| 382 |
+
|
| 383 |
+
# Tenter de faire un appel très léger (récupérer la première ligne)
|
| 384 |
+
# On utilise page_size=1 pour minimiser la charge
|
| 385 |
+
url = f"{DATA_BASE_URL}table/{table_id}/?page_size=1"
|
| 386 |
+
|
| 387 |
+
try:
|
| 388 |
+
response = requests.get(url, headers=HEADERS, timeout=5)
|
| 389 |
+
|
| 390 |
+
if response.status_code != 200:
|
| 391 |
+
# Si un 404, 403, ou autre erreur est retournée par Baserow pour CETTE table
|
| 392 |
+
print(f"DEBUG: Baserow check failed for table {table_id} with status code {response.status_code}", file=sys.stderr)
|
| 393 |
+
return "outage"
|
| 394 |
+
|
| 395 |
+
except requests.exceptions.RequestException as e:
|
| 396 |
+
# Erreur de réseau (timeout, DNS, etc.)
|
| 397 |
+
print(f"DEBUG: Baserow connection error for table {table_id}: {e}", file=sys.stderr)
|
| 398 |
+
return "outage"
|
| 399 |
+
|
| 400 |
+
# Si toutes les tables critiques ont été vérifiées avec succès
|
| 401 |
+
return "operational"
|
| 402 |
+
|
| 403 |
+
|
| 404 |
+
def get_health_status() -> Dict:
|
| 405 |
+
"""
|
| 406 |
+
Collecte l'état de santé de tous les services pour la page /statut.
|
| 407 |
+
"""
|
| 408 |
+
|
| 409 |
+
db_status = check_baserow_connection()
|
| 410 |
+
|
| 411 |
+
# L'état de l'authentification et de l'API principale sont
|
| 412 |
+
# généralement liés à l'état de la DB pour une application simple.
|
| 413 |
+
# Si la DB est HS, l'auth est HS. Sinon, ils sont OK.
|
| 414 |
+
|
| 415 |
+
auth_status = db_status # Lié à la DB (pour charger les utilisateurs)
|
| 416 |
+
api_endpoint_status = "operational" # L'endpoint Flask lui-même est considéré comme OK s'il tourne
|
| 417 |
+
|
| 418 |
+
# Version du service (pour information)
|
| 419 |
+
service_version = os.environ.get("SERVICE_VERSION", "1.0.0 (Baserow)")
|
| 420 |
+
|
| 421 |
+
|
| 422 |
+
return {
|
| 423 |
+
# Ces valeurs correspondent aux attributs 'data-status' dans statut.html
|
| 424 |
+
"auth": auth_status,
|
| 425 |
+
"data_storage": db_status,
|
| 426 |
+
"api_endpoint": api_endpoint_status,
|
| 427 |
+
"version": service_version,
|
| 428 |
+
"last_update": datetime.now().strftime("%Y-%m-%d %H:%M:%S UTC")
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
def is_baserow_up() -> bool:
|
| 432 |
+
"""
|
| 433 |
+
Vérifie l'état de Baserow en utilisant l'URL qui garantit un statut 100% fonctionnel
|
| 434 |
+
sur Hugging Face, SANS utiliser la fonction de construction d'URL de table.
|
| 435 |
+
"""
|
| 436 |
+
try:
|
| 437 |
+
# Envoie une requête GET à l'URL qui répond positivement pour le health check.
|
| 438 |
+
response = requests.get(
|
| 439 |
+
HEALTH_CHECK_URL,
|
| 440 |
+
headers=HEADERS,
|
| 441 |
+
timeout=5
|
| 442 |
+
)
|
| 443 |
+
# On vérifie si la réponse est un succès (code 200).
|
| 444 |
+
return response.status_code == 200
|
| 445 |
+
except requests.exceptions.RequestException as e:
|
| 446 |
+
print(f"DEBUG: Baserow health check failed: {e}")
|
| 447 |
+
return False
|
| 448 |
+
|
| 449 |
+
def log_baserow_api_call(method: str, url: str, headers: Dict, data: Optional[Dict] = None, log_response: bool = True):
|
| 450 |
+
"""
|
| 451 |
+
Fonction utilitaire pour effectuer des appels API à Baserow et journaliser
|
| 452 |
+
les requêtes et les réponses dans les logs du Space Hugging Face.
|
| 453 |
+
"""
|
| 454 |
+
|
| 455 |
+
# 1. Journalisation de la requête
|
| 456 |
+
logger.info(f"BASEROW REQUEST: {method} {url}")
|
| 457 |
+
# ATTENTION: Ne pas logger le token API complet!
|
| 458 |
+
logged_headers = {k: v.replace(API_TOKEN, '[TOKEN_MASKED]') if k == 'Authorization' else v for k, v in headers.items()}
|
| 459 |
+
logger.debug(f"BASEROW REQUEST Headers: {logged_headers}")
|
| 460 |
+
if data:
|
| 461 |
+
# Pour les requêtes POST/PUT, logger les données (sans le hash du mot de passe si possible)
|
| 462 |
+
logged_data = data.copy() if isinstance(data, dict) else data
|
| 463 |
+
if isinstance(logged_data, dict) and 'Hachage du mot de passe' in logged_data:
|
| 464 |
+
logged_data['Hachage du mot de passe'] = '[PASSWORD_HASH_MASKED]'
|
| 465 |
+
logger.debug(f"BASEROW REQUEST Body: {logged_data}")
|
| 466 |
+
|
| 467 |
+
# 2. Exécution de la requête
|
| 468 |
+
try:
|
| 469 |
+
if method == "GET":
|
| 470 |
+
response = requests.get(url, headers=headers)
|
| 471 |
+
elif method == "POST":
|
| 472 |
+
response = requests.post(url, headers=headers, json=data)
|
| 473 |
+
elif method == "PUT":
|
| 474 |
+
response = requests.put(url, headers=headers, json=data)
|
| 475 |
+
elif method == "DELETE":
|
| 476 |
+
response = requests.delete(url, headers=headers)
|
| 477 |
+
else:
|
| 478 |
+
raise ValueError(f"Méthode HTTP non supportée: {method}")
|
| 479 |
+
|
| 480 |
+
# 3. Journalisation de la réponse
|
| 481 |
+
if log_response:
|
| 482 |
+
logger.info(f"BASEROW RESPONSE: Status {response.status_code}")
|
| 483 |
+
# Journaliser le contenu pour les erreurs
|
| 484 |
+
if response.status_code >= 400:
|
| 485 |
+
logger.error(f"BASEROW ERROR RESPONSE Body: {response.text}")
|
| 486 |
+
|
| 487 |
+
# 4. Retourner la réponse
|
| 488 |
+
return response
|
| 489 |
+
|
| 490 |
+
except requests.exceptions.RequestException as e:
|
| 491 |
+
logger.error(f"BASEROW CONNECTION ERROR: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
return None # Retourne None en cas d'erreur de connexion non gérée par le statut HTTP
|