Skip to main content

kliz

CI PyPI - Python Version GitHub issues License: MIT

kliz est un bot d'indexation SEO agnostique. Il permet à une application de notifier plusieurs moteurs de recherche dès qu'une URL est créée ou mise à jour.

Le package ne dépend ni de Django, ni de Celery, ni de Redis. Il expose une API Python synchrone que l'application appelante peut exécuter directement ou encapsuler dans le système de tâches de son choix.

Installation

pip install kliz

Une documentation web statique est disponible dans docs/index.html. Elle peut aussi être publiée via GitHub Pages avec le workflow fourni.

Une traduction anglaise est disponible dans README.en.md.

Pour contribuer et exécuter les tests :

python -m pip install -e ".[dev]"
pytest --cov=kliz

Démarrage rapide

from kliz import GoogleProvider, IndexNowProvider, Kliz

indexer = Kliz(
    [
        IndexNowProvider(
            api_key="votre-cle-indexnow",
            key_location="https://example.com/votre-cle-indexnow.txt",
        ),
        GoogleProvider("/run/secrets/google-service-account.json"),
    ]
)

statuses = indexer.notify_all("https://example.com/articles/nouvel-article")
# {
#     "IndexNowProvider": True,
#     "GoogleProvider": True,
# }

Pour soumettre plusieurs URL d'un coup, notify_many découpe selon max_urls_per_request (lots IndexNow) et retombe sur une boucle notify pour les autres providers :

statuses = indexer.notify_many(
    [
        "https://example.com/articles/a",
        "https://example.com/articles/b",
    ]
)

Le retry intégré est désactivé par défaut (max_attempts=1). Pour l'activer avec backoff exponentiel et jitter :

indexer = Kliz(
    [IndexNowProvider(api_key="votre-cle-indexnow")],
    max_attempts=3,
)

notify_all continue d'appeler les autres fournisseurs lorsqu'un fournisseur échoue. Son statut vaut alors False. Un appel direct à provider.notify(url) laisse en revanche remonter une ProviderError afin que l'application puisse appliquer sa propre politique de retry.

Pour obtenir la cause, le statut HTTP et l'indication de retry :

results = indexer.notify_all_detailed(
    "https://example.com/articles/nouvel-article"
)

for name, result in results.items():
    print(name, result.success, result.retryable, result.error)

Si plusieurs instances ont le même nom, leurs clés sont suffixées : IndexNowProvider, IndexNowProvider#2, etc.

Architecture agnostique

BaseProvider définit une stratégie minimale : notify(url) -> bool. Chaque adaptateur traduit ce contrat vers l'API distante concernée :

  • IndexNowProvider envoie une requête HTTP à l'API IndexNow ;
  • GoogleProvider publie une notification URL_UPDATED via l'API Google Indexing ;
  • Kliz orchestre les stratégies injectées dans son constructeur.

Cette séparation permet d'ajouter un moteur sans modifier l'orchestrateur et laisse l'application libre de choisir son framework web, sa file d'attente et sa politique de retry.

Un fournisseur personnalisé doit uniquement hériter de BaseProvider :

from kliz import BaseProvider


class CustomProvider(BaseProvider):
    def notify(self, url: str) -> bool:
        # Appel vers l'API du moteur concerné
        return True

Pour un moteur qui accepte des lots d'URL sur le même hôte, héritez de BatchProvider : notify et la validation (hôte commun, taille max, URL propres) sont fournis ; il reste à implémenter _notify_many.

from urllib.parse import SplitResult

from kliz import BatchProvider


class CustomBatchProvider(BatchProvider):
    max_urls_per_request = 100

    def _notify_many(
        self, urls: list[str], parsed_urls: list[SplitResult]
    ) -> bool:
        # Appel HTTP groupé vers le moteur
        return True

Validation des URL

