libs.zazac.link
Libs/capy

capy v0.5.4

Le réseau facile en Python, chiffré et sans dépendance

installer
python3 -m pip install --user capyy
Télécharger .tar.gz

import capy · PyPI : capyy · Python 3.9+ · aucune dépendance

Présentation

Le réseau facile en Python, chiffré et sans dépendance. Tes programmes se parlent sur la même machine, sur le wifi ou à travers internet ; ta maison s'allume depuis ton code ; tes projets se lancent avec une commande.

Au cœur de capy, une idée simple : un serveur propose des actions (des fonctions Python), et des clients les appellent par leur nom, avec des paramètres. Pas de HTTP, pas d'URL, pas de framework : un décorateur côté serveur, une ligne côté client. Tout le reste est construit là-dessus.

En bref#

PartieSert àEn une ligne
capyune API chiffrée entre deux programmes (même machine ou IP connue)capy.call("localhost", 4000, "hello")
capy.wifise trouver par un nom sur le même wifi, sans connaître l'IPcapy.wifi.call("maison", "hello")
capy.networkse parler à travers internet, même derrière une box, via un relaiscapy.network.call("maison", "hello")
capy.domopiloter les lampes et prises (Hue, WiZ, Shelly, Yeelight, Home Assistant)home["salon"].color("orange")
capy.config.json + cydécrire un projet une fois, et le lancer comme avec npmcy start, cy test

