* correction de la fonction grist_priority_bals ()
* ajout des colonnes en .env
This commit is contained in:
+47
-194
@@ -37,6 +37,9 @@ Variables d'environnement requises (fichier .env ou export shell) :
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import http
|
||||
import http.client
|
||||
import json
|
||||
import logging
|
||||
import math
|
||||
import os
|
||||
@@ -54,7 +57,6 @@ import requests
|
||||
# ─────────────────────────────────────────────
|
||||
# Arguments CLI
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Daemon imapsync piloté par Grist",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
@@ -71,11 +73,10 @@ parser.add_argument(
|
||||
)
|
||||
ARGS = parser.parse_args()
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Configuration depuis l'environnement
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def require_env(name: str) -> str:
|
||||
val = os.environ.get(name)
|
||||
if not val:
|
||||
@@ -104,7 +105,7 @@ load_dotenv()
|
||||
|
||||
GRIST_API_KEY = require_env("GRIST_API_KEY")
|
||||
GRIST_DOC_ID = require_env("GRIST_DOC_ID")
|
||||
GRIST_BASE_URL = opt_env("GRIST_BASE_URL", "http://localhost:8484")
|
||||
GRIST_BASE_URL = require_env("GRIST_BASE_URL")
|
||||
HOST1 = require_env("HOST1")
|
||||
HOST2 = require_env("HOST2")
|
||||
USER1_PREFIX = require_env("USER1_PREFIX")
|
||||
@@ -120,18 +121,23 @@ N_WORKERS = int(opt_env("N_WORKERS", "4"))
|
||||
POLL_INTERVAL = int(opt_env("POLL_INTERVAL", "30"))
|
||||
NEW_RATIO = float(opt_env("NEW_RATIO", "0.20"))
|
||||
|
||||
# Interval minimum entre 2 vérification d'une meme BAL
|
||||
# Interval en minute minimum entre 2 vérification d'une meme BAL
|
||||
MIN_INTERVAL_MINUTES = 15
|
||||
|
||||
|
||||
# Noms des tables Grist (casse exacte telle qu'affichée dans Grist)
|
||||
TABLE_BALS = "BALs"
|
||||
TABLE_HISTORIQUE = "Historique"
|
||||
TABLE_BALS = require_env("TABLE_BALS")
|
||||
|
||||
# Colonnes Grist
|
||||
COL_EMAIL = "Courriel"
|
||||
COL_SYNC = "Synchronisation" # booléen déclencheur
|
||||
COL_NB_SYNCS = "Nb_syncs" # compteur calculé par formule Grist
|
||||
COL_EMAIL = require_env("COL_EMAIL")
|
||||
COL_SYNC = require_env("COL_SYNC")
|
||||
COL_NB_SYNCS = require_env("COL_NB_SYNCS")
|
||||
COL_PRIORITY = require_env("COL_PRIORITY")
|
||||
TABLE_HISTORIQUE = require_env("TABLE_HISTORIQUE")
|
||||
COL_BAL = require_env("COL_BAL")
|
||||
COL_STATUS = require_env("COL_STATUS")
|
||||
COL_NB_EMAILS = require_env("COL_NB_EMAILS")
|
||||
COL_DUREE_SEC = require_env("COL_DUREE_SEC")
|
||||
COL_LOG = require_env("COL_LOG")
|
||||
COL_DATE = require_env("COL_DATE")
|
||||
|
||||
TOKEN_VALIDITY = 30 # Minutes
|
||||
|
||||
@@ -140,7 +146,6 @@ _LAST_RENEWAL_TIME = 0.0
|
||||
# ─────────────────────────────────────────────
|
||||
# Logging
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(message)s",
|
||||
@@ -152,11 +157,10 @@ logging.basicConfig(
|
||||
)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Renouvellement du token OAuth2
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def renew_oauth2_token() -> bool:
|
||||
"""
|
||||
Exécute : OAUTH2IMAP --token_file OAUTH2_TOKEN_FILE USER1_PREFIX
|
||||
@@ -201,181 +205,22 @@ def renew_oauth2_token() -> bool:
|
||||
# ─────────────────────────────────────────────
|
||||
# Mode --test
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def run_tests() -> bool:
|
||||
"""
|
||||
Vérifie tous les prérequis de la configuration.
|
||||
Retourne True si tout est OK, False sinon.
|
||||
"""
|
||||
ok = True
|
||||
sep = "─" * 56
|
||||
|
||||
def check(label: str, passed: bool, detail: str = ""):
|
||||
nonlocal ok
|
||||
icon = "✓" if passed else "✗"
|
||||
msg = f" {icon} {label}"
|
||||
if detail:
|
||||
msg += f" → {detail}"
|
||||
print(msg)
|
||||
if not passed:
|
||||
ok = False
|
||||
|
||||
print()
|
||||
print("═" * 56)
|
||||
print(" imapsync_daemon — test de configuration")
|
||||
print("═" * 56)
|
||||
|
||||
# ── Variables d'environnement ────────────────
|
||||
print(f"\n{sep}")
|
||||
print(" Variables d'environnement")
|
||||
print(sep)
|
||||
env_checks = [
|
||||
("GRIST_API_KEY", GRIST_API_KEY, None),
|
||||
("GRIST_DOC_ID", GRIST_DOC_ID, None),
|
||||
("GRIST_BASE_URL", GRIST_BASE_URL, GRIST_BASE_URL),
|
||||
("HOST1", HOST1, HOST1),
|
||||
("HOST2", HOST2, HOST2),
|
||||
("USER1_PREFIX", USER1_PREFIX, USER1_PREFIX),
|
||||
("USER2_PREFIX", USER2_PREFIX, USER2_PREFIX),
|
||||
("OAUTH2_TOKEN_FILE", OAUTH2_TOKEN_FILE, OAUTH2_TOKEN_FILE),
|
||||
("PASS2", PASS2, "défini"),
|
||||
("IMAPSYNC", IMAPSYNC, IMAPSYNC),
|
||||
("OAUTH2IMAP", OAUTH2IMAP, OAUTH2IMAP),
|
||||
("TMPDIR_CACHE", TMPDIR_CACHE, TMPDIR_CACHE),
|
||||
("N_WORKERS", str(N_WORKERS), str(N_WORKERS)),
|
||||
("POLL_INTERVAL", str(POLL_INTERVAL), f"{POLL_INTERVAL}s"),
|
||||
("NEW_RATIO", str(NEW_RATIO), f"{NEW_RATIO:.0%}"),
|
||||
]
|
||||
for name, val, shown in env_checks:
|
||||
set_ok = bool(val)
|
||||
check(name, set_ok, shown if set_ok else "MANQUANTE")
|
||||
|
||||
# ── Fichiers et répertoires ──────────────────
|
||||
print(f"\n{sep}")
|
||||
print(" Fichiers et répertoires")
|
||||
print(sep)
|
||||
|
||||
for exe_name, exe_path in [("imapsync", IMAPSYNC), ("oauth2imap", OAUTH2IMAP)]:
|
||||
p = Path(exe_path)
|
||||
found = p.exists() and os.access(p, os.X_OK)
|
||||
check(
|
||||
f"{exe_name} exécutable",
|
||||
found,
|
||||
str(p) + (" (trouvé)" if found else " (introuvable)"),
|
||||
)
|
||||
|
||||
token_p = Path(OAUTH2_TOKEN_FILE)
|
||||
check(
|
||||
"OAUTH2_TOKEN_FILE",
|
||||
token_p.exists() and token_p.is_file(),
|
||||
str(token_p) + (" (trouvé)" if token_p.exists() else " (introuvable)"),
|
||||
)
|
||||
|
||||
tmpdir_p = Path(TMPDIR_CACHE)
|
||||
if tmpdir_p.exists():
|
||||
check("TMPDIR_CACHE", True, f"{tmpdir_p} (existe)")
|
||||
else:
|
||||
try:
|
||||
tmpdir_p.mkdir(parents=True, exist_ok=True)
|
||||
check("TMPDIR_CACHE", True, f"{tmpdir_p} (créé)")
|
||||
except OSError as e:
|
||||
check("TMPDIR_CACHE", False, str(e))
|
||||
|
||||
# ── Renouvellement token ─────────────────────
|
||||
print(f"\n{sep}")
|
||||
print(" Renouvellement token OAuth2")
|
||||
print(sep)
|
||||
token_ok = renew_oauth2_token()
|
||||
check(
|
||||
"oauth2imap --token_file … renew",
|
||||
token_ok,
|
||||
"succès" if token_ok else "échec — vérifier oauth2imap et le token",
|
||||
)
|
||||
|
||||
# ── Connectivité Grist ───────────────────────
|
||||
print(f"\n{sep}")
|
||||
print(" Connectivité Grist")
|
||||
print(sep)
|
||||
|
||||
grist_headers = {
|
||||
"Authorization": f"Bearer {GRIST_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{GRIST_BASE_URL}/api/docs/{GRIST_DOC_ID}",
|
||||
headers=grist_headers,
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
doc_name = resp.json().get("name", "?")
|
||||
check("Accès document Grist", True, f'"{doc_name}"')
|
||||
except requests.RequestException as e:
|
||||
check("Accès document Grist", False, str(e))
|
||||
|
||||
for table in (TABLE_BALS, TABLE_HISTORIQUE):
|
||||
try:
|
||||
resp = requests.get(
|
||||
f"{GRIST_BASE_URL}/api/docs/{GRIST_DOC_ID}/tables/{table}/records",
|
||||
headers=grist_headers,
|
||||
timeout=10,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
nb = len(resp.json().get("records", []))
|
||||
|
||||
if table == TABLE_BALS and nb > 0:
|
||||
sample = resp.json()["records"][0]["fields"]
|
||||
has_nb_syncs = COL_NB_SYNCS in sample
|
||||
detail = (
|
||||
f"{nb} enregistrements"
|
||||
if has_nb_syncs
|
||||
else f"{nb} enregistrements ⚠ colonne '{COL_NB_SYNCS}' absente — à créer dans Grist"
|
||||
)
|
||||
check(f"Lecture table {table}", True, detail)
|
||||
else:
|
||||
check(f"Lecture table {table}", True, f"{nb} enregistrements")
|
||||
except requests.RequestException as e:
|
||||
check(f"Lecture table {table}", False, str(e))
|
||||
|
||||
grist_add_historique("mathieu.maura@sdis66.fr", "Test", 0, 10, "Test")
|
||||
|
||||
# ── Paramètres calculés ──────────────────────
|
||||
print(f"\n{sep}")
|
||||
print(" Paramètres calculés")
|
||||
print(sep)
|
||||
slots_new = math.ceil(N_WORKERS * NEW_RATIO)
|
||||
slots_rep = N_WORKERS - slots_new
|
||||
print(f" • N_WORKERS={N_WORKERS} NEW_RATIO={NEW_RATIO:.0%}")
|
||||
print(f" → {slots_new} slot(s) nouvelles BAL")
|
||||
print(f" → {slots_rep} slot(s) re-passes")
|
||||
if ARGS.dry:
|
||||
print(" • Mode --dry actif : imapsync sera lancé avec --dry")
|
||||
|
||||
# ── Résumé ───────────────────────────────────
|
||||
print()
|
||||
print("═" * 56)
|
||||
if ok:
|
||||
print(" Résultat : OK — configuration valide")
|
||||
else:
|
||||
print(" Résultat : ERREUR — corriger les points marqués ✗")
|
||||
print("═" * 56)
|
||||
print()
|
||||
return ok
|
||||
http.client.HTTPConnection.debuglevel = 1
|
||||
print(f" → payload: {grist_priority_bals()}")
|
||||
return True
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────
|
||||
# Grist API
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
GRIST_HEADERS = {
|
||||
"Authorization": f"Bearer {GRIST_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
|
||||
def grist_url(table: str) -> str:
|
||||
def grist_records_url(table: str) -> str:
|
||||
return f"{GRIST_BASE_URL}/api/docs/{GRIST_DOC_ID}/tables/{table}/records"
|
||||
|
||||
|
||||
@@ -389,7 +234,10 @@ def grist_fetch_bals() -> list[dict]:
|
||||
# 1. Récupération triée via l'API Grist
|
||||
params = {"sort": "Date_derniere_passe"}
|
||||
resp = requests.get(
|
||||
grist_url(TABLE_BALS), headers=GRIST_HEADERS, params=params, timeout=15
|
||||
grist_records_url(TABLE_BALS),
|
||||
headers=GRIST_HEADERS,
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
records = resp.json().get("records", [])
|
||||
@@ -445,25 +293,29 @@ def grist_priority_bals() -> dict:
|
||||
log.info("grist_priority_bals")
|
||||
try:
|
||||
# 1. Récupération triée via l'API Grist
|
||||
params = {"limit": 1, "filter": {"priority": ["true"], COL_SYNC: ["true"]}}
|
||||
params = {
|
||||
"limit": 1,
|
||||
"filter": json.dumps({COL_PRIORITY: [True], COL_SYNC: [True]}),
|
||||
}
|
||||
resp = requests.get(
|
||||
grist_url(TABLE_BALS), headers=GRIST_HEADERS, json=params, timeout=15
|
||||
grist_records_url(TABLE_BALS),
|
||||
headers=GRIST_HEADERS,
|
||||
params=params,
|
||||
timeout=15,
|
||||
)
|
||||
|
||||
resp.raise_for_status()
|
||||
records = resp.json().get("records", [])
|
||||
|
||||
# for r in records:
|
||||
r = records[0]
|
||||
log.info(f"grist_priority_bals: {r.id}")
|
||||
fields = r["fields"]
|
||||
|
||||
# Vérification de la condition de synchronisation globale
|
||||
if fields.get(COL_SYNC) is not True:
|
||||
return {}
|
||||
|
||||
# Si le timestamp existe, on valide l'écart de temps
|
||||
if fields.get("Date_derniere_passe") is not None:
|
||||
# Au moins une passe déjà faite
|
||||
if fields.get("Date_derniere_passe") is None:
|
||||
return {}
|
||||
|
||||
return r
|
||||
@@ -487,12 +339,12 @@ def grist_add_historique(
|
||||
"records": [
|
||||
{
|
||||
"fields": {
|
||||
"Bal": email,
|
||||
"Status": final_status,
|
||||
"Nb_emails": nb_emails,
|
||||
"Duree_sec": duree_sec,
|
||||
"Log": log_text[:4000],
|
||||
"Date": datetime.now(timezone.utc).isoformat(),
|
||||
COL_BAL: email,
|
||||
COL_STATUS: final_status,
|
||||
COL_NB_EMAILS: nb_emails,
|
||||
COL_DUREE_SEC: duree_sec,
|
||||
COL_LOG: log_text[:4000],
|
||||
COL_DATE: datetime.now(timezone.utc).isoformat(),
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -500,7 +352,10 @@ def grist_add_historique(
|
||||
# print(f" → payload: {payload}")
|
||||
try:
|
||||
resp = requests.post(
|
||||
grist_url(TABLE_HISTORIQUE), headers=GRIST_HEADERS, json=payload, timeout=15
|
||||
grist_records_url(TABLE_HISTORIQUE),
|
||||
headers=GRIST_HEADERS,
|
||||
json=payload,
|
||||
timeout=15,
|
||||
)
|
||||
resp.raise_for_status()
|
||||
log.info(f"[{email}] Résultat écrit dans HISTORIQUE ({final_status})")
|
||||
@@ -511,8 +366,6 @@ def grist_add_historique(
|
||||
# ─────────────────────────────────────────────
|
||||
# Partition nouvelles BAL / re-passes
|
||||
# ─────────────────────────────────────────────
|
||||
|
||||
|
||||
def partition_bals(rows: list[dict], available: int) -> list[dict]:
|
||||
"""
|
||||
Répartit les slots disponibles (`available`) entre nouvelles BAL et re-passes.
|
||||
|
||||
Reference in New Issue
Block a user