Skip to main content

loom-notes

Une mémoire locale pour Claude Desktop et les agents IA. Serveur MCP + RAG hybride (BGE-M3, reranking, Qdrant). L'utilisateur y met ce qu'il veut, Claude vient y chercher. Tout tourne en local, sans cloud ni clé API.

Pourquoi

La mémoire automatique des assistants enregistre ce qu'elle croit avoir compris : des demi-vérités, des choses périmées, des inférences. Ici, rien n'entre sans que l'utilisateur l'ait décidé. Il ajoute un texte, une page web ou un fichier markdown ; Claude interroge cette base avant de répondre sur ses projets, ses décisions ou ses notes, et n'écrit dedans que si l'utilisateur le lui demande explicitement dans le message courant.

Le contenu vit dans un dossier local (~/.local/share/loom-notes par défaut). Les modèles d'embedding et de reranking tournent en local. Le réseau ne sert qu'à deux choses : télécharger les modèles au premier usage, et télécharger une page quand l'utilisateur demande de la mémoriser.

Le même serveur sert Claude Desktop (chat et Cowork) et tout agent qui parle MCP : les tools sont les mêmes.

Ce que Claude peut faire

Neuf tools, séparés en deux familles.

Tool Rôle
search(query, project?, tags?, k) Recherche hybride puis reranking. Sans project, la recherche est globale et chaque résultat indique son projet. k est plafonné à 10.
get(doc_id) Texte intégral d'un document et ses métadonnées.
list_docs(project?, n) Derniers documents ajoutés, du plus récent au plus ancien, sans le texte.
projects() Projets présents et nombre de documents pour chacun.
add_text(text, title, project, tags?) Ajoute un texte brut.
add_url(url, project, tags?) Télécharge une page, en extrait le contenu et l'ajoute. Le titre est celui de la page.
add_file(path, project, tags?) Ajoute un fichier markdown local, découpé par titres. Soumis au contrôle d'accès décrit plus bas.
update(doc_id, text) Remplace le texte d'un document. Même identifiant, titre, projet et tags conservés.
delete(doc_id) Supprime définitivement un document et ses chunks.

Les quatre premiers sont déclarés en lecture seule au sens MCP. Les cinq autres portent, dans leur description et dans les instructions du serveur, la même consigne : n'appeler que sur demande explicite de l'utilisateur, jamais de sa propre initiative. C'est une consigne au modèle, pas un contrôle technique. Le seul contrôle technique porte sur add_file.