Les trois façons de se parler (capy, capy.wifi, capy.network) marchent pareil : @api.action("nom") côté serveur, call(..., "nom", param=valeur) côté client. Avec un [fichier capy.config.json](#fichier-de-projet-capy-config-json), le choix entre les trois se fait dans le fichier, pas dans le code.

Installer#

terminal
python3 -m pip install --user capyy

Le paquet s'appelle capyy, avec deux y

capy était déjà pris sur PyPI. On installe capyy, mais on écrit toujours import capy dans le code.

Démarrage en 2 minutes#

  1. Crée un fichier serveur.py avec une action hello.
  2. Lance-le dans un terminal : il affiche son port et son empreinte (son identité chiffrée).
  3. Dans un autre terminal, lance client.py : il appelle hello et affiche la réponse.
serveur.py
import capy

server = capy.Server(4000)

@server.action("hello")
def hello(nom="monde"):
    return {"message": f"Bonjour {nom} !"}

server.run()
résultat
API en écoute sur le port 4000, chiffrée (TLS) · empreinte : 9dfa7b98c1e0…
client.py
import capy

try:
    reponse = capy.call("localhost", 4000, "hello", nom="Izac")
except ConnectionError as e:
    exit(str(e))

print(reponse)                          # tout le dict
print(capy.get(reponse, "message"))     # juste le message
résultat
{'message': 'Bonjour Izac !'}
Bonjour Izac !

Et côté serveur, chaque requête s'affiche au passage :

résultat
127.0.0.1 → {"action": "hello", "nom": "Izac"}

Arrêter le serveur

Ctrl+C dans son terminal. Et **après chaque modification de serveur.py, relance-le** : Python ne relit pas le fichier tout seul, l'ancien code continue de tourner tant que tu ne l'arrêtes pas.

Comment ça marche#

À chaque capy.call(...), le client :

  1. ouvre une connexion TCP chiffrée vers le serveur ;
  2. envoie une ligne de JSON : le nom de l'action + les paramètres ;
  3. lit une ligne de JSON en réponse, et la renvoie en objet Python (dict, liste…) ;
  4. ferme la connexion.

Ce qui passe vraiment dans le tuyau (chiffré, donc illisible pour les autres) :

échanges
→ {"action": "hello", "nom": "Izac"}
← {"message": "Bonjour Izac !"}

Côté serveur, chaque client est servi dans son propre thread : plusieurs clients peuvent appeler en même temps, une action lente ne bloque pas les autres.

Combien ça coûte ?

Environ 3,5 ms par appel en local avec le chiffrement, 0,4 ms sans (tls=False). Largement assez pour un jeu, un tableau de bord ou une petite appli : capy n'est pas fait pour des milliers d'appels par seconde.

Écrire des actions#

Une action, c'est une fonction normale avec un décorateur. Ses paramètres sont ceux que le client envoie, et ce qu'elle renvoie part au client.

python
@server.action("addition")
def addition(a, b):
    return {"resultat": a + b}

@server.action("saluer")
def saluer(nom, poli=True):          # poli a une valeur par défaut : le client peut l'oublier
    return {"texte": f"Bonjour {nom}" if poli else f"Salut {nom}"}

Ce qu'une action peut renvoyer#

Tout ce qui existe en JSON : un dict (le plus pratique), une liste, un texte, un nombre, True / False, None.

python
return {"nom": "Zoe", "age": 14}          # dict
return [1, 2, 3]                           # liste
return "ok"                                # texte
return capy.json('{"text": "Hello"}')      # du JSON déjà écrit en texte -> le client reçoit un dict

print() n'envoie rien au client

print(...) dans une action affiche dans le terminal du serveur. Le client, lui, reçoit None. Pour envoyer quelque chose, c'est toujours return.

Pas de set, de date ou d'objet

Un set, un datetime ou un objet à toi n'existent pas en JSON. Le client reçoit alors {"erreur": "réponse pas en JSON : Object of type set is not JSON serializable"}. Convertis avant : list(mon_set), ma_date.isoformat(), vars(mon_objet).

Les erreurs dans une action#

Si ton action lève une exception, capy l'attrape et le client reçoit {"erreur": "le message"} : le serveur ne plante pas. Tu peux donc t'en servir exprès pour refuser une demande :

python
@server.action("diviser")
def diviser(a, b):
    if b == 0:
        raise ValueError("division par zéro impossible")
    return {"resultat": a / b}
résultat
{'erreur': 'division par zéro impossible'}

Les autres erreurs arrivent pareil, toutes seules :

Le client…Il reçoit
appelle une action qui n'existe pas{"erreur": "action inconnue"}
oublie un paramètre obligatoire{"erreur": "addition() missing 1 required positional argument: 'b'"}
envoie un paramètre en trop{"erreur": "addition() got an unexpected keyword argument 'c'"}
envoie autre chose que du JSON{"erreur": "JSON invalide"}

Moins de 3 secondes

Le client attend la réponse 3 secondes au maximum. Au-delà, il abandonne avec ConnectionError: … n'a pas répondu à temps (3 s) : l'action est sans doute trop lente. Pour un long calcul, lance-le dans un thread et propose une 2e action qui donne le résultat quand il est prêt.

Plusieurs clients en même temps : protège ce qui est partagé

Chaque appel tourne dans son thread. Si deux appels modifient la même variable ou le même fichier en même temps, l'un peut écraser l'autre. Un threading.Lock() règle ça (voir l'exemple « Un compteur partagé »).

Appeler une action#

python
capy.call(hote, port, action, param1=..., param2=...)
  • hote : "localhost" (la même machine), une IP ("192.168.1.42"), ou un nom de machine ("macbook.local").
  • port : un nombre, celui du serveur.
  • action : le nom donné à @server.action("...").
  • les paramètres : autant que tu veux, par leur nom.

La réponse est déjà un objet Python : reponse["message"] marche directement.

L'ordre est hôte, port, action

capy.call(4000, "localhost", "hello") donne une TypeError qui rappelle le bon ordre. Et capy ne parle pas HTTP : pas de http:// devant l'hôte.

Des paramètres qui s'appellent name, host ou action

Pas de souci : capy.call("localhost", 4000, "creer", name="Zoe", action="sauter") marche, ces noms ne se mélangent pas avec ceux de call. Seul tls est réservé.

Depuis le terminal#

Pas besoin d'écrire un client pour essayer une action : capy call l'appelle et affiche la réponse en JSON.

terminal
capy call localhost:4000 hello nom=Izac
résultat
{
  "message": "Bonjour Izac !"
}
  • Les paramètres s'écrivent nom=valeur. Les nombres, true / false, null et les listes sont compris : age=14 envoie le nombre 14, actif=true un booléen, ids=[1,2] une liste. Le reste est du texte.
  • Un serveur en clair : ajoute --no-tls.
  • Si le serveur répond {"erreur": …}, la commande se termine avec le code 1 : pratique dans un script.
  • Dans un projet, plus besoin d'adresse : cy call hello nom=Izac appelle le serveur du projet, qu'il soit en local, sur le wifi ou derrière un relais.

Le mode interactif#

capy cli ouvre une invite où tu tapes les commandes à la suite, sans retaper capy à chaque fois :

résultat
◆ capy  v0.5.4 · mode interactif
  help pour l'aide · Tab pour compléter · ↑↓ l'historique · exit pour sortir

capy (mon-jeu) ❯ call hello nom=Izac
{
  "message": "Bonjour Izac !"
}
  4 ms
capy (mon-jeu) ❯ wifi scan
  ●   maison   192.168.1.42:51234
capy (mon-jeu) ❯ exit
  • Tab complète les commandes, les scripts et les fichiers du projet, et les sous-commandes (wifi sc → wifi scan).
  • ↑ ↓ retrouvent les commandes d'avant, même d'une fois sur l'autre (gardées dans ~/.capy/history).
  • cd dossier change de projet sans sortir : l'invite affiche le projet où tu es.
  • Une erreur s'affiche en rouge, sans faire sortir du mode. Ctrl+C arrête la commande en cours, exit ou Ctrl+D sort.

Les couleurs

Dans un terminal, capy et cy affichent tout en couleurs (thème Catppuccin), avec des icônes et des tableaux alignés. Dans un fichier ou un pipe (| jq, > sortie.txt), la sortie redevient du texte et du JSON purs. Pour couper les couleurs partout : export NO_COLOR=1.

Avec jq

La sortie est du JSON pur (les messages comme « empreinte retenue » vont sur la sortie d'erreur) : capy call localhost:4000 hello | jq -r .message affiche juste Bonjour monde !.

Fichier de projet : capy.config.json#

Plutôt que de répéter le port, l'adresse ou le relais dans chaque fichier, décris tout ton projet une fois dans un capy.config.json : son nom, sa version, comment le joindre, et ses fichiers. Le serveur et le client le lisent avec capy.project(), et la commande cy lance ses scripts et ses fichiers, comme npm avec package.json (cy ne marche que dans un projet ; pour en créer un : capy new).

Créer un projet en une commande#

terminal
capy new mon-jeu
cd mon-jeu
cy run
résultat
Créé : /Users/izac/projets/mon-jeu
mon-jeu v1.0
  mode     local (localhost:4000)
  tls      oui (chiffré)
  server   server.py  ✓
  client   client.py  ✓
server │ API en écoute sur le port 4000, chiffrée (TLS) · empreinte : 9dfa7b98…
Bonjour toi !
server │ 127.0.0.1 → {"action": "hello", "nom": "toi"}

new crée le dossier avec capy.config.json, un server.py et un client.py qui marchent déjà. run lance le serveur en fond, attend qu'il soit prêt, lance le client, puis arrête le serveur.

Le fichier#

capy.config.json
{
    "name": "mon-jeu",
    "description": "Un petit jeu en réseau",
    "version": "1.0",
    "author": "Izac",
    "mode": "local",
    "url": "127.0.0.1",
    "port": 2388,
    "tls": true,
    "files": {
        "server": "server.py",
        "client": "client.py"
    }
}
server.py
import capy

app = capy.project()          # lit capy.config.json
server = app.server()         # déjà réglé : port, chiffrement…

@server.action("hello")
def hello(nom="monde"):
    return {"message": f"Bonjour {nom} !"}

server.run()
client.py
import capy

app = capy.project()
reponse = app.call("hello", nom="Izac")     # pas d'IP, pas de port
print(capy.get(reponse, "message"))

Ce qu'on peut mettre dedans#

CléPourPar défaut
namele nom du projet, et le nom du serveur en wifi / networkle nom du dossier
descriptionune phrase sur le projet—
versionla version du projet, ex : "1.0"—
authorqui a fait le projet—
mode"local", "wifi" ou "network""network" s'il y a un relay, sinon "local"
url ou hostl'adresse du serveur pour le client (mode local) ; url peut contenir le port : "127.0.0.1:2388""localhost"
portle port du serveur (obligatoire en local, choisi tout seul en wifi)—
tlsle chiffrementtrue
relayle relais capy.network, ex : "mon-vps.fr:3879"—
keyla clé du relais—
fingerprintl'empreinte du relais—
filesles fichiers du projet, par nom : "server", "client", ou ceux que tu veux—

Écris-le comme tu veux

capy convertit tout seul : "port": "2388" (en texte) devient 2388, et "tls" accepte true, "True", "oui", "non", "1", "0"… Une valeur vraiment impossible donne une erreur claire : 'tls' doit être true ou false dans capy.config.json (reçu 'peut-etre').

Le même code marche dans les trois modes : seul le fichier change.

capy.config.json (wifi)
{ "name": "salon", "mode": "wifi", "files": {"server": "server.py", "client": "client.py"} }
capy.config.json (network)
{ "name": "maison", "mode": "network", "relay": "mon-vps.fr:3879", "key": "$CAPY_KEY" }

Les commandes#

CommandeCe qu'elle fait
cymontre le projet : nom, version, mode, adresse, fichiers (✓ s'ils existent), scripts
capy new NOM [mode]crée un dossier NOM avec capy.config.json, server.py, client.py
capy init [nom] [mode]crée juste capy.config.json dans le dossier actuel
cy serverlance le fichier "server" de files
cy clientlance le fichier "client" (les arguments en plus lui sont passés)
cy NOMlance n'importe quel fichier de files, ex : cy tests
cy NOM [args…]lance le script NOM de scripts, comme npm run (avant un fichier du même nom)
cy scriptsliste les scripts
cy runserveur en fond + client, puis arrête le serveur

mode vaut local par défaut, ou wifi, ou network : capy new salon wifi.

Plus de fichiers dans files

"files": {"server": "server.py", "client": "client.py", "admin": "outils/admin.py", "tests": "tests.py"}, puis cy admin ou cy tests. Dans le code : app.file("admin") donne le chemin.

Les secrets dans des variables d'environnement

Un texte qui commence par $ est lu dans les variables d'environnement : avec "key": "$CAPY_KEY", la vraie clé reste dans ton ~/.zshrc (export CAPY_KEY='…') et le fichier peut être partagé ou mis sur GitHub sans risque.

Trouvé même depuis un sous-dossier

capy.project() et cy cherchent capy.config.json dans le dossier, puis dans les dossiers au-dessus (comme git). Pour un autre fichier : capy.project("autre.json") ou capy.project("~/projets/jeu").

Une faute de frappe dans le fichier

capy vérifie tout au chargement et dit quoi corriger : ValueError: capy.project : clé 'prot' inconnue dans capy.config.json (tu voulais dire 'port' ?). Pareil pour "url": "http://…" (capy ne parle pas HTTP) ou deux ports différents dans url et port.

Lire une réponse#

capy.get(reponse, "chemin") sort une valeur d'une réponse (dict, liste ou texte JSON). Le chemin passe dans les dicts par leur clé, et dans les listes par leur numéro (0 = le 1er).

python
reponse = {"text": "Hello", "users": [{"name": "Izac"}, {"name": "Zoe"}]}

capy.get(reponse, "text")             # "Hello"
capy.get(reponse, "users.0.name")     # "Izac"
capy.get(reponse, "users.-1.name")    # "Zoe" (-1 = le dernier)
capy.get(reponse, "age", default=0)   # 0 (au lieu d'une erreur si la clé n'existe pas)
capy.get(reponse, "texte")
# ValueError: capy.get : clé 'texte' introuvable dans 'texte' (clés : text, users)

Vérifier une erreur en une ligne

capy.get(reponse, "erreur", default=None) vaut None quand tout va bien : if capy.get(reponse, "erreur", default=None): exit(reponse["erreur"])

Convertir#

Pour transformer une valeur reçue. Si c'est impossible : ValueError avec un message clair (dans une action, le client reçoit {"erreur": "..."}).

FonctionAccepteExemple
capy.to_str(x)dict, liste… (un texte reste tel quel)to_str({"a": 1}) → '{"a": 1}'
capy.to_int(x)42, 42.0, "42"to_int("42") → 42
capy.to_float(x)4.5, 4, "4.5"to_float("4.5") → 4.5
capy.to_bool(x)true/false, 1/0, "oui"/"non"to_bool("oui") → True
capy.to_list(x)liste, tuple, '[1, 2]'to_list("[1, 2]") → [1, 2]
capy.to_dict(x)dict, '{"a": 1}'to_dict('{"a": 1}') → {"a": 1}

Toutes acceptent un chemin en 2e argument pour extraire et convertir d'un coup :

python
capy.to_int(reponse, "age")    # "12" -> 12
capy.to_str(reponse, "text")   # "Hello"

Côté serveur aussi

Ce que tape un utilisateur avec input() est toujours du texte. Dans l'action, capy.to_int(age) transforme "14" en 14, ou renvoie une erreur claire au client si on a tapé "abc".

Gérer les erreurs#

Il y a deux sortes de problèmes, et il faut gérer les deux :

ProblèmeCe qui se passeComment le gérer
le serveur n'est pas lancé, mauvais port, réseau coupéConnectionErrortry / except ConnectionError
l'action trop lente (plus de 3 s)ConnectionErroridem
ce n'est pas le bon serveur (empreinte changée)capy.SecurityErrorne pas l'ignorer !
l'action a refusé ou plantéune réponse {"erreur": "..."}regarder la clé "erreur"

Un client complet et propre :

client.py
import capy

try:
    reponse = capy.call("localhost", 4000, "diviser", a=10, b=0)
except ConnectionError as e:
    exit(str(e))                         # str() : exit() veut du texte, pas une erreur

if capy.get(reponse, "erreur", default=None):
    exit("Refusé : " + reponse["erreur"])

print(capy.get(reponse, "resultat"))

Pylance râle sur exit(e)

exit() accepte un texte ou un nombre, pas une exception : écris exit(str(e)).

Chiffrement (TLS)#

Tout est chiffré par défaut, sans rien configurer : capy, capy.wifi et capy.network.

  • Chaque machine qui fait tourner un serveur crée son certificat toute seule au 1er lancement (~/.capy/server-cert.pem, avec openssl). Le serveur affiche son empreinte en démarrant.
  • Comme SSH, un client retient l'empreinte d'un serveur à la 1re connexion, dans ~/.capy/known_hosts.json. Si elle change ensuite, capy refuse avec capy.SecurityError : quelqu'un se fait peut-être passer pour le serveur.
  • Un serveur sur ta propre machine (localhost) est reconnu tout seul, sans rien retenir.
  • capy.fingerprint() donne l'empreinte des serveurs de ta machine.
résultat
capy : nouveau serveur 192.168.1.42:4000, empreinte retenue 9dfa7b98c1e0…
« l'empreinte a CHANGÉ ! » : que faire ?

Si tu as réinstallé le serveur, changé de machine ou effacé ~/.capy sur le serveur, c'est normal : ouvre ~/.capy/known_hosts.json sur le client et supprime la ligne de ce serveur. Sinon, méfie-toi : c'est exactement le message qu'on verrait si quelqu'un se faisait passer pour ton serveur.

Sans chiffrement#

Pour parler en clair (plus rapide en local), mets tls=False des deux côtés :

python
api = capy.Server(4000, tls=False)                   # serveur
capy.call("localhost", 4000, "hello", tls=False)     # client

Si un seul côté est chiffré, le message le dit :

résultat
ConnectionError: capy : connexion chiffrée impossible avec localhost:4000 — le serveur n'est peut-être pas chiffré (tls=False des deux côtés pour s'en passer)

Tester un serveur sans Python

Un serveur tls=False se teste depuis le terminal avec nc : une ligne JSON envoyée, une ligne JSON reçue. printf '{"action": "hello", "nom": "Izac"}\n' | nc localhost 4000

Sécurité#

Qui peut appeler tes actions ?

Le serveur écoute sur toutes les adresses de la machine : n'importe quel appareil qui peut joindre ce port (le même wifi, voire internet si ta box redirige le port) peut appeler tes actions. Le chiffrement empêche d'espionner les échanges, pas d'appeler. Ne mets pas d'action dangereuse (supprimer des fichiers, lancer des commandes…) sans demander un mot de passe.

  • Les secrets ne s'affichent jamais dans le journal du serveur : les paramètres dont le nom contient password, pwd, mdp, secret ou token apparaissent comme "***".
  • Ne stocke jamais un mot de passe en clair : garde son hash (voir l'exemple « Connexion avec mot de passe »).
  • Côté client, simplesuit.sys.password() demande un mot de passe en affichant des *.
résultat
127.0.0.1 → {"action": "login", "name": "Zoe", "password": "***"}

Astuces en vrac#

Laisser le système choisir le port

capy.Server(0) prend un port libre au hasard ; après run(), il est dans server.port.

Lancer un serveur dans le même script (pour tester)

run() ne rend jamais la main : lance-le dans un thread, et attends server.listening. Voir l'exemple « Serveur et client dans le même fichier ».

OSError: Address already in use

Le port est déjà pris, souvent par ton propre serveur lancé dans un autre terminal (ou resté ouvert). Trouve qui l'utilise avec lsof -i :4000, puis arrête-le, ou choisis un autre port.

« n'a rien répondu » ou None partout

Ton serveur tourne peut-être encore avec l'ancien code. Arrête-le (Ctrl+C) et relance-le.

Même code, trois réseaux

Une action écrite pour capy.Server marche telle quelle avec capy.wifi.Server("nom") ou capy.network.Server("nom") : seule la ligne qui crée le serveur change.

Questions fréquentes#

Pourquoi capy plutôt que Flask ou FastAPI ?

Pour la simplicité : pas d'URL, pas de méthode GET/POST, pas de serveur web à configurer, le chiffrement déjà fait, et le client tient en une ligne. Si tu as besoin d'un vrai site web ou d'une API publique utilisée par des navigateurs, prends plutôt un framework HTTP.

Un programme qui n'est pas en Python peut-il appeler capy ?

Oui, avec tls=False c'est juste du TCP avec une ligne JSON : n'importe quel langage sait faire ça (même nc). En chiffré, il faut parler TLS et accepter le certificat auto-signé du serveur.

Le serveur peut-il envoyer un message au client sans qu'on lui demande ?

Non : le client demande, le serveur répond. Pour l'inverse, le client peut lui aussi lancer un petit capy.Server et donner son adresse au serveur.

Où sont rangés les certificats et les empreintes ?

Dans le dossier ~/.capy : server-cert.pem (le certificat de tes serveurs), relay-cert.pem (celui d'un relais capy.network) et known_hosts.json (les empreintes retenues par tes clients).

Exemples

Premier serveur et son client

Un serveur avec deux actions, et un client qui les appelle. Lance serveur.py dans un terminal, puis client.py dans un autre.

serveur.py
import capy

api = capy.Server(4000)


# Les paramètres envoyés par le client arrivent comme arguments
@api.action("bonjour")
def bonjour(nom="toi"):
    return {"message": f"Bonjour {nom} !"}


@api.action("addition")
def addition(a, b):
    return {"resultat": a + b}


api.run()

Renvoyer du JSON écrit en texte

capy.json() transforme un texte JSON en vraie réponse : le client reçoit un dict, pas un texte.

serveur.py
import capy

api = capy.Server(4000)


@api.action("profil")
def profil():
    texte = """
    {
        "nom": "Alice",
        "age": "12",
        "langages": ["python", "html"]
    }
    """
    return capy.json(texte)


api.run()

Lire une réponse : get() et les conversions

Extraire une valeur avec un chemin "a.b.0", et la convertir avec une erreur claire si c'est impossible. Pas besoin de serveur pour celui-ci : lance-le directement.

lire_une_reponse.py
import capy

reponse = {
    "ville": "Paris",
    "temperature": "18.5",
    "pluie": "non",
    "jours": [{"nom": "lundi", "max": 21}, {"nom": "mardi", "max": 19}],
}

print(capy.get(reponse, "ville"))                 # Paris
print(capy.get(reponse, "jours.1.nom"))           # mardi  (0 = le 1er)
print(capy.get(reponse, "jours.-1.max"))          # 19     (-1 = le dernier)
print(capy.get(reponse, "vent", default=0))       # 0      (la clé n'existe pas)

print(capy.to_float(reponse, "temperature"))      # 18.5
print(capy.to_bool(reponse, "pluie"))             # False
print(capy.to_str({"ok": True}))                  # {"ok": true}  (dict -> texte JSON)

# Si c'est impossible : ValueError avec un message qui dit quoi corriger
for essai in (lambda: capy.get(reponse, "jours.5"), lambda: capy.to_int(reponse, "ville")):
    try:
        essai()
    except ValueError as e:
        print("Erreur :", e)

Gérer les erreurs

Deux sortes d'erreurs : le serveur ne répond pas (ConnectionError), ou il répond {"erreur": "..."} (action inconnue, mauvais paramètre, exception dans l'action). Pas besoin de serveur pour celui-ci : lance-le directement.

gerer_les_erreurs.py
import capy

# 1. Personne n'écoute sur ce port -> ConnectionError
try:
    capy.call("localhost", 4999, "bonjour")
except ConnectionError as e:
    print("Pas de réponse :", e)

# 2. Les erreurs côté serveur reviennent dans la réponse
#    (handle() traite une demande sans passer par le réseau : pratique pour tester)
api = capy.Server(4000)


@api.action("diviser")
def diviser(a, b):
    return {"resultat": a / b}


print(api.handle('{"action": "diviser", "a": 6, "b": 3}'))  # {'resultat': 2.0}
print(api.handle('{"action": "diviser", "a": 6, "b": 0}'))  # {'erreur': 'division by zero'}
print(api.handle('{"action": "multiplier"}'))               # {'erreur': 'action inconnue'}
print(api.handle("pas du json"))                            # {'erreur': 'JSON invalide'}

capy.wifi : se trouver par son nom sur le même wifi

Pas besoin de connaître l'IP ni le port : le serveur s'annonce sous un nom. Lance serveur.py sur un appareil, client.py sur un autre du même wifi (ou le même).

serveur.py
import capy.wifi

api = capy.wifi.Server("salon")  # Port choisi tout seul


@api.action("lumiere")
def lumiere(allumer=True):
    etat = "allumée" if allumer else "éteinte"
    return {"lumiere": etat}


api.run()

capy.network : à travers tout internet, chiffré

Un relais tourne sur une machine joignable depuis internet (un VPS). Les serveurs s'y connectent même derrière une box, les clients les appellent par leur nom.

1. Sur le VPS : ouvrir le port, puis lancer le relais (il affiche son empreinte)

relais.sh
ufw allow 3879/tcp
capy relay 3879 motdepasse
# Relais capy.network en écoute sur le port 3879, chiffré (TLS), avec clé
# Empreinte : 9b734c758696262e758f8664db40fafe3601f3804ced5dc331527704319f3049

Chiffrement : rien à faire

Tout est chiffré par défaut. Au démarrage, le serveur affiche son empreinte (son « identité ») ; le client la retient à la 1re connexion et refuse ensuite de parler à un imposteur.

serveur.py
import capy

api = capy.Server(4000)                 # Chiffré (TLS)
# api = capy.Server(4000, tls=False)    # En clair : le client doit aussi mettre tls=False


@api.action("secret")
def secret():
    return {"code": 1234}


api.run()
# API en écoute sur le port 4000, chiffrée (TLS) · empreinte : 9dfa7b98…

Connexion avec mot de passe

Un serveur qui crée des comptes et vérifie les mots de passe, sans jamais les enregistrer en clair. On garde seulement un « hash » (une empreinte impossible à inverser) mélangé à un « sel » aléatoire. Lance serveur.py dans un terminal, puis client.py dans un autre.

serveur.py
import hashlib
import secrets

import capy

api = capy.Server(4000)
comptes = {}  # nom -> {"sel": ..., "hash": ...}  (dans un vrai projet : un fichier JSON)


def hacher(mot_de_passe, sel):
    # 200 000 tours : très lent à deviner par force brute, mais instantané pour une seule vérification
    return hashlib.pbkdf2_hmac("sha256", mot_de_passe.encode(), bytes.fromhex(sel), 200_000).hex()


@api.action("inscription")
def inscription(nom, password):
    if nom in comptes:
        raise ValueError(f"le nom {nom!r} est déjà pris")
    sel = secrets.token_hex(16)
    comptes[nom] = {"sel": sel, "hash": hacher(password, sel)}
    return {"ok": True}


@api.action("connexion")
def connexion(nom, password):
    compte = comptes.get(nom)
    # Même message si le nom OU le mot de passe est faux : on ne révèle pas quels noms existent
    if compte is None or not secrets.compare_digest(hacher(password, compte["sel"]), compte["hash"]):
        raise ValueError("nom ou mot de passe incorrect")
    return {"ok": True, "message": f"Bienvenue {nom} !"}


api.run()  # Dans le journal, les mots de passe s'affichent "***"

Un compteur partagé : plusieurs clients en même temps

Chaque appel tourne dans son propre thread. Sans verrou, deux appels simultanés peuvent lire la même valeur et l'un écrase l'autre : le compte final est faux. threading.Lock() règle ça. Tout est dans ce fichier (serveur dans un thread + 20 clients) : lance-le directement.

compteur_partage.py
import threading
import time

import capy

api = capy.Server(0)  # 0 : un port libre au hasard
compteur = 0
verrou = threading.Lock()


@api.action("plus_un")
def plus_un():
    global compteur
    with verrou:  # Un seul appel à la fois dans ce bloc
        valeur = compteur
        time.sleep(0.01)  # Simule un petit travail (lire un fichier, calculer…)
        compteur = valeur + 1
    return {"compteur": compteur}


threading.Thread(target=api.run, daemon=True).start()
while not api.listening:  # Attendre que le serveur soit prêt
    time.sleep(0.05)

clients = [threading.Thread(target=capy.call, args=("localhost", api.port, "plus_un")) for _ in range(20)]
for client in clients:
    client.start()
for client in clients:
    client.join()

print("Compteur :", compteur)  # Toujours 20 (sans le verrou : souvent moins !)

Serveur et client dans le même fichier

run() ne rend jamais la main : on le lance dans un thread, puis on attend server.listening. Pratique pour tester ses actions, ou pour un programme qui est à la fois serveur et client. Pour tester une action sans réseau du tout : api.handle('{"action": "..."}').

serveur_et_client_ensemble.py
import threading
import time

import capy

api = capy.Server(0)


@api.action("carre")
def carre(x):
    return {"resultat": capy.to_float(x) ** 2}


threading.Thread(target=api.run, daemon=True).start()  # daemon : s'arrête avec le programme
while not api.listening:
    time.sleep(0.05)

print("Port choisi :", api.port)
print(capy.call("localhost", api.port, "carre", x=7))     # {'resultat': 49.0}
print(capy.call("localhost", api.port, "carre", x="3"))   # {'resultat': 9.0} ("3" converti)
print(capy.call("localhost", api.port, "carre", x="abc")) # {'erreur': "capy.to_float : impossible de transformer 'abc' (str) en float"}
print(api.handle('{"action": "carre", "x": 5}'))          # {'resultat': 25.0}, sans réseau

Parler à capy depuis le terminal

Sans chiffrement (tls=False), capy c'est juste une ligne JSON envoyée, une ligne JSON reçue : n'importe quel outil ou langage peut l'appeler. Ici avec nc (netcat), déjà installé sur Mac et Linux.

serveur.py
import capy

api = capy.Server(4000, tls=False)


@api.action("hello")
def hello(nom="monde"):
    return {"message": f"Bonjour {nom} !"}


api.run()

Lister les serveurs du wifi

scan() demande à tout le wifi « il y a des serveurs capy ? » et attend les réponses pendant 1 seconde. Lance quelques capy.wifi.Server ailleurs (ou l'exemple 5) pour voir quelque chose.

scanner_le_wifi.py
import capy.wifi

serveurs = capy.wifi.scan(timeout=1)

if not serveurs:
    print("Aucun serveur capy.wifi trouvé (même wifi ? wifi public qui isole les appareils ?)")

for nom, (ip, port) in sorted(serveurs.items()):
    ici = " (cet appareil)" if ip == "127.0.0.1" else ""
    print(f"{nom:<15} {ip}:{port}{ici}")
# maison          192.168.1.42:51234
# salon           127.0.0.1:50110 (cet appareil)

Garder le relais allumé (systemd)

Sur le VPS : le relais tourne en fond, redémarre s'il plante et se relance au démarrage du VPS. Remplace MA_CLE_SECRETE par ta clé, et /root si capy est installé pour un autre utilisateur.

relais_systemd.sh
sudo tee /etc/systemd/system/capy-relay.service > /dev/null <<'SERVICE'
[Unit]
Description=Relais capy.network
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=/usr/bin/python3 -u -m capy.network 3879 MA_CLE_SECRETE
WorkingDirectory=/root
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
SERVICE

sudo systemctl daemon-reload
sudo systemctl enable --now capy-relay   # Lancer maintenant + à chaque démarrage
systemctl status capy-relay              # Il doit être "active (running)"
journalctl -u capy-relay -f              # Voir son journal en direct (Ctrl+C pour sortir)

Un projet complet : capy.config.json

On décrit le projet une fois (nom, version, port, adresse, chiffrement, fichiers…) ; serveur et client le lisent avec capy.project(), et cy lance ses scripts et ses fichiers (comme npm).

1. Créer le projet (modes : local, wifi ou network), puis tout lancer d'un coup

creer_le_projet.sh
capy new mon-jeu
cd mon-jeu
cy          # Voir ce que capy a compris
# mon-jeu v1.0
#   mode     local (localhost:4000)
#   tls      oui (chiffré)
#   server   server.py  ✓
#   client   client.py  ✓

cy run      # Serveur en fond + client, puis arrêt du serveur
# server │ API en écoute sur le port 4000, chiffrée (TLS) · empreinte : …
# Bonjour toi !

cy server   # Ou chacun dans son terminal
cy client

capy.domo : trouver les appareils de la maison

Cherche sur le wifi les lampes et prises Hue, WiZ, Shelly, Yeelight et Home Assistant, en même temps. ⚠️ À lancer chez toi : sur un wifi public, ce ne sont pas tes lampes.

trouver_les_appareils.py
import capy.domo

home = capy.domo.Home()  # capy.domo.json à côté du script (créé au save())

nouveaux = home.scan()   # ~2 secondes
for appareil in nouveaux:
    print(f"+ {appareil.name:<18} {appareil.brand:<14} {appareil.host or appareil.id}")
for conseil in home.hints:  # Ex : « Pont Hue trouvé à 192.168.1.10 : appaire-le avec … »
    print("!", conseil)

home.save()
print(home)  # <capy.domo.Home 4 appareils, 0 groupes, 0 scènes (…/capy.domo.json)>

capy.domo : piloter une lampe

Les mêmes méthodes pour toutes les marques. Régler une lampe éteinte l'allume.

piloter_une_lampe.py
import time

import capy.domo

home = capy.domo.Home()
lampe = home["bureau"]  # Le nom donné dans capy.domo.json

lampe.on()
lampe.brightness(30)            # %
time.sleep(1)
lampe.color("orange")           # "orange", "#FF7800" ou (255, 120, 0)
time.sleep(1)
lampe.white("chaud")            # Ou 2700 (kelvins)
time.sleep(1)
lampe.set(brightness=100, color="bleu")

print(lampe.state())  # {'on': True, 'brightness': 100, 'color': '#0000FF', 'kelvin': None, 'reachable': True}
lampe.off()

capy.domo : groupes et scènes

Un groupe se pilote comme une lampe (tous ensemble, en même temps). Une scène règle plusieurs groupes d'un coup. Groupes et scènes sont gardés dans capy.domo.json.

groupes_et_scenes.py
import capy.domo

home = capy.domo.Home()

home.set_group("salon", ["lampe-du-salon", "bureau"])
home.scenes["film"] = {
    "salon": {"brightness": 15, "color": "orange"},
    "cuisine": {"on": False},
}
home.save()

home["salon"].white("chaud")   # Les deux lampes en même temps
home.scene("film")             # Tout le monde prend son réglage
home["tout"].off()             # « tout » : tous les appareils

home.save_scene("soiree")      # L'état actuel devient une scène
home.save()

capy.domo à distance : la maison devient un serveur capy

Sur l'ordi qui reste à la maison. Avec un capy.config.json, c'est son mode (local, wifi ou network) qui est utilisé : avec "relay", la maison est pilotable depuis tout internet. Sans, un serveur capy sur le port 4040.

maison.py
import capy.domo

capy.domo.serve()  # Actions domo.list, domo.state, domo.set, domo.toggle, domo.scene

capy.domo : un réveil lumineux

La lampe s'allume très doucement, d'un orange chaud vers un blanc de jour, comme un lever de soleil. Lance-le avec l'heure du réveil : python3 19_reveil_lumineux.py 7:30

reveil_lumineux.py
import datetime as dt
import sys
import time

import capy.domo

DUREE = 20 * 60  # Le lever de soleil dure 20 minutes
lampe = capy.domo.Home()["chambre"]

heure, minute = (int(x) for x in (sys.argv[1] if len(sys.argv) > 1 else "7:30").split(":"))
reveil = dt.datetime.now().replace(hour=heure, minute=minute, second=0, microsecond=0)
if reveil < dt.datetime.now():
    reveil += dt.timedelta(days=1)
debut = reveil - dt.timedelta(seconds=DUREE)
print(f"Lever de soleil de {debut:%H:%M} à {reveil:%H:%M} (Ctrl+C pour annuler)")
time.sleep(max((debut - dt.datetime.now()).total_seconds(), 0))

for etape in range(1, 61):  # 60 étapes : toutes les 20 secondes
    avance = etape / 60
    if avance < 0.4:
        lampe.set(brightness=1 + avance * 50, color=(255, round(80 + avance * 200), 0))  # Rouge-orangé qui monte
    else:
        lampe.set(brightness=avance * 100, kelvin=round(2200 + avance * 3800))         # Puis blanc de plus en plus froid
    time.sleep(DUREE / 60)
print("Debout ! ☀️")

Référence

Classes

class Project(path)

Un projet capy, lu depuis capy.config.json

Exapp = capy.project() puis app.server() côté serveur, app.call("hello") côté client

Project.file(name)

Chemin d'un fichier du projet ("files" dans capy.config.json)

Exapp.file("server") -> /Users/izac/projets/jeu/server.py

Voir le code
python
def file(self, name):
if name not in self.files:
    known = ", ".join(self.files) or "aucun"
    raise KeyError(f'capy.project : pas de fichier {name!r} dans "files" de {self.path.name} (fichiers : {known})')
return self.files[name]

Project.run_script(name, args=())

Lancer un script de "scripts" dans le dossier du projet -> son code de sortie (0 = tout va bien)

Comme npm run : la commande passe par le shell ; "python3" au début = le Python qui lance capy ;

le script reçoit CAPY_NAME, CAPY_MODE, CAPY_HOST, CAPY_PORT dans ses variables d'environnement

Exapp.run_script("test") / app.run_script("start", ["--verbose"])

Voir le code
python
def run_script(self, name, args=()):
if name not in self.scripts:
    known = ", ".join(self.scripts) or "aucun"
    close = difflib.get_close_matches(name, self.scripts, n=1)
    hint = f" (tu voulais dire {close[0]!r} ?)" if close else ""
    raise KeyError(f'capy.project : pas de script {name!r} dans "scripts" de {self.path.name}{hint} (scripts : {known})')
command = self.scripts[name]
first, _, rest = command.partition(" ")
if first in ("python3", "python"):
    command = shlex.quote(sys.executable) + (" " + rest if rest else "")
elif first == "cy":  # cy au début = capy avec ce même Python (même si cy n'est pas dans le PATH)
    command = shlex.quote(sys.executable) + " -m capy" + (" " + rest if rest else "")
if args:
    command += " " + " ".join(shlex.quote(str(a)) for a in args)
env = {**os.environ, "CAPY_NAME": self.name, "CAPY_MODE": self.mode, "CAPY_PROJECT": str(self.folder)}
if self.mode == "local":
    env.update(CAPY_HOST=self.host, CAPY_PORT=str(self.port))
return subprocess.call(command, shell=True, cwd=self.folder, env=env)

Project.describe()

Tout le projet en texte (cy)

Voir le code
python
def describe(self):
where = {"local": f"{self.host}:{self.port}", "wifi": f"sur le wifi, sous le nom {self.name!r}",
         "network": f"via le relais {self.relay}, sous le nom {self.name!r}"}[self.mode]
lines = [f"{self.name}" + (f" v{self.version}" if self.version else "") + (f" par {self.author}" if self.author else "")]
if self.description:
    lines.append(f"  {self.description}")
lines += [f"  mode     {self.mode} ({where})", f"  tls      {'oui (chiffré)' if self.tls else 'non (en clair)'}"]
for name, path in self.files.items():
    shown = path.relative_to(self.folder) if path.is_relative_to(self.folder) else path
    lines.append(f"  {name:<8} {shown}  {'✓' if path.is_file() else '✗ introuvable'}")
for name, command in self.scripts.items():
    lines.append(f"  script   {name:<8} {command}")
lines.append(f"  fichier  {self.path}")
return "\n".join(lines)

Project.server()

Le serveur de ce projet, déjà réglé (port, nom, chiffrement, relais)

Exserver = app.server() ; @server.action("hello") ... ; server.run()

Voir le code
python
def server(self):
import capy
if self.mode == "local":
    return capy.Server(self.port, tls=self.tls)
if self.mode == "wifi":
    import capy.wifi
    return capy.wifi.Server(self.name, port=self.port or 0, tls=self.tls)
self._use_relay()
import capy.network
return capy.network.Server(self.name)

Project.call(action, /, **params) -> Any

Appeler une action du serveur de ce projet (sans redonner l'IP, le port ni le relais)

Exapp.call("hello", nom="Izac") -> {"message": "Bonjour Izac !"}

Voir le code
python
def call(self, action, /, **params) -> Any:
import capy
if self.mode == "local":
    return capy.call(self.host, self.port, action, tls=self.tls, **params)
if self.mode == "wifi":
    import capy.wifi
    return capy.wifi.call(self.name, action, tls=self.tls, **params)
self._use_relay()
import capy.network
return capy.network.call(self.name, action, **params)

class SecurityError

L'empreinte d'un serveur ne correspond pas : on refuse de lui parler

Hérite de builtins.Exception et de toutes ses méthodes.

class Server(port=None, tls=True)

Serveur : @server.action("nom") pour déclarer une action, server.run() pour lancer

Le port se donne à Server(port) ou à run(port), au choix

Chiffré (TLS) par défaut ; tls=False pour parler en clair (le client aussi doit mettre tls=False)

Server.action(name)

Pas de description.

Voir le code
python
def action(self, name):
def decorator(function):
    self.actions[name] = function
    return function
return decorator

Server.handle(line)

{"action": "get", "id": 2} -> get(id=2)

Voir le code
python
def handle(self, line):
try:
    request = jsonlib.loads(line)
    function = self.actions[request.pop("action")]
except (ValueError, AttributeError, TypeError):  # pas du JSON, ou pas un objet {...}
    return {"erreur": "JSON invalide"}
except KeyError:
    return {"erreur": "action inconnue"}
try:
    return function(**request)
except Exception as e:
    return {"erreur": str(e)}

Server.client(conn, addr)

Pas de description.

Voir le code
python
def client(self, conn, addr):
if self._context:
    conn = _tls.accept(self._context, conn)
    if conn is None:  # Le client ne parle pas en chiffré -> on raccroche
        return
else:
    conn.settimeout(10)
with conn:
    try:
        line = conn.makefile(encoding="utf-8").readline()
        if not line.strip():  # Connexion ouverte puis fermée sans rien demander
            return
        conn.sendall((_dumps(self.handle(line)) + "\n").encode())
    except (OSError, ValueError):
        return
    print(f"{addr[0]} → {_hide(line)}")

Server.run(port=None)

port 0 -> le système choisit un port libre

Voir le code
python
def run(self, port=None):
self.port = self.port if port is None else port
if self.port is None:
    raise ValueError("capy: aucun port, utilise Server(port) ou run(port)")
if self.tls:
    self._context, empreinte = _tls.server_context("server")
with socket.create_server(("", self.port)) as server:
    self.port = server.getsockname()[1]
    self.listening = True
    if self.tls:
        print(f"API en écoute sur le port {self.port}, chiffrée (TLS) · empreinte : {empreinte}")
    else:
        print(f"API en écoute sur le port {self.port} (pas chiffrée)")
    while True:
        conn, addr = server.accept()
        threading.Thread(target=self.client, args=(conn, addr), daemon=True).start()

Fonctions

project(path=None) -> capy._project.Project

Lire le capy.config.json du projet -> Project

Sans chemin : cherché à côté du script lancé, puis dans les dossiers au-dessus

Exapp = capy.project() / capy.project("autre.json") / capy.project("~/projets/jeu")

Voir le code
python
def project(path=None) -> Project:
    if path is None:
        found = find()
        if found is None:
            raise FileNotFoundError(
                f"capy.project : aucun {FILE_NAME} trouvé dans {_main_dir()} ni au-dessus.\n"
                f"  Crée-le avec : capy init\n"
                f"  ou écris-le toi-même, par exemple : {jsonlib.dumps(EXAMPLE, ensure_ascii=False)}"
            )
        return Project(found)
    path = pth.Path(path).expanduser()
    if not path.is_absolute():
        path = _main_dir() / path
    return Project(path / FILE_NAME if path.is_dir() else path)

json(text) -> Any

Exreturn capy.json('{"text": "Hello"}')

Voir le code
python
def json(text) -> Any:
    try:
        return jsonlib.loads(text)
    except ValueError as e:
        raise ValueError(f"capy.json : JSON invalide ({e})") from None

get(data, path, default=<object object at 0x1065682c0>) -> Any

Extraire une valeur d'un JSON (dict, liste, ou texte JSON) avec un chemin "a.b.0.c"

Exget('{"text": "Hello"}', "text") -> "Hello"

Exget(reponse, "users.0.name") -> "Izac" (0 = 1er élément d'une liste)

Exget(reponse, "age", default=0) -> 0 si "age" n'existe pas (au lieu d'une erreur)

Ex-> Any : la valeur peut être de n'importe quel type (sinon Pylance croit que c'est un object)

Voir le code
python
def get(data, path, default=_NOTHING) -> Any:
    value = json(data) if isinstance(data, str) else data
    for key in str(path).split("."):
        if isinstance(value, dict) and key in value:
            value = value[key]
        elif isinstance(value, list) and key.lstrip("-").isdigit() and -len(value) <= int(key) < len(value):
            value = value[int(key)]
        elif default is not _NOTHING:
            return default
        elif isinstance(value, dict):
            raise ValueError(f"capy.get : clé {key!r} introuvable dans {path!r} (clés : {', '.join(map(str, value)) or 'aucune'})")
        elif isinstance(value, list):
            raise ValueError(f"capy.get : {key!r} pas valable dans {path!r} (liste de {len(value)} éléments, 0 à {len(value) - 1})")
        else:
            raise ValueError(f"capy.get : impossible d'aller dans {key!r} de {path!r}, la valeur est un {type(value).__name__}")
    return value

to_str(data, key=None) -> str

Du JSON (dict, liste...) -> texte JSON sur une ligne (un texte reste tel quel)

Exto_str({"text": "Hello"}) -> '{"text": "Hello"}'

Exto_str('{"text": "Hello"}', "text") -> "Hello"

Voir le code
python
def to_str(data, key=None) -> str:
    if key is not None:
        data = get(data, key)
    if isinstance(data, str):
        return data
    try:
        return jsonlib.dumps(data, ensure_ascii=False)
    except (TypeError, ValueError):
        raise _error("to_str", data, "texte JSON") from None

to_int(data, key=None) -> int

-> entier. Accepte 42, 42.0, "42", " 42 "

Exto_int("42") -> 42 / to_int("abc") -> ValueError

Voir le code
python
def to_int(data, key=None) -> int:
    if key is not None:
        data = get(data, key)
    if isinstance(data, bool):  # True est un int pour Python, mais pas pour nous
        raise _error("to_int", data, "int")
    if isinstance(data, int):
        return data
    if isinstance(data, float) and data.is_integer():
        return int(data)
    if isinstance(data, str):
        try:
            return to_int(float(data)) if "." in data else int(data)
        except ValueError:
            pass
    raise _error("to_int", data, "int")

to_float(data, key=None) -> float

-> nombre à virgule. Accepte 4.5, 4, "4.5"

Exto_float("4.5") -> 4.5

Voir le code
python
def to_float(data, key=None) -> float:
    if key is not None:
        data = get(data, key)
    if not isinstance(data, bool) and isinstance(data, (int, float, str)):
        try:
            return float(data)
        except ValueError:
            pass
    raise _error("to_float", data, "float")

to_bool(data, key=None) -> bool

-> True / False. Accepte true/false, 1/0, "true"/"false", "oui"/"non", "1"/"0"

Exto_bool("oui") -> True

Voir le code
python
def to_bool(data, key=None) -> bool:
    if key is not None:
        data = get(data, key)
    if isinstance(data, bool):
        return data
    if data in (0, 1):
        return bool(data)
    if isinstance(data, str):
        text = data.strip().lower()
        if text in ("true", "oui", "yes", "1"):
            return True
        if text in ("false", "non", "no", "0"):
            return False
    raise _error("to_bool", data, "bool")

to_list(data, key=None) -> list

-> liste. Accepte une liste, un tuple, ou du texte JSON '[1, 2]'

Exto_list("[1, 2]") -> [1, 2]

Voir le code
python
def to_list(data, key=None) -> list:
    if key is not None:
        data = get(data, key)
    if isinstance(data, (list, tuple)):
        return list(data)
    if isinstance(data, str):
        try:
            value = jsonlib.loads(data)
        except ValueError:
            value = None
        if isinstance(value, list):
            return value
    raise _error("to_list", data, "list")

to_dict(data, key=None) -> dict

-> dict. Accepte un dict, ou du texte JSON '{"a": 1}'

Exto_dict('{"a": 1}') -> {"a": 1}

Voir le code
python
def to_dict(data, key=None) -> dict:
    if key is not None:
        data = get(data, key)
    if isinstance(data, dict):
        return data
    if isinstance(data, str):
        try:
            value = jsonlib.loads(data)
        except ValueError:
            value = None
        if isinstance(value, dict):
            return value
    raise _error("to_dict", data, "dict")

call(host, port, action, /, tls=True, **params) -> Any

Client : capy.call("127.0.0.1", 4000, "get", id=2) -> dict

Lève ConnectionError si le serveur ne répond pas, SecurityError si ce n'est pas le bon serveur

tls=False : pour un serveur pas chiffré (Server(..., tls=False))

Voir le code
python
def call(host, port, action, /, tls=True, **params) -> Any:
    return _call(host, port, action, params, tls, label=f"{host}:{port}")

fingerprint() -> str

Empreinte du certificat des serveurs capy de cette machine (à donner aux clients qui veulent vérifier)

Excapy.fingerprint() -> "9dfa7b98…"

Voir le code
python
def fingerprint() -> str:
    _tls.server_context("server")  # Crée le certificat s'il n'existe pas encore
    return _tls.my_fingerprint("server")

Constantes

SECRETS

Paramètres jamais affichés dans le journal du serveur (mots de passe, tokens…)

SECRETS = ("password", "passwd", "pwd", "mdp", "motdepasse", "secret", "token")