[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
# © 2026 TechnoLibre (http://www.technolibre.ca)
|
|
|
|
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
|
|
|
|
"""Les sessions Claude Code de la machine : lesquelles vivent, lesquelles
|
|
|
|
|
se reprennent.
|
|
|
|
|
|
|
|
|
|
Ce module ne fait que LISTER. Construire la ligne de commande qui interroge
|
|
|
|
|
une session appartient à `backends.py`, qui garde l'invite hors de l'argv et
|
|
|
|
|
impose la lecture seule par des drapeaux.
|
|
|
|
|
|
|
|
|
|
**Deux sources, et la première fait autorité.** `claude agents --json` est le
|
|
|
|
|
listage que l'outil publie ; il n'exige pas de terminal et rend pid,
|
|
|
|
|
répertoire, genre, identifiant, nom et état. Le registre par processus, sous
|
|
|
|
|
le répertoire de configuration, y ajoute la version et le moment de démarrage.
|
|
|
|
|
Un `claude -p` en cours n'est dans NI l'un NI l'autre : seules les sessions
|
|
|
|
|
interactives et d'arrière-plan s'y inscrivent, donc l'absence d'une session
|
|
|
|
|
de la liste ne prouve pas qu'aucune ne tourne.
|
|
|
|
|
|
|
|
|
|
**Un pid ne suffit pas à dire qu'une session vit.** Les pids se recyclent, et
|
|
|
|
|
une entrée laissée par un arrêt brutal désignerait alors le processus d'un
|
|
|
|
|
autre. Le registre porte le moment de démarrage du processus ; la vivacité se
|
|
|
|
|
prouve donc par pid vivant ET démarrage identique, jamais par le pid seul.
|
|
|
|
|
|
|
|
|
|
**Ce que l'affichage a le droit de montrer.** Le registre est lisible par tout
|
|
|
|
|
compte de la machine — pid, répertoire, nom, identifiant n'y sont donc pas des
|
|
|
|
|
secrets. Les TRANSCRIPTIONS, elles, sont sous un répertoire fermé à leur
|
|
|
|
|
propriétaire seul, et c'est une frontière que le système a déjà tracée : ni
|
|
|
|
|
titre, ni invite, ni message n'en sort ici. Un transcript n'est lu que pour
|
|
|
|
|
deux champs de STRUCTURE — le répertoire de travail et la branche git — parce
|
|
|
|
|
que le nom du répertoire qui les contient est une transformation à perte : les
|
|
|
|
|
séparateurs, les points et les tirets bas y deviennent tous des tirets, donc
|
|
|
|
|
deux dépôts voisins s'y confondent.
|
|
|
|
|
|
|
|
|
|
**Une transcription ne se charge jamais en entier.** La plus grosse de cette
|
|
|
|
|
machine se compte en dizaines de mégaoctets ; seules les premières lignes sont
|
|
|
|
|
lues, et le listage se garde de les relire à chaque affichage.
|
|
|
|
|
"""
|
[REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.
--- EN ---
This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.
Assisted-by: Claude Opus 5
2026-09-24 13:30:31 -04:00
|
|
|
|
[ADD] assistant : serveur LLM reconnu, catalogue gpt, sessions locales
L'entrée « Question IA » envoyait toute question à une seule API distante, sur
un modèle figé et sans historique : aucun serveur local n'était atteignable,
et rien ne disait où partait le texte.
Deux règles gouvernent ce qui la remplace. Un port ne dit jamais qui répond —
un seul en héberge jusqu'à trois, et une famille réémet l'API d'une autre en
entier — donc l'identité se lit dans le CORPS, par une échelle ordonnée. Et
une adresse ne devient jamais du texte d'invite : le détecteur du dépôt ne
voit pas les noms d'hôte, d'où une poignée opaque à sa place.
Vérifié : 284 tests, et contre un serveur du réseau — reconnaissance,
capacités lues, conversation multi-tours, triage juste.
--- EN ---
The "AI question" entry sent every question to a single remote API, on a
frozen model and with no history: no local server was reachable, and nothing
said where the text was going.
Two rules govern what replaces it. A port never says who answers — one hosts
up to three products, and one family re-serves another's native API in full —
so identity is read from the BODY, by an ordered ladder. And an address never
becomes prompt text: the repository's detector does not see host names, hence
an opaque handle in its place.
Checked: 284 tests, and against a server on the network — recognition,
capabilities read, a multi-turn conversation, a correct triage.
Assisted-by: Claude Opus 5
2026-09-09 06:41:56 -04:00
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import subprocess
|
|
|
|
|
from dataclasses import dataclass, replace
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
# Le listage que l'outil publie. Sans terminal, il refuse sa forme lisible et
|
|
|
|
|
# renvoie explicitement vers celle-ci, qui est donc la seule utilisable ici.
|
|
|
|
|
AGENTS_ARGV = ("claude", "agents", "--json")
|
|
|
|
|
|
|
|
|
|
# Le registre des processus vivants, un fichier par pid.
|
|
|
|
|
REGISTRE = "~/.claude/sessions"
|
|
|
|
|
|
|
|
|
|
# Les transcriptions persistées, un répertoire par projet.
|
|
|
|
|
PROJETS = "~/.claude/projects"
|
|
|
|
|
|
|
|
|
|
# Le champ de « /proc/<pid>/stat » qui porte le moment de démarrage, compté
|
|
|
|
|
# depuis le premier. C'est lui qui distingue un pid recyclé d'un pid vivant.
|
|
|
|
|
CHAMP_DEMARRAGE = 22
|
|
|
|
|
|
|
|
|
|
# Ce que le listage lit dans une transcription, et rien d'autre : deux champs
|
|
|
|
|
# de structure. Le contenu des messages n'est pas de ce côté-ci de la
|
|
|
|
|
# frontière que le système a posée sur le répertoire.
|
|
|
|
|
CHAMPS_TRANSCRIPT = ("cwd", "gitBranch")
|
|
|
|
|
|
|
|
|
|
# Le nombre de lignes lues en tête d'une transcription pour y trouver ces deux
|
|
|
|
|
# champs. Les premiers enregistrements les portent tous ; en lire plus
|
|
|
|
|
# coûterait des mégaoctets pour la même réponse.
|
|
|
|
|
LIGNES_EN_TETE = 40
|
|
|
|
|
|
|
|
|
|
# Le délai d'un appel au listage. Il borne l'attente si l'outil est absent ou
|
|
|
|
|
# occupé, pour que le menu rende la main.
|
|
|
|
|
DELAI = 15
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class Session:
|
|
|
|
|
"""Une session, telle que le registre l'annonce.
|
|
|
|
|
|
|
|
|
|
`live` dit qu'un processus la tient EN CE MOMENT, prouvé par son moment
|
|
|
|
|
de démarrage et non par son seul pid. `kind` vaut `interactive` ou
|
|
|
|
|
`background`, et cette distinction décide du risque : reprendre une
|
|
|
|
|
session tenue par un terminal n'est pas refusé par l'outil, là où une
|
|
|
|
|
session d'arrière-plan l'est.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
session_id: str
|
|
|
|
|
pid: int = 0
|
|
|
|
|
kind: str = ""
|
|
|
|
|
status: str = ""
|
|
|
|
|
cwd: str = ""
|
|
|
|
|
name: str = ""
|
|
|
|
|
version: str = ""
|
|
|
|
|
branch: str = ""
|
|
|
|
|
live: bool = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def live(*, run=None, read_registry=None, read_stat=None) -> list[Session]:
|
|
|
|
|
"""Les sessions qu'un processus tient en ce moment.
|
|
|
|
|
|
|
|
|
|
`run(argv)` rend le texte du listage, `read_registry()` les entrées du
|
|
|
|
|
registre, `read_stat(pid)` le contenu de l'état d'un processus : les trois
|
|
|
|
|
coutures permettent à un test de décrire une flotte entière sans qu'aucune
|
|
|
|
|
session réelle ne soit lue ni dérangée.
|
|
|
|
|
|
|
|
|
|
Rend une liste VIDE quand l'outil est absent ou muet — une machine sans
|
|
|
|
|
Claude Code n'est pas une panne du menu.
|
|
|
|
|
"""
|
|
|
|
|
lanceur = run or _lancer
|
|
|
|
|
entrees = (read_registry or _lire_registre)()
|
|
|
|
|
par_pid = {int(e.get("pid", 0) or 0): e for e in entrees}
|
|
|
|
|
trouvees = []
|
|
|
|
|
for brute in _agents(lanceur):
|
|
|
|
|
pid = int(brute.get("pid", 0) or 0)
|
|
|
|
|
enrichie = par_pid.get(pid, {})
|
|
|
|
|
vivante = is_live(pid, enrichie.get("procStart"), read_stat=read_stat)
|
|
|
|
|
trouvees.append(
|
|
|
|
|
Session(
|
|
|
|
|
session_id=str(brute.get("sessionId") or ""),
|
|
|
|
|
pid=pid,
|
|
|
|
|
kind=str(brute.get("kind") or ""),
|
|
|
|
|
status=str(brute.get("status") or ""),
|
|
|
|
|
cwd=str(brute.get("cwd") or ""),
|
|
|
|
|
name=str(brute.get("name") or ""),
|
|
|
|
|
version=str(enrichie.get("version") or ""),
|
|
|
|
|
live=vivante,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return trouvees
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def is_live(pid, procstart, *, read_stat=None) -> bool:
|
|
|
|
|
"""Ce pid porte-t-il TOUJOURS la session que le registre y attachait ?
|
|
|
|
|
|
|
|
|
|
Un pid vivant ne suffit pas : les pids se recyclent, et une entrée laissée
|
|
|
|
|
par un arrêt brutal désignerait le processus d'un autre. Le moment de
|
|
|
|
|
démarrage du processus tranche — il est propre à un démarrage, donc un pid
|
|
|
|
|
réattribué ne le porte pas.
|
|
|
|
|
|
|
|
|
|
Sans moment de démarrage connu, la réponse est la présence du pid : c'est
|
|
|
|
|
ce que le listage de l'outil affirme déjà, et le prétendre mort serait
|
|
|
|
|
plus faux que de le croire vivant.
|
|
|
|
|
"""
|
|
|
|
|
lecteur = read_stat or _lire_stat
|
|
|
|
|
contenu = lecteur(pid)
|
|
|
|
|
if not contenu:
|
|
|
|
|
return False
|
|
|
|
|
if procstart in (None, ""):
|
|
|
|
|
return True
|
|
|
|
|
champs = contenu.rsplit(")", 1)[-1].split()
|
|
|
|
|
# Le champ compté depuis le premier, et le nom du programme — qui peut
|
|
|
|
|
# contenir des espaces — est déjà écarté par la coupe ci-dessus.
|
|
|
|
|
rang = CHAMP_DEMARRAGE - 3
|
|
|
|
|
if rang >= len(champs):
|
|
|
|
|
return True
|
|
|
|
|
return champs[rang] == str(procstart)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def resumable(*, projects_root=None, read_head=None) -> list[Session]:
|
|
|
|
|
"""Les sessions persistées, reprenables et sans processus.
|
|
|
|
|
|
|
|
|
|
Le répertoire de travail se LIT dans la transcription, jamais dans le nom
|
|
|
|
|
du répertoire qui la contient : cette transformation remplace les
|
|
|
|
|
séparateurs, les points et les tirets bas par des tirets, donc elle ne
|
|
|
|
|
s'inverse pas et confondrait deux dépôts voisins.
|
|
|
|
|
|
|
|
|
|
Deux champs sont lus, et deux seulement — le répertoire et la branche.
|
|
|
|
|
Aucun titre, aucune invite, aucun message : la transcription est sous un
|
|
|
|
|
répertoire que le système ferme à son propriétaire, et cette frontière
|
|
|
|
|
n'est pas à rouvrir pour décorer une liste.
|
|
|
|
|
"""
|
|
|
|
|
racine = Path(projects_root or os.path.expanduser(PROJETS))
|
|
|
|
|
lecteur = read_head or _lire_en_tete
|
|
|
|
|
trouvees = []
|
|
|
|
|
for chemin in _transcripts(racine):
|
|
|
|
|
faits = _structure(lecteur(chemin))
|
|
|
|
|
trouvees.append(
|
|
|
|
|
Session(
|
|
|
|
|
session_id=chemin.name[: -len(".jsonl")],
|
|
|
|
|
cwd=faits.get("cwd", ""),
|
|
|
|
|
branch=faits.get("gitBranch", ""),
|
|
|
|
|
live=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
return sorted(trouvees, key=lambda s: s.session_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def fleet(
|
|
|
|
|
*,
|
|
|
|
|
run=None,
|
|
|
|
|
read_registry=None,
|
|
|
|
|
read_stat=None,
|
|
|
|
|
projects_root=None,
|
|
|
|
|
read_head=None,
|
|
|
|
|
) -> list[Session]:
|
|
|
|
|
"""La flotte : les sessions vivantes, puis celles qui se reprennent.
|
|
|
|
|
|
|
|
|
|
Une session persistée est aussi présente tant qu'un processus la tient :
|
|
|
|
|
les deux listages se recouvrent donc, et les présenter côte à côte
|
|
|
|
|
montrerait deux fois la même session, une fois vivante et une fois comme
|
|
|
|
|
reprenable. La fusion garde l'entrée VIVANTE, qui porte le pid, le genre
|
|
|
|
|
et l'état — c'est-à-dire tout ce qui décide du risque.
|
|
|
|
|
|
|
|
|
|
Les vivantes ouvrent la liste : ce sont celles où écrire coûte quelque
|
|
|
|
|
chose.
|
|
|
|
|
"""
|
|
|
|
|
persistees = {
|
|
|
|
|
session.session_id: session
|
|
|
|
|
for session in resumable(
|
|
|
|
|
projects_root=projects_root, read_head=read_head
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
# La branche vient de la transcription, que le listage de l'outil ne
|
|
|
|
|
# connaît pas : sans cette reprise, elle paraîtrait pour les sessions
|
|
|
|
|
# dormantes et manquerait pour les vivantes, ce qui se lit comme un
|
|
|
|
|
# défaut alors que l'information est là.
|
|
|
|
|
vivantes = [
|
|
|
|
|
replace(
|
|
|
|
|
session,
|
|
|
|
|
branch=getattr(persistees.get(session.session_id), "branch", ""),
|
|
|
|
|
)
|
|
|
|
|
for session in live(
|
|
|
|
|
run=run, read_registry=read_registry, read_stat=read_stat
|
|
|
|
|
)
|
|
|
|
|
]
|
|
|
|
|
connues = {session.session_id for session in vivantes}
|
|
|
|
|
dormantes = [
|
|
|
|
|
session
|
|
|
|
|
for identifiant, session in persistees.items()
|
|
|
|
|
if identifiant not in connues
|
|
|
|
|
]
|
|
|
|
|
return vivantes + dormantes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def displayable(session) -> dict:
|
|
|
|
|
"""Ce qu'une session a le droit de montrer à l'écran.
|
|
|
|
|
|
|
|
|
|
Le répertoire est réduit à son dernier segment et l'identifiant à son
|
|
|
|
|
préfixe : les deux suffisent à reconnaître une session sans étaler le
|
|
|
|
|
chemin d'un compte, que le détecteur du dépôt compte d'ailleurs parmi les
|
|
|
|
|
données identifiantes.
|
|
|
|
|
"""
|
|
|
|
|
return {
|
|
|
|
|
"id": session.session_id[:8],
|
|
|
|
|
"pid": session.pid,
|
|
|
|
|
"kind": session.kind,
|
|
|
|
|
"status": session.status,
|
|
|
|
|
"dir": Path(session.cwd).name if session.cwd else "",
|
|
|
|
|
"name": session.name,
|
|
|
|
|
"version": session.version,
|
|
|
|
|
"branch": session.branch,
|
|
|
|
|
"live": session.live,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def held_by(session) -> str:
|
|
|
|
|
"""Le pid qui tient cette session, ou "" quand personne ne la tient.
|
|
|
|
|
|
|
|
|
|
Sert la seule question qui compte avant d'écrire dans une session : y
|
|
|
|
|
a-t-il quelqu'un dedans. Reprendre une session tenue par un terminal n'est
|
|
|
|
|
PAS refusé par l'outil, et deux écritures simultanées scindent la
|
|
|
|
|
transcription en silence — une branche est alors orpheline. Le menu
|
|
|
|
|
demande donc, et branche une copie par défaut.
|
|
|
|
|
"""
|
|
|
|
|
return str(session.pid) if session.live and session.pid else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _agents(lanceur):
|
|
|
|
|
"""Les entrées du listage publié par l'outil, ou une liste vide."""
|
|
|
|
|
texte = lanceur(list(AGENTS_ARGV))
|
|
|
|
|
if not texte:
|
|
|
|
|
return []
|
|
|
|
|
try:
|
|
|
|
|
charge = json.loads(texte)
|
|
|
|
|
except ValueError:
|
|
|
|
|
return []
|
|
|
|
|
return (
|
|
|
|
|
[e for e in charge if isinstance(e, dict)]
|
|
|
|
|
if isinstance(charge, list)
|
|
|
|
|
else []
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lancer(argv):
|
|
|
|
|
"""La sortie standard du listage, ou "" quand l'outil manque."""
|
|
|
|
|
try:
|
|
|
|
|
answer = subprocess.run(
|
|
|
|
|
argv, capture_output=True, text=True, timeout=DELAI
|
|
|
|
|
)
|
|
|
|
|
except (OSError, subprocess.SubprocessError):
|
|
|
|
|
return ""
|
|
|
|
|
return answer.stdout if answer.returncode == 0 else ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lire_registre():
|
|
|
|
|
"""Les entrées du registre des processus vivants.
|
|
|
|
|
|
|
|
|
|
Les fichiers de jetons voisins ne sont jamais ouverts : ils portent une
|
|
|
|
|
autorisation de messagerie, et ce listage n'a rien à en faire.
|
|
|
|
|
"""
|
|
|
|
|
racine = Path(os.path.expanduser(REGISTRE))
|
|
|
|
|
entrees = []
|
|
|
|
|
try:
|
|
|
|
|
fichiers = sorted(racine.glob("*.json"))
|
|
|
|
|
except OSError:
|
|
|
|
|
return entrees
|
|
|
|
|
for chemin in fichiers:
|
|
|
|
|
try:
|
|
|
|
|
entrees.append(json.loads(chemin.read_text()))
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
# Un registre à moitié écrit ne doit pas cacher les autres.
|
|
|
|
|
continue
|
|
|
|
|
return [e for e in entrees if isinstance(e, dict)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lire_stat(pid):
|
|
|
|
|
"""Le contenu de l'état d'un processus, ou "" s'il n'existe plus."""
|
|
|
|
|
try:
|
|
|
|
|
return Path(f"/proc/{int(pid)}/stat").read_text()
|
|
|
|
|
except (OSError, ValueError):
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _transcripts(racine):
|
|
|
|
|
"""Les transcriptions persistées, triées, sans descendre plus bas.
|
|
|
|
|
|
|
|
|
|
Les sous-répertoires par session portent des travaux dérivés — agents,
|
|
|
|
|
flux — que ce listage n'a pas à parcourir.
|
|
|
|
|
"""
|
|
|
|
|
try:
|
|
|
|
|
return sorted(
|
|
|
|
|
chemin
|
|
|
|
|
for projet in sorted(racine.iterdir())
|
|
|
|
|
if projet.is_dir()
|
|
|
|
|
for chemin in sorted(projet.glob("*.jsonl"))
|
|
|
|
|
)
|
|
|
|
|
except OSError:
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _lire_en_tete(chemin):
|
|
|
|
|
"""Les premières lignes d'une transcription.
|
|
|
|
|
|
|
|
|
|
En tête seulement : une transcription se compte en mégaoctets, et les
|
|
|
|
|
premiers enregistrements portent déjà les deux champs cherchés.
|
|
|
|
|
"""
|
|
|
|
|
lignes = []
|
|
|
|
|
try:
|
|
|
|
|
with open(chemin, encoding="utf-8", errors="replace") as fichier:
|
|
|
|
|
for rang, ligne in enumerate(fichier):
|
|
|
|
|
if rang >= LIGNES_EN_TETE:
|
|
|
|
|
break
|
|
|
|
|
lignes.append(ligne)
|
|
|
|
|
except OSError:
|
|
|
|
|
return []
|
|
|
|
|
return lignes
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _structure(lignes):
|
|
|
|
|
"""Les deux champs de structure, pris dans les premiers enregistrements.
|
|
|
|
|
|
|
|
|
|
Ne lit que les clés déclarées : un enregistrement porte aussi le contenu
|
|
|
|
|
des messages, et le parcourir pour en extraire deux champs ne donne aucun
|
|
|
|
|
droit sur le reste.
|
|
|
|
|
"""
|
|
|
|
|
faits = {}
|
|
|
|
|
for ligne in lignes or ():
|
|
|
|
|
try:
|
|
|
|
|
enregistrement = json.loads(ligne)
|
|
|
|
|
except ValueError:
|
|
|
|
|
continue
|
|
|
|
|
if not isinstance(enregistrement, dict):
|
|
|
|
|
continue
|
|
|
|
|
for champ in CHAMPS_TRANSCRIPT:
|
|
|
|
|
valeur = enregistrement.get(champ)
|
|
|
|
|
if champ not in faits and isinstance(valeur, str) and valeur:
|
|
|
|
|
faits[champ] = valeur
|
|
|
|
|
if len(faits) == len(CHAMPS_TRANSCRIPT):
|
|
|
|
|
break
|
|
|
|
|
return faits
|