Chaque tool d'écriture renvoie ce qu'il a réellement fait : identifiant, titre, projet, nombre de chunks écrits, et le cas échéant duplicate_of (contenu identique à un document existant, rien n'a été écrit) ou updated (un document existant a été remplacé).

search renvoie le chunk entier quand il fait moins de 1 000 caractères, ce qui couvre la plupart des notes et évite de couper un tableau au milieu. Au delà, un extrait de 300 caractères et truncated: true, signal pour appeler get.

Comment ça marche

Ingestion

Les trois entrées convergent vers le même pipeline. Le texte brut est pris tel quel. Une page web est convertie en markdown structuré : titres, listes, tableaux et blocs de code sont conservés, la navigation, le pied de page, les scripts et les formulaires retirés. Un fichier markdown est lu tel quel, son titre est le premier # H1 ou, à défaut, le nom du fichier.

Le texte est ensuite découpé par sections, en suivant les titres markdown. Une section trop longue est retaillée en paragraphes à environ 1 800 caractères avec un chevauchement de 180. Chaque chunk est préfixé par son chemin de titres (Titre du document > Section > Sous-section) avant d'être vectorisé : un paragraphe isolé perd son contexte, et c'est le détail qui fait le plus de différence en rappel sur des notes.

Une empreinte SHA-256 du texte normalisé (casse, espaces) sert de déduplication. Un contenu déjà présent n'est pas réécrit. Une source déjà connue, URL ou chemin de fichier, dont le contenu a changé remplace le document existant au lieu d'en créer un second.

BGE-M3 produit en un seul passage un vecteur dense (1 024 dimensions) et un vecteur sparse lexical par chunk.

Retrieval

La requête est vectorisée de la même façon. Qdrant exécute deux recherches, dense et sparse, avec les filtres de projet et de tags appliqués dans chacune, puis fusionne les deux listes par RRF. Les candidats passent ensuite dans le cross-encoder bge-reranker-v2-m3, qui note chaque couple question/chunk entre 0 et 1. Les résultats sont regroupés à deux chunks maximum par document, filtrés par un score minimal, puis tronqués à k.

Le score renvoyé est celui du reranker. Au dessus de 0,8, le passage répond directement à la question ; entre 0,3 et 0,8, il traite le sujet sans y répondre franchement ; en dessous de 0,1, c'est du bruit ramené par le vecteur faute de mieux. Le seuil par défaut est 0,1 ; la section Évaluation explique comment le régler.

Stockage

Deux collections Qdrant. documents contient le texte intégral et les métadonnées, sans vecteur : c'est la source de vérité. memory contient les chunks vectorisés et se reconstruit entièrement depuis documents avec loom-notes reindex. Les identifiants de chunk sont dérivés de l'identifiant du document et de l'index du chunk, donc une réécriture ne laisse pas d'orphelin.

Le nom du modèle d'embedding est enregistré dans DATA_DIR/meta.json au moment de l'indexation. Si la configuration demande un autre modèle, le serveur refuse de démarrer et demande un reindex : deux modèles ne se mélangent jamais en silence dans le même index.

Installation

Linux, Python 3.12 ou plus récent, et une machine capable de faire tourner deux modèles de 570 M de paramètres. Les modèles sont dans l'extra models : sans lui, le serveur démarre, mais la première recherche échoue en demandant de l'installer.

Avec uv, en outil isolé :

uv tool install "loom-notes[models]"

Les commandes loom-notes et loom-notes-mcp arrivent dans ~/.local/bin. Avec pip, dans un environnement virtuel : pip install "loom-notes[models]".

Sous Linux, la roue torch de PyPI embarque CUDA et pèse plusieurs Go. Sur une machine sans GPU NVIDIA, la version CPU, bien plus légère, s'installe d'abord depuis l'index de PyTorch, dans le même environnement :

pip install torch --index-url https://download.pytorch.org/whl/cpu
pip install "loom-notes[models]"

Avec uv, le même enchaînement fonctionne dans un environnement créé par uv venv, avec uv pip install à la place de pip install.

Les poids de BGE-M3 (environ 3 Go) et du reranker (environ 2,3 Go) sont téléchargés depuis Hugging Face au premier usage et mis en cache dans ~/.cache/huggingface (HF_HOME pour le déplacer). Il n'y a rien d'autre à télécharger ensuite.

Sur GPU, les deux modèles tiennent dans 3 Go de VRAM en fp16. Le périphérique est choisi au chargement : cuda, sinon mps, sinon CPU ; LOOM_NOTES_DEVICE l'impose, et le fp16 n'est activé que sur cuda. Sur CPU, le reranker devient le poste dominant : comptez une dizaine de secondes par recherche. Une carte Pascal ou plus ancienne (compute capability inférieure à 7.5) n'est pas prise en charge par les roues torch CUDA 13 publiées sur PyPI ; il faut alors soit le CPU, soit une roue CUDA 12.6.

Le pinning transformers<5 n'est pas un oubli : FlagEmbedding 1.4 casse avec transformers 5 (tokenizer.pad reçoit une liste au lieu d'un dictionnaire).

Qdrant

Sans LOOM_NOTES_QDRANT_URL, Qdrant tourne embarqué dans DATA_DIR/qdrant (QdrantClient(path=…)) : rien à installer, mais un seul processus à la fois. Ce mode suffit pour la CLI et les tests. Il ne convient pas à Claude Desktop, qui lance deux instances de chaque serveur MCP, une pour le chat et une pour les sessions Cowork et Code : la seconde mourrait à l'ouverture. Pour Claude Desktop, il faut un Qdrant en conteneur :

mkdir -p /home/[user]/.local/share/qdrant
docker run -d --name qdrant --restart unless-stopped \
  --user "$(id -u):$(id -g)" \
  -e QDRANT__STORAGE__SNAPSHOTS_PATH=/qdrant/storage/snapshots \
  -p 127.0.0.1:6333:6333 \
  -v /home/[user]/.local/share/qdrant:/qdrant/storage \
  qdrant/qdrant
