Accueil
YCappuccino
Python Component-Based Software Everywhere
YCappuccino est un framework de composants à la manière d'OSGi, bâti sur iPOPO. On écrit des classes Python ordinaires, des modèles annotés et des écrans décrits en YAML ; le framework en fait des composants qui se trouvent par leurs interfaces, génère le stockage, l'API et les interfaces, et un hôte assemble le tout par configuration, dans un seul processus ou réparti sur plusieurs. Le même framework tourne côté serveur, dans un terminal et dans le navigateur.
Cette page est elle-même une application YCappuccino : un site statique généré par le framework et servi par un hôte YCappuccino.
À propos / Philosophie
YCappuccino est un projet personnel, développé sur mon temps libre pour mes projets musicaux, indépendamment de toute activité professionnelle.
YCappuccino est un framework Python léger, modulaire et expérimental. Il est né d'un besoin concret : faire vivre la présence en ligne de mon groupe de musique, Y.Blues — son site, sa boutique, ses tablatures, son agenda — sans dépendre d'outils figés ni d'abonnements.
Plutôt qu'un bloc monolithique, YCappuccino est une suite d'outils modulaires en Python : un CMS pour les contenus, une boutique pour le merchandising et les albums, un agenda synchronisé, un éditeur de tablatures interactif. Chaque brique est un composant indépendant, qu'on assemble par simple configuration — dans un seul processus ou réparti sur plusieurs conteneurs.
Python Component-Based Software Everywhere — la devise du logo résume l'idée : tout est composant Python. Chaque fonctionnalité est une brique autonome, décrite par ses interfaces, qu'on assemble à volonté ; et le même code tourne partout : sur un serveur, réparti dans des conteneurs, dans un terminal, ou directement dans le navigateur grâce à Pyodide.
C'est un projet open source, mené par passion, au croisement du développement web, de la gestion de contenus et de la musique.
Ce site tourne lui-même sous YCappuccino.
Fonctionnalités
Gestion web & administration
Un back-office sécurisé pour gérer tous les contenus : authentification, rôles et droits fins (par donnée, par champ, par organisation).
Outils médias & musique
Un éditeur de tablatures interactif et collaboratif (import Guitar Pro, lecture, édition à plusieurs en temps réel), et une boutique de merchandising et d'albums, physiques ou numériques.
Données & temps réel
API REST et WebSockets pour les mises à jour en direct, intégration cartographique pour localiser concerts et événements, synchronisation avec Google Agenda pour les dates et les répétitions.
Architecture
Un moteur Python modulaire à base de composants, des modèles de données déclaratifs, un stockage souple (mémoire ou MongoDB) et des interfaces web, statiques ou console construites sur le même socle.
Le principe
OSGi en Python, sur iPOPO
YCappuccino repose sur iPOPO (Pelix), l'implémentation Python du modèle OSGi. Chaque fonctionnalité est un composant qui publie des services sous le nom de ses interfaces et en réclame d'autres. Le framework lie les composants dynamiquement : un composant démarre quand ses dépendances obligatoires sont là et s'arrête si l'une disparaît, une liste suit les services au fil de leurs arrivées et départs. Un composant ne connaît que des interfaces, jamais une implémentation : HTTP, WebSocket ou l'écran ne sont que des adaptateurs autour. La console Pelix permet d'inspecter l'application en marche.
core : des classes devenues composants
Le cœur parcourt les paquets déclarés et génère un composant iPOPO de chaque classe qui implémente une interface YCappuccino, sans décorateur. Le constructeur dit tout : un paramètre typé par une interface est une dépendance (obligatoire, optionnelle, filtrée par les propriétés du service ou liste vivante), un paramètre avec une valeur par défaut est une propriété, réglable dans application.yml. start et stop sont asynchrones. Des couches activent ou non des groupes de classes, et les valeurs ${nom:défaut} de la configuration viennent de l'environnement ou d'un fichier de propriétés.
api : les interfaces partagées
Le paquet api réunit les contrats que tous les modules partagent, et ne dépend que d'iPOPO : la base des composants, la journalisation et la configuration, le stockage (IManager, IStorage, déclencheurs, filtres), le CRUD (ICrud, IAuthorization), les services exposés (IExposedService, @rpc_method), HTTP et WebSocket, la publication par sujets, et les décorateurs des modèles. Les implémentations vivent dans les autres modules et se remplacent sans toucher au code qui les utilise.
Modules d'application, parties et hôtes
Une application est un paquet Python avec un descripteur, ycappuccino_app.yml : ses parties (des groupes de paquets et leurs réglages), les applications qu'elle requiert, ses fronts (les adresses qu'elle sert) et sa section de la console d'administration. Un hôte est un application.yml qui compose les applications voulues et n'ajoute que l'infrastructure : le stockage, le port, les secrets. Un hôte peut ne charger que certaines parties d'une application : c'est ainsi qu'une même application se répartit sur plusieurs processus.
component-creator : des composants créés en marche
Au-delà des composants trouvés au démarrage, component-creator installe et arrête des instances pendant que l'application tourne, à partir d'enregistrements stockés (une classe, ses propriétés, actif ou non). Il apporte aussi des connecteurs prêts à l'emploi (client HTTP, envoi de mails, paiement en ligne, protocoles de messagerie légers), déclarés dans le stockage ou dans la configuration, chacun publié derrière son interface.
# library/shelf.py: an interface, its implementation, its dependencies from abc import ABC from ycappuccino.api.core import IActivityLogger from ycappuccino.api.core_base import YCappuccinoComponent, YCappuccinoType from ycappuccino.api.storage import IManager class IShelf(YCappuccinoComponent, ABC): # published as the service IShelf async def longest(self) -> str: raise NotImplementedError class Shelf(IShelf): def __init__(self, manager: IManager, # required service logger: YCappuccinoType(IActivityLogger, "(name=main)"), # filtered service limit: int = 1): # property self._manager, self._logger, self._limit = manager, logger, limit async def longest(self) -> str: books = await self._manager.get_many("book", {"sort": {"pages": -1}, "limit": self._limit}) return books[0].get_storage_model()["title"] if books else "" async def start(self): self._logger.info("shelf ready") async def stop(self): pass
Modèles, stockage et CRUD
Les modèles : @Item et @Property
Un modèle est une classe annotée : @Item déclare l'élément et sa collection, @Property chaque champ avec ses contraintes de schéma JSON (type, bornes, obligatoire, privé, texte multilingue), @Reference et @References les liens vers d'autres éléments. Le schéma, le catalogue des modèles et les formulaires de la console en découlent ; un modèle peut hériter d'un autre.
Le stockage : MongoDB ou en mémoire
storage persiste les modèles derrière IManager : aujourd'hui MongoDB ou la mémoire (pour le développement et les tests), choisis par un profil inclus dans l'hôte. Il fournit les lectures filtrées, triées et paginées, le chargement des références (expand), les déclencheurs avant et après écriture, le filtre par organisation, les fichiers et le chargement d'un contenu initial (seed). D'autres moteurs s'ajoutent derrière la même interface.
Le CRUD : endpoints_storage
endpoints_storage porte les cas d'usage du CRUD, indépendants de tout transport : ICrud (lire, créer, modifier, supprimer, avec le total pour la pagination), les brouillons nommés, le catalogue des modèles et les fichiers. Chaque action est vérifiée par IAuthorization pour l'appelant, et des règles de champs masquent des champs ou des lignes. Le même ICrud sert l'API REST, les autres processus et le navigateur.
Les services exposés
Une action métier est un IExposedService : un nom et des méthodes marquées @rpc_method (verbe, chemin). endpoints_service les trouve par leur nom, vérifie le droit execute de l'appelant et les appelle, quel que soit le transport.
# library/models.py and library/services.py: a model and an exposed service from ycappuccino.api.decorators import Item, Property, rpc_method from ycappuccino.api.endpoints_service import IExposedService from ycappuccino.api.endpoints_storage import ICrud from ycappuccino.api.models import Model @Item(collection="books", name="book", plural="books") # REST: /api/crud/books class Book(Model): def __init__(self, a_dict=None): super().__init__(a_dict) self._title = None self._pages = None @Property(name="title", maxLength=120, required=True) def title(self, a_value): self._title = a_value @Property(name="pages", type="integer", minimum=0) def pages(self, a_value): self._pages = a_value class Library(IExposedService): # REST: /api/services/library name = "library" def __init__(self, crud: ICrud): self._crud = crud @rpc_method(method="GET", path="/longest") async def longest(self, subject: dict | None) -> dict: # checked with the caller's rights page = await self._crud.get_many("book", {"sort": {"pages": -1}, "limit": 1}, subject=subject) return page["items"][0] if page["items"] else {} @rpc_method(method="POST", path="/books") async def add(self, title: str, pages: int, subject: dict | None) -> dict: return await self._crud.create("book", {"title": title, "pages": pages}, subject=subject) async def start(self): pass async def stop(self): pass
Communication
http_server : l'API REST
http_server monte une seule servlet qui traduit HTTP vers les cas d'usage : /api/crud/<pluriel> pour le CRUD, /api/drafts pour les brouillons, /api/items pour le catalogue et /api/services/<nom> pour les services exposés, dans une enveloppe JSON commune, les erreurs devenant des codes HTTP. Plusieurs fournisseurs de chaque interface se composent : les locaux et ceux des pairs distants répondent côte à côte. L'authentification est un port (IAuthentication) : jeton JWT d'un utilisateur, signature d'un pair.
remote : JSON-RPC sur HTTP et RPC WebSocket
remote relie des instances YCappuccino entre elles. Chaque instance publie ce qu'elle sait faire (ses services, ses modèles, ses interfaces) ; ses pairs le découvrent et créent à la volée, par réflexion, un proxy qui implémente réellement l'interface et transmet chaque appel. Le protocole est léger : JSON-RPC sur HTTP, ou RPC sur une connexion WebSocket persistante (ws, wss), au choix par pair, les appels étant signés (HMAC) avec un secret partagé. Un pair peut aussi s'abonner au flux d'événements WebSocket d'un autre.
hosts : les fronts et les montages
hosts sert les fichiers statiques : des répertoires montés sous un préfixe, avec une priorité, éventuellement protégés ou isolés (COOP/COEP), déclarés dans la configuration ou stockés et rechargés à chaud. Chaque front d'une application composée devient un montage ; plusieurs processus partageant un stockage gardent chacun les leurs, et le service apps liste les fronts et les sections d'administration de l'hôte.
pubsub et collab : le temps réel
pubsub publie des événements par sujets, poussés par WebSocket ou lus par HTTP ; collab s'en sert pour l'édition à plusieurs d'un document stocké : une autorité ordonne les modifications, chacun voit celles des autres en direct, avec la présence et des verrous par partie.
Un hôte réparti sur plusieurs processus
Répartir une application, c'est écrire un hôte par processus : chacun choisit les parties qu'il charge, déclare ses pairs, et ce qu'il n'a pas sur place lui arrive en proxy. Le code des composants ne change pas, seule la configuration. Ici, les données et les services dans un processus, les pages dans un autre, qui lit les enregistrements à travers un proxy d'ICrud :
# conf/data.yml: the storage, the models and the services name: library-data include: [ycappuccino.storage/profiles/mongo.yml] apps: {library: {parts: [models, services]}} bundle_prefix: [ycappuccino.remote.dispatch, ycappuccino.remote.capabilities, ycappuccino.remote.rpc_websocket_handler, ycappuccino.remote.configured_peers, ycappuccino.remote.peer_authentication] components: ConfiguredPeers: {peers: "web", secret: "${remote.secret}"} # web signs its calls config: http_server: {active: true, port: 8081} # conf/web.yml: the pages only, ICrud is a proxy to data (WebSocket RPC) name: library-web include: [ycappuccino.storage/profiles/memory.yml] apps: {library: {parts: [site]}} bundle_prefix: [ycappuccino.remote.configured_peers, ycappuccino.remote.component_directory] components: ConfiguredPeers: {peers: "data=ws://data:8081", secret: "${remote.secret}"} ComponentDirectory: {specifications: "ycappuccino.api.endpoints_storage.ICrud"} config: http_server: {active: true, port: 8080}
Interfaces
ui : des écrans décrits une fois
ui décrit sans code d'affichage les formulaires (champs, validations, actions qui appellent un service), les consoles d'administration (menus, enchaînements d'écrans, CRUD générique d'une collection, droits de l'utilisateur) et les sites (pages, blocs, collections d'enregistrements, langues et textes lus dans le stockage). Ces descriptions sont du YAML, indépendant du rendu : chaque adaptateur les affiche à sa façon.
ui_web : le navigateur
ui_web rend ces descriptions en DOM vivant dans le navigateur : écrans, console d'administration, site, cartes, thème. Aucun HTML n'est généré côté serveur pour les écrans : le rendu se fait en Python, dans la page.
Le client : iPOPO et YCappuccino dans le navigateur
Le client n'est pas une autre technologie : c'est le même framework, iPOPO et les composants YCappuccino, qui tourne en Python dans le navigateur grâce à Pyodide. Un composant du navigateur dépend des interfaces du serveur (ICrud, le service de connexion, toute interface publique) et reçoit des proxys générés par réflexion qui les appellent en JSON-RPC ou en RPC WebSocket, avec l'utilisateur connecté comme appelant. L'outil de construction (build) assemble chaque front client à partir de la configuration de l'hôte : les wheels, la page de démarrage et son descripteur.
ui_static : du HTML statique pour Google
Un client Pyodide se charge en quelques secondes et les moteurs de recherche n'exécutent pas Python. ui_static rend donc le même site en HTML statique, complet et lisible sans JavaScript : une page par langue, plan du site, robots.txt, données structurées et llms.txt, régénérés quand le contenu change. C'est ce que lit Google et ce que voit d'abord un visiteur ; quand Pyodide est prêt, le client reprend la page sans la redessiner, car le balisage des deux rendus est le même.
ui_shell : le terminal
ui_shell rend les mêmes écrans, la même console d'administration et le même site dans un terminal, avec textual : une application console complète, connexion et CRUD compris, sans une ligne d'affichage à écrire.
# library/screens/book.yml: one screen, rendered in the browser and in a terminal title: library.book fields: - {name: title, label: library.title, required: true, max_length: 120} - {name: pages, label: library.pages, type: number, minimum: 0} actions: - {name: save, label: library.save, endpoint: {service: library, method: POST, path: [books]}}
# library/app.json: the browser's application, components started by iPOPO under Pyodide {"name": "library", "wheels": ["ycappuccino-api", "ycappuccino-core", "ycappuccino-client", "ycappuccino-ui", "ycappuccino-ui-web", "library"], "bundles": ["ycappuccino.ui_web.page", "ycappuccino.ui_web.site"], "components": {"PyodidePage": {"mount_selector": "#app"}, "WebSite": {"layout": "library.layouts/site.yml"}}}
Modules prêts à composer
Des modules d'application génériques, à composer dans un hôte comme n'importe quelle application.
Administration : comptes et mots de passe
Le module permissions fournit les comptes, la connexion par mot de passe (haché en scrypt) et les jetons JWT, en en-tête ou en cookie partagé par tous les fronts, les invitations et les réinitialisations de mot de passe par mail. Il apporte la console /admin, qui réunit en une seule page les sections de toutes les applications composées, chaque menu suivant les droits de l'utilisateur.
Rôles et permissions
Les droits s'écrivent action:élément (read:book, execute:library, avec jokers). Le module génère un rôle de base par droit de chaque modèle et de chaque service, et l'on compose des rôles qui en incluent d'autres. Deux rôles intégrés disent ce que peut faire tout le monde (anonymous) et tout utilisateur connecté (authenticated). Des règles de champs masquent des champs ou des lignes selon le rôle ou une condition, et un document se partage avec des membres.
Organisations et multi-tenant
Une organisation est un tenant. Chaque enregistrement appartient à une organisation, et les organisations forment un arbre : un utilisateur voit les données de la sienne et de celles qui en dépendent, ce que le stockage applique à chaque lecture. Certains modèles sont partagés par tous. Les réglages des connecteurs (paiement, cartes…) se donnent par organisation et s'héritent de l'organisation parente.
La géolocalisation
Le module geo ajoute la notion de lieu à n'importe quel modèle, sans le modifier : un lieu (un point GeoJSON) localise un enregistrement de tout élément, un enregistrement peut en avoir plusieurs, et ils sont supprimés avec lui. Une carte montre les enregistrements localisés que l'utilisateur peut lire, une couche par modèle ; des itinéraires relient des lieux dans l'ordre. Le géocodage et le calcul d'itinéraire sont des ports, OpenRouteService étant le premier moteur.
Créer une application
Une application web entièrement en Python : un paquet avec ses modèles et ses composants (ci-dessus), ses écrans et ses pages en YAML, et son descripteur, qui déclare ses parties, ses fronts et sa section d'administration.
# library/ycappuccino_app.yml: the app module app: library label: Library requires_apps: [permissions] # accounts, roles, sign-in, the /admin console parts: models: {bundles: [library.models]} # the @Item classes services: {bundles: [library.services]} # the components and exposed services site: # the public pages, static HTML bundles: [ycappuccino.ui_static] fronts: [site] components: {StaticSiteBuilder: {layout: library.layouts/site.yml, output: public}} fronts: site: {path: /, directory: public} app: {path: /app, client: library/app.json, label: Library, home: true} # the Pyodide client admin: {layout: library.screens/admin.yml, screens: library.screens} # its section of /admin
Un hôte compose l'application avec le stockage voulu. L'outil de construction assemble ses fronts (le site statique, le client Pyodide et ses wheels, la console /admin) puis démarre le processus :
# conf/application.yml: the host name: library-host include: [ycappuccino.storage/profiles/mongo.yml] # or profiles/memory.yml apps: [library] # permissions follows config: http_server: {active: true, port: 8080} $ python -m ycappuccino.build run --host conf/application.yml
Une application console : le même paquet, sans HTTP, rendu dans un terminal. Les mêmes écrans, la même console d'administration et le même site, servis par les mêmes composants :
# conf/console.yml: the same app in a terminal name: library-console include: [ycappuccino.storage/profiles/memory.yml] apps: [library] bundle_prefix: [ycappuccino.ui_shell] components: SiteConsole: layout: library.layouts/site.yml admin: library.screens/admin.yml screens: library.screens key: "${jwt.token.key}" $ python -m ycappuccino.ui_shell --config conf/console.yml
Pour passer d'un processus à plusieurs, on écrit un hôte par processus (voir Communication) : l'application ne change pas.
Python 3.10 ou plus et uv. Chaque module du framework est un dépôt qui publie une wheel à chaque version ; une application déclare celles qu'elle utilise. Un hôte se lance directement, ou par l'outil de construction quand il a des fronts à assembler :
uv pip install wheels/*.whl # the wheels of the repositories used uv run ycappuccino --config conf/application.yml # one process, composed and started python -m ycappuccino.build run --host conf/application.yml # its fronts assembled, then started
Un composant est une classe ordinaire, testable avec des doublures ; pour un test d'intégration, le framework démarre réellement sur une application temporaire, et chaque module fournit des doublures en mémoire.
Sur quoi il repose
Des briques éprouvées, et du Python partout, jusque dans le navigateur.
Serveur
Python 3.10 et plus, iPOPO (Pelix) pour les composants et les services, aiohttp pour HTTP et WebSocket, MongoDB par pymongo pour le stockage, PyJWT pour les jetons, PyYAML pour la configuration.
Navigateur et terminal
Pyodide (CPython compilé en WebAssembly) fait tourner le framework dans le navigateur ; Leaflet et les tuiles OpenStreetMap affichent les cartes ; textual dessine les applications console.
Construction
uv construit une wheel par dépôt ; l'outil de construction du framework assemble les fronts d'un hôte depuis sa seule configuration.
Exemples
Ce site tourne sur YCappuccino : c'est un module d'application dont les textes, en français et en anglais, sont des enregistrements du stockage. ui_static en génère les pages statiques, régénérées quand un texte change dans la console, et hosts les sert.
Un exemple réel construit avec le framework :
Bientôt : un site d'exemple complet, en bac à sable, pour essayer le framework directement en ligne.
© 2026 YCappuccino