Automathèque
Code de base pour automatheque.
Installation
mais il est peu probable que vous ayez besoin de l'installer, c'est avant tout une dépendance.
pip install automatheque
Dépendances
- voir
pyproject.toml
Install en mode dev
pip install -e .[dev,docs] ou monas install depuis la racine.
Usage : Utilitaire pour script
from automatheque.script import script # alias court de script_automatheque
@script(__doc__, __version__)
def main(_script):
print(_script.config)
if __name__ == "__main__":
main()
L'API a été promue de
automatheque.util.scriptversautomatheque.script(#41). L'ancien chemin reste importable (shim) mais émet unDeprecationWarning: migrez versfrom automatheque.script import script.
Le décorateur câble automatiquement, si le script les déclare dans son usage :
--dry-run (via _script.dry_run), la verbosité -v/-q (niveau de log), une
sortie propre sur Ctrl-C (code 130), et la durée d'exécution.
Sous-commandes (via commandopt)
On déclare des fonctions-commandes avec @commande([...]) (alias de
commandopt.commandopt) et on aiguille avec _script.execute_commande(). Les
options internes d'automatheque (--config, --dry-run, -v/-q) sont
exclues de la sélection, mais restent transmises à la commande.
"""Mon script
Usage:
mon_script.py (--ajouter | --supprimer) [--config=<f>] [-v]
"""
from automatheque.script import script, commande
@commande(["--ajouter"])
def ajouter(arguments): ...
@commande(["--supprimer"])
def supprimer(arguments): ...
@script(__doc__, __version__)
def main(_script):
return _script.execute_commande()
if __name__ == "__main__":
main()
Gestion des secrets
Un mot de passe ou un jeton ne doit jamais fuiter dans les logs ou une
traceback, et sa source ne devrait pas être figée dans le code (parfois une
variable d'environnement, parfois la config, parfois un trousseau système…).
Le module automatheque.secret répond aux deux besoins.
Secret : une valeur qui ne fuite pas
Secret enveloppe une valeur sensible : son repr, son str, un f-string et le
logging affichent tous ***. La valeur réelle n'est accessible qu'au point
d'usage, via .reveler().
from automatheque.secret import Secret
mdp = Secret("s3cr3t")
print(mdp) # ***
print(f"mdp={mdp}") # mdp=***
logging.info("mdp=%s", mdp) # …mdp=*** (pas de fuite dans les logs)
connexion.login("moi", mdp.reveler()) # .reveler() UNIQUEMENT ici
recup_secret : d'où vient le secret ?
recup_secret(cle, config=, resolveurs=) cherche la valeur auprès de plusieurs
sources essayées dans l'ordre (premier gagnant) et renvoie un Secret (ou
None si introuvable). Par défaut : la variable d'environnement puis, si on
lui passe une config, la configuration.
from automatheque.secret import recup_secret
# factrice.smtp.mdp → variable FACTRICE_SMTP_MDP,
# sinon [factrice.smtp] mdp = … dans la config
mdp = recup_secret("factrice.smtp.mdp", config=_script.config)
if mdp is not None:
serveur.login(user, mdp.reveler())
Les sources sont des greffons
Chaque source est un greffon (cf. automatheque.greffon) rendant la capacité
ResoudreSecret — on ajoute donc une nouvelle source comme n'importe quel
greffon. Fournis d'origine :
| Greffon | Source |
|---|---|
GreffonSecretEnv |
variable d'env (factrice.smtp.mdp → FACTRICE_SMTP_MDP) |
GreffonSecretConfig |
configuration (section.option) |
GreffonSecretKeyring |
trousseau système (dépendance optionnelle keyring) |
GreffonSecretCommande |
sortie d'une commande externe (p. ex. pass show {cle}) |
Pour un ordre ou des sources personnalisés, on passe resolveurs= (liste
ordonnée de greffons) — ici on interroge d'abord le trousseau, puis une commande :
from automatheque.secret import (
recup_secret,
GreffonSecretKeyring,
GreffonSecretCommande,
)
mdp = recup_secret(
"factrice.smtp.mdp",
resolveurs=[
GreffonSecretKeyring(service="mon-appli"),
GreffonSecretCommande(gabarit="pass show {cle}"),
],
)
Caviardage des logs (défense en profondeur)
La configuration de log par défaut (configure_logging_defaut(), appelée par un
script @script_automatheque) installe un filtre de caviardage : si la
valeur d'un Secret vivant apparaît dans un message de log, elle est remplacée
par ***.
mdp = recup_secret("factrice.smtp.mdp", config=_script.config)
logging.getLogger(__name__).info("connexion mdp=%s", mdp.reveler())
# → journalisé : « connexion mdp=*** » (même la valeur révélée est rattrapée)
La première ligne de défense reste de ne jamais logger un secret en clair
(Secret est déjà caviardé par str/repr) ; le filtre rattrape les fuites
indirectes. Si tu configures le logging toi-même (dictConfig maison), pose le
filtre sur tes handlers avec installe_caviardage() :
from automatheque.log import installe_caviardage
installe_caviardage() # racine par défaut (couvre les loggers enfants)
Un secret dans une section de configuration
Une section de configuration porte souvent la valeur la plus sensible de
l'application (mot de passe SMTP, clé d'API, jeton). Dans une classe de section
(cf. charge_section), on la
déclare avec Secret en converteur : la valeur est enveloppée dès le
chargement, donc caviardée partout où la classe est affichée.
import attr
from automatheque.configuration import charge_section
from automatheque.secret import Secret
@attr.s
class ConfigSmtp:
hote = attr.ib()
mdp = attr.ib(converter=Secret)
jeton = attr.ib(default=None, converter=attr.converters.optional(Secret))
smtp = charge_section(ConfigSmtp, _script.config, "smtp")
smtp # ConfigSmtp(hote='smtp.exemple.org', mdp=Secret(***), jeton=None)
serveur.login(smtp.hote, smtp.mdp.reveler()) # .reveler() au point d'usage
Sans cela l'option reste une chaîne nue : elle ressort telle quelle dans le
repr de la classe — donc dans une traceback ou un LOGGER.debug("config=%s", reglages) — et le filtre de caviardage ne la rattrape pas, puisqu'il ne connaît
que les Secret vivants.
Secret et recup_secret répondent à deux questions différentes ; ils se
composent, ils ne se remplacent pas :
| Question | Quand | |
|---|---|---|
converter=Secret |
« cette option est-elle sensible ? » | la valeur vit dans le .ini |
recup_secret |
« d'où vient la valeur ? » | env d'abord, puis config, trousseau… |
Une option secrète écrite dans le .ini doit de toute façon être déclarée
dans la classe : en mode strict (le défaut), charge_section refuse une option
inconnue. Autant la déclarer avec converter=Secret.
Greffons : annoncer et rendre une capacité
Un greffon déclare ce qu'il sait faire dans CAPACITES, et c'est par là qu'on le
retrouve — jamais par son nom :
from typing import Protocol
from automatheque.greffon import Greffon
from automatheque.greffon.capacite import Capacite
class Lire(Capacite, Protocol):
def lire(self) -> bool: ...
class GreffonLecteur(Greffon):
CAPACITES = [Lire]
def lire(self) -> bool: ...
lecteurs = Greffon.greffons_par_capacite(Lire)
Déclarer engage : la classe est vérifiée à sa définition. Un greffon qui
annonce Lire sans fournir lire() ne se définit pas — CapaciteNonRendue
nomme le greffon, la capacité et le membre manquant, à l'import du greffon
fautif, au lieu d'un AttributeError obscur chez l'appelant. Le membre peut
être hérité : seul compte le fait de le fournir.
Une sous-classe ajoute ses capacités à celles de ses mères :
class GreffonLecteurEcrivain(GreffonLecteur):
CAPACITES = [Ecrire] # rend Ecrire *et* Lire
greffon.capacites # [Ecrire, Lire]
L'appariement porte sur l'objet capacité, pas sur son nom : deux protocoles
homonymes définis dans des modules différents restent distincts. Une capacité
peut aussi être une simple chaîne — une étiquette, appariée par égalité, qui
n'achète évidemment aucune vérification (un Protocol sans membre rend le même
service avec la rigueur en plus).
Une capacité vit dans le module qui possède le domaine — ResoudreSecret est
définie dans automatheque.secret, aux côtés des greffons qui la rendent.
Configuration : sections typées et validées
_script.config (ou charge_configuration()) renvoie un ConfigParser brut :
tout y est chaîne, rien n'est validé, et une clé absente ou mal typée n'explose
que tard, au point d'accès. Pour valider tôt — et récupérer des valeurs
déjà typées — décris une section comme une classe attrs et peuple-la avec
charge_section :
import attr
from automatheque.configuration import charge_section, booleen, liste
@attr.s
class ConfigSmtp:
hote = attr.ib(validator=attr.validators.instance_of(str))
port = attr.ib(default=465, converter=int)
tls = attr.ib(default=True, converter=booleen)
relais = attr.ib(factory=list, converter=liste)
smtp = charge_section(ConfigSmtp, _script.config, "smtp")
smtp.port # 587 : un int, pas "587"
smtp.tls # True : un bool
pour la section :
[smtp]
hote = smtp.exemple.org
port = 587
tls = yes
relais = a.exemple.org, b.exemple.org
Une option sensible (mot de passe, jeton, clé d'API) se déclare avec Secret
en converteur, pour qu'elle ne fuite ni par le repr de la classe ni par une
traceback : cf. Un secret dans une section de
configuration.
Les converter/validator des attr.ib font la conversion (chaîne → int,
booleen, liste…) et le contrôle. L'erreur est précoce et nommée :
- section absente, option inconnue (une faute de frappe est rattrapée),
clé requise manquante, ou valeur refusée par un converter/validator →
ConfigurationInvalide(qui hérite deValueError), avec le nom de la section et de la clé fautive. charge_section(..., strict=False)ignore les options inconnues, quand une même section sert à plusieurs consommateurs.
Deux converteurs sont fournis, puisqu'un .ini ne rend que des chaînes :
| Converteur | .ini → |
reconnaît |
|---|---|---|
booleen |
bool |
yes/no, true/false, on/off, oui/non, vrai/faux |
liste |
list[str] |
valeurs séparées par des virgules (éléments vides ignorés) |
Valider la configuration d'un greffon
Un greffon qui a besoin d'une configuration déclare la classe qui la décrit
via CONFIG : sa validité devient alors celle de sa section, clé par clé —
et non plus le vague « une configuration existe-t-elle ? ».
import attr
from automatheque.configuration import booleen
from automatheque.greffon import Greffon
@attr.s
class ConfigMeteo:
"""Config attendue par le greffon météo (section [meteo])."""
cle_api = attr.ib(validator=attr.validators.instance_of(str))
hote = attr.ib(default="api.exemple.org", converter=str)
actif = attr.ib(default=True, converter=booleen)
@attr.s(eq=False)
class GreffonMeteo(Greffon):
CONFIG = ConfigMeteo
SECTION_CONFIG = "meteo" # facultatif : par défaut, la `cle` du greffon
Ce que ça donne :
| Appel | Comportement |
|---|---|
greffon.actif |
True si la section est présente et valide ; sinon False et un WARNING journalisé — ne lève jamais |
greffon.reglages |
l'instance de ConfigMeteo peuplée et typée (reglages.actif est un bool), mémoïsée |
greffon.valide_config() |
la version qui lève : ConfigurationInvalide nommant la section et la clé fautive |
FabriqueGreffon.active() s'appuie sur actif : un greffon dont la section est
absente, incomplète ou mal typée n'est pas activé, et la raison est
journalisée au lieu de ressurgir plus tard au point d'usage.
Sans CONFIG (le défaut), rien ne change : le comportement historique
(config_requise + présence d'une configuration) est conservé.
Configuration du logging
Automathèque ne configure rien à l'import (une bibliothèque ne doit pas
toucher au logging global). C'est l'application qui configure : un script
décoré par @script_automatheque appelle configure_logging_defaut() (sortie
console) puis applique la section [log] de sa configuration.
Un script étant une application, sa configuration de log vise la racine :
logging.getLogger(__name__) dans le script et les loggers des dépendances
en héritent.
Forme simple (inline dans le .ini)
Dans le config.ini du script (~/.config/<mon_script>/config.ini) :
[log]
niveau = INFO
fichier = mon_script.log ; optionnel (sinon console)
format = %%(asctime)s [%%(levelname)s] %%(name)s: %%(message)s
; niveaux par logger (nom seul = niveau global) :
names = automatheque:WARNING, mon_script:DEBUG, requests:ERROR
Dans un
.ini, les%se doublent en%%(convention ConfigParser) ; un%non échappé lève une erreur explicite.
Un seul handler/destination est partagé ; names n'ajuste que des niveaux.
Forme complète (dictConfig externe)
Pour router des loggers vers des destinations différentes (erreurs du script dans un fichier, automatheque ailleurs…), pointer vers un dictConfig complet (JSON ou YAML, détecté au contenu) :
[log]
fichier_config = log.yaml
Voir l'exemple canonique log.yaml.dist.
Release files for automatheque 0.25.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| automatheque-0.25.0.tar.gz | 92.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| automatheque-0.25.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 181.4 kB
Release files / automatheque-0.25.0.tar.gz
| Download URL | automatheque-0.25.0.tar.gz |
|---|---|
| Size | 92.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
80db7d593f8b3c2b94ba151d4775f068e137b67fb8014fbdfbefff49c102de34
|
|
BLAKE2b-256 checksum How to use checksums |
104a2cc81dc03b1b1ed7659b06586136ddbbfd30d47d59881fbe4908c71765e7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency logRelease files / automatheque-0.25.0-py3-none-any.whl
| Download URL | automatheque-0.25.0-py3-none-any.whl |
|---|---|
| Size | 88.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
e41861f86dbdcb332f20822f79db3ce9a7a201352308b98f90a23bcf91c284be
|
|
BLAKE2b-256 checksum How to use checksums |
c4ca932e846dcc59e3b2f6b39f9a3c42daab33978be898a5a2e53d957edf29f8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 18, 2026.
Transparency log