curl -s localhost:6333/

Le dossier du volume est créé avant le conteneur : sinon Docker le crée au nom de root, et Qdrant, lancé sous l'utilisateur, ne peut pas y écrire. Il est distinct de DATA_DIR/qdrant, le chemin du mode embarqué.

--user fait que les fichiers du volume appartiennent à l'utilisateur et non à root ; la variable QDRANT__STORAGE__SNAPSHOTS_PATH est nécessaire dans ce cas, sinon Qdrant ne peut pas écrire son dossier de snapshots dans l'image. Le port n'est exposé que sur l'interface locale.

--restart unless-stopped relance le conteneur avec le service Docker au démarrage de la machine, à condition que ce service soit activé (systemctl is-enabled docker, sinon sudo systemctl enable docker). Un docker stop qdrant manuel le laisse arrêté jusqu'au prochain docker start qdrant.

Les deux modes n'ont pas le même format sur disque, on ne passe pas de l'un à l'autre sans réingérer (export puis import).

Branchement dans Claude Desktop

~/.config/Claude/claude_desktop_config.json :

{
  "mcpServers": {
    "loom-notes": {
      "command": "/home/[user]/.local/bin/loom-notes-mcp",
      "args": [],
      "env": {
        "FASTMCP_SHOW_SERVER_BANNER": "false",
        "FASTMCP_CHECK_FOR_UPDATES": "off",
        "LOOM_NOTES_QDRANT_URL": "http://127.0.0.1:6333",
        "LOOM_NOTES_ALLOWED_ROOTS": "/home/[user]/dev"
      }
    }
  }
}

command est le chemin absolu du binaire installé, que donne which loom-notes-mcp. Pas de uvx ni de uv run ici : au premier lancement, ils résoudraient et téléchargeraient les dépendances, torch compris, entre l'exec() et le handshake MCP, sans rien écrire sur la sortie standard pendant ce temps, et le client attend. Fermer complètement l'application avant d'éditer ce fichier, elle le réécrit à la fermeture.

Au démarrage, le serveur répond au handshake tout de suite et charge les modèles en tâche de fond, après les avoir téléchargés la toute première fois. La première recherche peut attendre la fin de ce chargement, les suivantes non.

Données

Tout ce que loom-notes écrit sur le disque est dans DATA_DIR : $XDG_DATA_HOME/loom-notes, sinon ~/.local/share/loom-notes, ou le dossier donné par LOOM_NOTES_DATA_DIR.

Fichier Contenu
meta.json Modèle d'embedding avec lequel l'index a été construit.
qdrant/ La base, en mode embarqué seulement.
export.jsonl Sauvegarde écrite par loom-notes export.
golden.jsonl Jeu doré de loom-notes eval.

En mode serveur, la base est dans le volume du conteneur Qdrant ; DATA_DIR ne garde que les trois fichiers. Le serveur et la CLI doivent voir le même DATA_DIR et le même QDRANT_URL.

Configuration

Toutes les options sont des variables d'environnement préfixées LOOM_NOTES_ : celles du bloc env de Claude Desktop pour le serveur, celles du shell pour la CLI. Aucun fichier .env n'est lu. Les valeurs ci-dessous sont les défauts.