Toutes les URL soumises à un provider sont contrôlées avant tout envoi :

  • le schéma doit être http ou https et l'hôte doit être présent ;
  • les identifiants (https://user:pass@...) sont interdits ;
  • les fragments (#...) sont toujours rejetés : ils ne sont jamais transmis au serveur et ne peuvent donc désigner un contenu distinct ;
  • les chaînes de requête (?...) sont rejetées pour les notifications : seule une URL canonique propre est soumise aux moteurs.

La fonction partagée parse_http_url(url, require_clean=True) applique ces règles. require_clean vaut False par défaut afin de ne pas casser les usages existants ; seules les notifications exigent une URL propre.

Configuration des fournisseurs

IndexNow

La clé doit être publiée conformément aux règles d'IndexNow. Si key_location est fourni, il est transmis dans le champ keyLocation.

from kliz import IndexNowProvider

provider = IndexNowProvider(
    api_key="votre-cle-valide",
    key_location="https://example.com/votre-cle-valide.txt",  # optionnel
    timeout=10.0,
)
provider.notify("https://example.com/page")

Le provider réutilise une connexion HTTP persistante (requests.Session) entre les notifications, afin de ne pas reconstruire une connexion et une poignée de main TLS à chaque appel. Vous pouvez injecter votre propre session (tests, configuration réseau partagée, proxies) :

import requests

provider = IndexNowProvider(
    api_key="votre-cle-valide",
    session=requests.Session(),
)

La session interne garde les connexions ouvertes ; appelez provider.close() à l'arrêt de votre application pour les libérer proprement.

Pour soumettre plusieurs URL du même hôte dans un seul appel :

provider.notify_many(
    [
        "https://example.com/page-1",
        "https://example.com/page-2",
    ]
)

IndexNow accepte jusqu'à 10 000 URL par requête. kliz classe les erreurs 429 et 5xx comme retentables.

Google

Activez l'API Google Indexing pour votre projet, créez un compte de service et autorisez-le sur la propriété concernée. Ne versionnez jamais le fichier JSON du compte de service.

Restriction importante : l'API Google Indexing est officiellement réservée aux pages contenant un JobPosting ou un BroadcastEvent intégré dans un VideoObject. N'utilisez pas ce provider comme API d'indexation générique pour les autres contenus ; utilisez notamment un sitemap pour leur couverture.

from kliz import GoogleProvider

provider = GoogleProvider(
    "/run/secrets/google-service-account.json",
    timeout=60.0,
    num_retries=2,
)
provider.notify("https://example.com/jobs/backend-python")

L'API Google Indexing est soumise aux règles d'éligibilité et aux quotas de Google. Une notification ne garantit pas l'indexation de l'URL.

Le client Indexing est construit de manière paresseuse : le fichier de compte de service n'est lu qu'au premier appel de notify, puis réutilisé pour les appels suivants. La création du provider ne déclenche donc aucune lecture de fichier. Les erreurs de configuration (fichier absent, JSON invalide) remontent au moment de la notification, sont marquées comme non retentables, et le provider se rétablit dès que le fichier est corrigé.

Recettes / Intégration Asynchrone

kliz reste volontairement synchrone. Pour une exécution asynchrone, placez l'appel dans un worker, une tâche ou un job appartenant à votre application. Ainsi, les dépendances d'infrastructure ne contaminent pas le package.

Tâche Celery (Python/Django)

Dans un projet Django utilisant déjà Celery, la tâche peut lire sa configuration depuis les settings et laisser Celery gérer les retries :

# myapp/tasks.py — ce code appartient à l'application, pas à kliz
from dataclasses import asdict

from celery import shared_task
from django.conf import settings

from kliz import IndexNowProvider, Kliz


@shared_task(bind=True, max_retries=5)
def notify_search_engines(self, url: str) -> dict[str, dict[str, object]]:
    indexer = Kliz(
        [
            IndexNowProvider(
                api_key=settings.INDEXNOW_API_KEY,
                key_location=settings.INDEXNOW_KEY_LOCATION,
            ),
        ]
    )
    results = indexer.notify_all_detailed(url)
    retryable = [result for result in results.values() if result.retryable]

    if retryable:
        raise self.retry(
            exc=RuntimeError("temporary indexing provider failure"),
            countdown=min(60 * (2**self.request.retries), 3600),
        )

    return {name: asdict(result) for name, result in results.items()}

Depuis une vue, un signal ou un service Django :

from myapp.tasks import notify_search_engines

notify_search_engines.delay("https://example.com/articles/nouveau")

Pour isoler les retries et quotas de chaque moteur, utilisez idéalement une tâche par provider. Le provider Google ne doit être ajouté que pour les pages officiellement éligibles.

Job générique

Le même principe fonctionne avec un scheduler, un worker maison, RQ, Dramatiq, une fonction serverless ou un cron. Le job ne connaît que l'API publique de kliz :

from kliz import IndexNowProvider, Kliz


class ContentIndexingJob:
    def __init__(self, api_key: str) -> None:
        self.indexer = Kliz([IndexNowProvider(api_key=api_key)])

    def run(self, payload: dict[str, str]) -> dict[str, bool]:
        return self.indexer.notify_all(payload["url"])


# Le système de jobs choisi sérialise ce payload et appelle job.run(payload).
job = ContentIndexingJob(api_key="votre-cle")
result = job.run({"url": "https://example.com/page-modifiee"})

Interface en ligne de commande

L'installation fournit aussi une commande kliz :

export KLIZ_INDEXNOW_API_KEY="votre-cle"
export KLIZ_INDEXNOW_KEY_LOCATION="https://example.com/votre-cle.txt"

kliz notify https://example.com/page  # une URL
kliz notify --batch urls.txt          # une URL par ligne, `#` pour un commentaire
kliz providers                        # liste des providers configurés
kliz --version

Les crédits se passent aussi en options (--indexnow-api-key, --indexnow-key-location, --google-service-account-file). Le processus termine avec le code 0 si tout a réussi, 1 en cas d'échec de notification et 2 en cas de configuration invalide.

Tests

Les tests mockent les appels requests et le client Google. Ils ne nécessitent donc ni accès réseau, ni clé IndexNow, ni compte de service Google.

La validation complète locale est :

ruff format --check src tests
ruff check src tests
mypy src
pytest --cov=kliz
python -m build
twine check --strict dist/*
pip-audit . --strict

Exploitation en production

Le package ne stocke aucun secret et n'impose aucun système de tâches. Dans l'application qui l'utilise :

  • injectez les clés par un gestionnaire de secrets ;
  • activez le retry opt-in de Kliz (max_attempts) ou appliquez un backoff applicatif aux résultats retryable=True ;
  • placez les échecs définitifs dans une dead-letter queue ;
  • mesurez latence, taux de succès, codes HTTP et quotas par provider ;
  • ne partagez pas une même instance GoogleProvider entre plusieurs threads ;
  • conservez un sitemap à jour : une notification ne garantit jamais l'indexation.

Publication

Les tags vX.Y.Z déclenchent le workflow de release. Le tag doit correspondre exactement à la version de pyproject.toml. La publication utilise le Trusted Publishing PyPI et ne nécessite aucun token PyPI permanent dans GitHub.

Avant la première release, configurez sur PyPI un publisher avec le dépôt freddychoudja/kliz-, le workflow release.yml et l'environnement pypi.

Contribuer

Les contributions sont les bienvenues. Consultez CONTRIBUTING.md avant d'ouvrir une issue ou une pull request.

Le code source et le suivi du projet sont disponibles sur GitHub.

Licence

kliz est distribué sous la licence MIT. Copyright © 2026 Freddy Choudja.

Metadata

Release files for kliz 0.2.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 kliz 0.2.0
File Size Uploaded
kliz-0.2.0.tar.gz 27.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for kliz 0.2.0
File Interpreter ABI Platform
kliz-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 46.2 kB

Release files / kliz-0.2.0.tar.gz

Download URL kliz-0.2.0.tar.gz
Size 27.1 kB
Tags Source
SHA-256 checksum
How to use checksums
c2a988613619570391a6efb3a86b303ef5245b133380b561bf29d80d189303e4
BLAKE2b-256 checksum
How to use checksums
60750b3fc6c596db572f075e4d38408b43608af620b3f81b9e2319f8b00a3229
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 29, 2026.

Transparency log

Release files / kliz-0.2.0-py3-none-any.whl

Download URL kliz-0.2.0-py3-none-any.whl
Size 19.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
acd6bd5e06c47cbda9f63d293b5caee3f23f31994d1568bfc0739505fffb7eec
BLAKE2b-256 checksum
How to use checksums
f9f7402de9a79854298ef38d7c2d0b0c1f437232666213c0005319d19a5ad6ed
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 29, 2026.

Transparency log

Release history Release notifications | RSS feed

0.3.1

2 release files

0.3.0

2 release files

This release

0.2.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