Variable Défaut Rôle
DATA_DIR $XDG_DATA_HOME/loom-notes, sinon ~/.local/share/loom-notes Répertoire des données : Qdrant embarqué, meta.json, export, jeu doré.
QDRANT_URL vide URL d'un serveur Qdrant. Vide : mode embarqué dans DATA_DIR/qdrant.
QDRANT_API_KEY vide Clé d'API si le serveur en exige une.
DEVICE auto Périphérique torch. auto : cuda, sinon mps, sinon cpu. Le fp16 n'est activé que sur cuda.
DENSE_MODEL BAAI/bge-m3 Modèle d'embedding. En changer impose un reindex.
RERANKER_MODEL BAAI/bge-reranker-v2-m3 Cross-encoder de reranking.
ALLOWED_ROOTS vide Racines lisibles par add_file, séparées par :. Vide : add_file refusé.
DENY_PATTERNS vide Motifs refusés en plus de la liste de base, séparés par :.
CHUNK_TARGET_CHARS 1800 Taille visée d'un chunk.
CHUNK_MAX_CHARS 2400 Taille maximale d'une section avant retaille.
CHUNK_OVERLAP_CHARS 180 Chevauchement entre chunks consécutifs d'une même section.
CHUNK_MIN_CHARS 200 En dessous, une section est fusionnée avec la précédente.
PREFETCH_LIMIT 15 Candidats par branche (dense, sparse) avant fusion.
RERANK_CANDIDATES 20 Candidats passés au reranker.
MAX_CHUNKS_PER_DOC 2 Chunks d'un même document dans les résultats.
FULL_CHUNK_CHARS 1000 En dessous, search renvoie le chunk entier.
SNIPPET_CHARS 300 Longueur de l'extrait au delà.
MIN_SCORE 0.1 Score reranker minimal d'un résultat.
WARMUP_ON_START true Préchargement des modèles au démarrage du serveur.
FAKE_MODELS false Modèles factices, pour les tests ou une démo sans GPU.
FETCH_TIMEOUT_S 20 Délai de téléchargement pour add_url.
FETCH_MAX_BYTES 5000000 Taille maximale d'une page téléchargée par add_url, après décompression.
FETCH_ALLOW_PRIVATE false true autorise add_url vers la machine et le réseau local.

Sécurité de add_file

Un serveur MCP tourne avec les droits de l'utilisateur qui lance Claude Desktop, et Claude Desktop ne lui applique aucune restriction de dossier. Sans garde-fou, add_file suivi de get est une lecture de fichier arbitraire, y compris depuis une session Cowork dont les dossiers autorisés sont bien plus étroits. Ce n'est pas théorique, c'est le test qui a motivé ce paragraphe.

Trois contrôles s'appliquent, dans cet ordre, et le message d'erreur nomme celui qui a mordu.

Le chemin résolu, symlinks compris, doit être inclus dans une des racines de ALLOWED_ROOTS, elles-mêmes résolues. Un lien qui sort d'une racine est refusé. Sans racine configurée, add_file est refusé d'office.

Aucun composant du chemin relatif à la racine ne doit correspondre à un motif refusé. La liste de base est dans le code et ne peut pas être retirée par configuration :

.env  .env.*  *.pem  *.key  *.p12  *.pfx
id_rsa*  id_ecdsa*  id_ed25519*
.ssh  .aws  .gnupg  .netrc  .npmrc  .pypirc
.git  .venv  node_modules  __pycache__

DENY_PATTERNS peut seulement y ajouter. Une garantie qu'un fichier de configuration peut désactiver n'en est plus une.

L'extension doit être .md, .markdown ou .txt.

Ce filtrage porte sur des noms. Un secret écrit en clair dans une note markdown d'une racine autorisée sera lu. La frontière, c'est le choix des racines ; le reste est de la défense en profondeur. add_url a ses propres contrôles, décrits ci-dessous, et add_text ne lit rien.

Sécurité de add_url

add_url est la seule sortie réseau que le modèle peut déclencher, et une page mémorisée peut contenir une injection. Deux contrôles s'appliquent à chaque requête, redirections comprises.

L'hôte doit être public. Une adresse IP écrite dans l'URL, ou chacune des adresses renvoyées par le DNS, est vérifiée : boucle locale, réseaux privés, lien local, adresses réservées et multicast sont refusés. Sans ce contrôle, une page pourrait faire lire au serveur http://127.0.0.1:6333/collections (l'API de Qdrant) ou l'interface d'un équipement du réseau local, et la ranger en mémoire. FETCH_ALLOW_PRIVATE=true lève ce contrôle, pour mémoriser la page d'un service interne.

La page ne doit pas dépasser FETCH_MAX_BYTES (5 Mo par défaut), mesurés après décompression : une réponse annoncée plus grosse est refusée avant le téléchargement, une réponse qui dépasse en cours de route est coupée.

Seules les URL http et https sont acceptées, et seul le contenu HTML ou XML.

Ligne de commande

loom-notes expose les mêmes opérations que le serveur, plus la maintenance. En mode serveur Qdrant, la CLI fonctionne pendant que Claude Desktop tourne. Elle lit les mêmes variables que le serveur, à exporter dans le shell : au minimum LOOM_NOTES_QDRANT_URL, et LOOM_NOTES_DATA_DIR si le dossier de données n'est pas celui par défaut.

loom-notes add-text PROJET TITRE [TEXTE]      texte brut ; lu sur stdin si absent
loom-notes add-url PROJET URL
loom-notes add-file PROJET CHEMIN
loom-notes search "question" [-p projet] [-t tag] [-k 5]
loom-notes get DOC_ID
loom-notes list [-p projet] [-n 20]
loom-notes projects
loom-notes delete DOC_ID
loom-notes export [FICHIER]                    sauvegarde JSONL, défaut DATA_DIR/export.jsonl
loom-notes import [FICHIER]                    réimport ; les doc_id déjà présents sont ignorés
loom-notes reindex                             reconstruit les chunks depuis les documents
loom-notes eval [FICHIER] [--json]             évalue le retrieval sur le jeu doré
loom-notes eval-add "question" DOC_ID [-p projet]

Les commandes d'ajout acceptent -t plusieurs fois pour les tags. Les options globales --fake (modèles factices) et --data-dir se placent avant la commande.

Évaluation

DATA_DIR/golden.jsonl est un jeu doré : une ligne par cas, avec la question telle que l'utilisateur la poserait et l'identifiant du document qui doit sortir.

{"query":"comment purger le journal d'audit sans arrêter le serveur","doc_id":"f3c9d186-…","title":"loom-fs — serveur MCP filesystem à rôles"}

loom-notes eval rejoue chaque question à seuil zéro et donne, par cas, le rang du document attendu et son score, puis recall@1, recall@5 et MRR, et enfin le plus haut seuil qui ne fait perdre aucun cas trouvé, avec la part des résultats hors document attendu qui tomberaient sous ce seuil. C'est la valeur à mettre dans MIN_SCORE.

Le jeu s'enrichit au fil de l'eau : deux questions par document ajouté, avec eval-add. Un cas qui sort avec un score faible alors que le document contient la réponse signale presque toujours un document mal rédigé, typiquement une commande sans la phrase qui dit quand et pourquoi l'utiliser. Sur les premiers cas, la même question passait de 0,91 à 0,11 selon que le document gardait ou non ses deux phrases de contexte.

Un changement de chunking, de modèle, de seuil ou de fusion se valide par un eval avant et après.

Sauvegarde et réindexation

loom-notes export écrit un document par ligne dans DATA_DIR/export.jsonl, texte intégral et métadonnées compris. C'est la sauvegarde de référence, indépendante de Qdrant et du modèle d'embedding.

loom-notes import recharge un export dans une base vide ou partielle, en réindexant chaque document et en ignorant les identifiants déjà présents. Changer de modèle d'embedding ou de mode Qdrant revient à un export, un changement de configuration, puis un import ou un reindex.

Dépannage

Le serveur affiche Échec dans Claude Desktop avec « Connection closed ». Lire les journaux depuis les paramètres de l'application. La première ligne d'erreur du serveur dit ce qui manque. Les deux causes habituelles : Qdrant injoignable (« Qdrant injoignable sur http://127.0.0.1:6333 … docker start qdrant »), ou un command qui ne désigne pas le binaire : y mettre le chemin absolu donné par which loom-notes-mcp.

Le serveur refuse de démarrer en parlant de modèle. DATA_DIR/meta.json enregistre le modèle avec lequel l'index a été construit et il diffère de DENSE_MODEL. loom-notes reindex reconstruit l'index avec le modèle courant.

add_file est refusé. Le message nomme la règle : hors des racines autorisées, motif refusé, extension. Sans ALLOWED_ROOTS, tout est refusé.

add_url est refusé. « adresse non publique refusée » : la page est sur la machine ou le réseau local ; LOOM_NOTES_FETCH_ALLOW_PRIVATE=true si c'est voulu. « page trop volumineuse » : relever LOOM_NOTES_FETCH_MAX_BYTES.

Une recherche prend dix secondes. Le reranker tourne sur CPU. Vérifier LOOM_NOTES_DEVICE et que torch voit bien la carte (python -c "import torch; print(torch.cuda.is_available())" avec le Python de l'installation, ~/.local/share/uv/tools/loom-notes/bin/python pour une installation par uv tool).

IndexError: list index out of range dans FlagEmbedding au premier appel. Le GPU est vu par torch mais sans kernels compatibles (carte trop ancienne pour la roue CUDA installée). FlagEmbedding attrape l'erreur CUDA, réduit son batch jusqu'à zéro et plante sur une liste vide. Passer en cpu ou installer une roue torch adaptée à la carte.

Limites connues

Le contrôle « écriture sur demande explicite » est une consigne au modèle. Une page ajoutée par add_url peut contenir une injection ; elle n'aura aucun pouvoir sur le disque grâce au confinement de add_file, mais elle pourrait pousser le modèle à écrire dans la mémoire. Les tools d'écriture renvoient toujours ce qu'ils ont fait, c'est à l'utilisateur de le lire.

Le contrôle d'adresse de add_url résout le nom, puis httpx se connecte : un DNS qui change de réponse entre les deux (DNS rebinding) passerait au travers. Il protège des liens et des redirections vers le réseau local, pas d'un serveur DNS hostile.

Les tailles de chunk sont en caractères, avec l'approximation de quatre caractères par token en français, pas en tokens du modèle.

list_docs et projects parcourent toute la collection documents. Linéaire, invisible jusqu'à quelques milliers de documents.

Le mode serveur Qdrant ne chiffre rien et n'authentifie personne par défaut. Le port est lié à l'interface locale ; ne pas l'exposer sans clé d'API.

Il n'y a pas de quota. Un agent en boucle peut lancer autant de recherches qu'il veut ; chacune coûte du temps de reranking, pas d'argent.

Développement

git clone https://github.com/denislamard/loom-notes.git
cd loom-notes
uv sync

uv sync installe aussi l'extra models (FlagEmbedding, torch), par le groupe du même nom. Les tests n'en ont pas besoin : uv sync --no-group models s'en passe.

uv run ruff check src tests
uv run ruff format --check src tests
uv run pyright
uv run pytest

Les tests tournent sans GPU et sans téléchargement : les modèles sont remplacés par des factices déterministes (dense par sac de mots haché, sparse par comptage) suffisants pour vérifier le filtrage, la déduplication, le remplacement par source, l'export et l'import, le confinement de add_file, les contrôles de add_url et le comportement des tools à travers un client MCP en mémoire. Pyright est en mode strict.

La CI (.github/workflows/ci.yml) lance ces contrôles sur Python 3.12, 3.13 et 3.14, puis construit le paquet et vérifie son contenu. .github/workflows/publish.yml publie : à la main vers TestPyPI, et vers PyPI à chaque étiquette v* dont le numéro est celui du pyproject.toml.

Licence

Apache 2.0.

Metadata

Release files for loom-notes 1.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for loom-notes 1.1.0
File Size Uploaded
loom_notes-1.1.0.tar.gz 45.5 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for loom-notes 1.1.0
File Interpreter ABI Platform
loom_notes-1.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 91.2 kB

Release files / loom_notes-1.1.0.tar.gz

Download URL loom_notes-1.1.0.tar.gz
Size 45.5 kB
Tags Source
SHA-256 checksum
How to use checksums
e6613cde2eb513ccb205d4327dc6dea5cde2745e04c980d63e88efa58cf27082
BLAKE2b-256 checksum
How to use checksums
0036e66c4a659653ecd4be41abaec9e58a08244c29356b7d09a69c67abf5ce1e
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 Oct 8, 2026.

Transparency log

Release files / loom_notes-1.1.0-py3-none-any.whl

Download URL loom_notes-1.1.0-py3-none-any.whl
Size 45.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
e1bd6085f028411ebe7bf622117100a2c2b71be6cb4fe98ee7fec3fe73b7de83
BLAKE2b-256 checksum
How to use checksums
c7778edd45cd10d32ba386d100193c096c63e96a5243360b310e1eec646cf9b2
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 Oct 8, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page