Home
YCappuccino
Python Component-Based Software Everywhere
YCappuccino is an OSGi-style component framework built on iPOPO. You write ordinary Python classes, annotated models and screens described in YAML; the framework turns them into components that find each other by their interfaces, generates the storage, the API and the user interfaces, and a host assembles it all by configuration, in one process or spread over several. The same framework runs on the server, in a terminal and in the browser.
This page is itself a YCappuccino application: a static site generated by the framework and served by a YCappuccino host.
About / Philosophy
YCappuccino is a personal project, developed in my free time for my music projects, independently of any professional activity.
YCappuccino is a lightweight, modular and experimental Python framework. It grew out of a concrete need: keeping my band Y.Blues alive online — its website, its shop, its tablatures, its calendar — without depending on rigid tools or subscriptions.
Rather than one monolithic block, YCappuccino is a suite of modular Python tools: a CMS for content, a shop for merchandise and albums, a synchronised calendar, an interactive tablature editor. Each brick is an independent component, assembled by configuration alone — in a single process or spread over several containers.
Python Component-Based Software Everywhere — the logo's motto sums up the idea: everything is a Python component. Each feature is a self-contained brick, described by its interfaces, assembled at will; and the same code runs everywhere: on a server, spread over containers, in a terminal, or right in the browser thanks to Pyodide.
It is an open-source project, driven by passion, where web development, content management and music meet.
This very site runs on YCappuccino.
Features
Web management & administration
A secure back office to manage all the content: authentication, roles and fine-grained rights (per record, per field, per organisation).
Media & music tools
An interactive, collaborative tablature editor (Guitar Pro import, playback, real-time editing with others), and a shop for merchandise and albums, physical or digital.
Data & real time
REST API and WebSockets for live updates, map integration to locate concerts and events, Google Calendar synchronisation for dates and rehearsals.
Architecture
A modular, component-based Python engine, declarative data models, flexible storage (in memory or MongoDB), and web, static or console interfaces built on the same foundation.
The principle
OSGi in Python, on iPOPO
YCappuccino builds on iPOPO (Pelix), the Python implementation of the OSGi model. Every feature is a component that publishes services under the names of its interfaces and requires others. The framework binds components dynamically: a component starts once its required dependencies are there and stops if one leaves, a list follows the services as they arrive and leave. A component only knows interfaces, never an implementation: HTTP, WebSocket or the screen are only adapters around it. The Pelix console inspects the running application.
core: classes turned into components
The core walks the declared packages and generates an iPOPO component from every class implementing a YCappuccino interface, without any decorator. The constructor says it all: a parameter typed by an interface is a dependency (required, optional, filtered by the service's properties, or a live list), a parameter with a default value is a property, set in application.yml. start and stop are asynchronous. Layers switch groups of classes on or off, and the ${name:default} values of the configuration come from the environment or a properties file.
api: the shared interfaces
The api package gathers the contracts every module shares, and depends on iPOPO only: the base of the components, logging and configuration, storage (IManager, IStorage, triggers, filters), CRUD (ICrud, IAuthorization), exposed services (IExposedService, @rpc_method), HTTP and WebSocket, publish/subscribe, and the model decorators. The implementations live in the other modules and are swapped without touching the code that uses them.
App modules, parts and hosts
An application is a Python package with a descriptor, ycappuccino_app.yml: its parts (groups of packages and their settings), the applications it requires, its fronts (the addresses it serves) and its section of the administration console. A host is an application.yml that composes the applications it wants and only adds the infrastructure: the storage, the port, the secrets. A host may load only some parts of an application: that is how one application spreads over several processes.
component-creator: components created while running
Beyond the components found at startup, component-creator installs and stops instances while the application runs, from stored records (a class, its properties, active or not). It also brings ready-made connectors (HTTP client, mail, online payment, lightweight messaging protocols), declared in the storage or in the configuration, each one published behind its 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
Models, storage and CRUD
Models: @Item and @Property
A model is an annotated class: @Item declares the item and its collection, @Property each field with its JSON schema constraints (type, bounds, required, private, multilingual text), @Reference and @References the links to other items. The schema, the catalogue of the models and the console's forms follow from it; a model may inherit from another.
Storage: MongoDB or in memory
storage persists the models behind IManager: today MongoDB or memory (for development and tests), chosen by a profile the host includes. It provides filtered, sorted and paged reads, loading of references (expand), triggers before and after a write, the per-organization filter, files and the loading of initial content (seed). Other engines are added behind the same interface.
CRUD: endpoints_storage
endpoints_storage carries the CRUD use cases, independent of any transport: ICrud (read, create, update, delete, with the total for paging), named drafts, the catalogue of the models and files. Every action is checked by IAuthorization for the caller, and field rules hide fields or rows. The same ICrud serves the REST API, the other processes and the browser.
Exposed services
A business action is an IExposedService: a name and methods marked @rpc_method (verb, path). endpoints_service finds them by name, checks the caller's execute right and calls them, whatever the 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: the REST API
http_server mounts a single servlet translating HTTP to the use cases: /api/crud/<plural> for CRUD, /api/drafts for drafts, /api/items for the catalogue and /api/services/<name> for the exposed services, in one JSON envelope, errors becoming HTTP codes. Several providers of each interface compose: the local ones and the remote peers' answer side by side. Authentication is a port (IAuthentication): a user's JWT token, a peer's signature.
remote: JSON-RPC over HTTP and WebSocket RPC
remote links YCappuccino instances together. Every instance publishes what it can do (its services, its models, its interfaces); its peers discover it and create on the fly, by reflection, a proxy that really implements the interface and forwards every call. The protocol is lightweight: JSON-RPC over HTTP, or RPC over a persistent WebSocket connection (ws, wss), chosen per peer, the calls being signed (HMAC) with a shared secret. A peer may also subscribe to another one's WebSocket event stream.
hosts: fronts and mounts
hosts serves static files: directories mounted under a prefix, with a priority, optionally protected or isolated (COOP/COEP), declared in the configuration or stored and reloaded live. Every front of a composed application becomes a mount; several processes sharing one storage each keep their own, and the apps service lists the host's fronts and administration sections.
pubsub and collab: real time
pubsub publishes events by topic, pushed over WebSocket or read over HTTP; collab builds on it for several people editing a stored document together: one authority orders the edits, everyone sees the others' live, with presence and per-part locks.
One host spread over several processes
Spreading an application is writing one host per process: each one picks the parts it loads, declares its peers, and what it lacks locally reaches it as a proxy. The components' code does not change, only the configuration. Here, the data and the services in one process, the pages in another, reading the records through an ICrud proxy:
# 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}
User interfaces
ui: screens described once
ui describes without any display code the forms (fields, validation, actions calling a service), the administration consoles (menus, chained screens, generic CRUD of a collection, the user's rights) and the sites (pages, blocks, collections of records, languages and texts read from the storage). These descriptions are YAML, independent of the rendering: each adapter displays them its own way.
ui_web: the browser
ui_web renders these descriptions as live DOM in the browser: screens, administration console, site, maps, theme. No HTML is generated on the server for the screens: the rendering happens in Python, in the page.
The client: iPOPO and YCappuccino in the browser
The client is not another technology: it is the same framework, iPOPO and the YCappuccino components, running in Python in the browser thanks to Pyodide. A browser component depends on the server's interfaces (ICrud, the sign-in service, any public interface) and receives proxies generated by reflection that call them over JSON-RPC or WebSocket RPC, with the signed-in user as the caller. The build tool assembles every client front from the host's configuration: the wheels, the boot page and its descriptor.
ui_static: static HTML for Google
A Pyodide client takes a few seconds to load and search engines do not run Python. ui_static therefore renders the same site as static HTML, complete and readable without JavaScript: one page per language, sitemap, robots.txt, structured data and llms.txt, rebuilt when the content changes. It is what Google reads and what a visitor sees first; once Pyodide is ready, the client takes the page over without redrawing it, both renderings sharing the same markup.
ui_shell: the terminal
ui_shell renders the same screens, the same administration console and the same site in a terminal, with textual: a complete console application, sign-in and CRUD included, without a line of display code to write.
# 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 ready to compose
Generic app modules, composed in a host like any application.
Administration: accounts and passwords
The permissions module provides the accounts, password sign-in (hashed with scrypt) and JWT tokens, in a header or in a cookie every front shares, invitations and password resets by mail. It brings the /admin console, gathering in one page the sections of every composed application, each menu following the user's rights.
Roles and permissions
Rights are written action:item (read:book, execute:library, with wildcards). The module generates a basic role per right of every model and every service, and roles are composed by including others. Two built-in roles say what everyone may do (anonymous) and what every signed-in user may do (authenticated). Field rules hide fields or rows by role or by a condition, and a document can be shared with members.
Organizations and multi-tenancy
An organization is a tenant. Every record belongs to an organization, and organizations form a tree: a user sees the data of its own and of those below it, which the storage enforces on every read. Some models are shared by all. The connectors' settings (payment, maps…) are given per organization and inherited from the parent organization.
Geolocation
The geo module adds the notion of place to any model, without changing it: a place (a GeoJSON point) locates a record of any item, a record may have several, and they are deleted with it. A map shows the located records the user may read, one layer per model; routes link places in order. Geocoding and routing are ports, OpenRouteService being the first engine.
Building an application
A web application entirely in Python: a package with its models and its components (above), its screens and pages in YAML, and its descriptor, declaring its parts, its fronts and its administration section.
# 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
A host composes the application with the storage it wants. The build tool assembles its fronts (the static site, the Pyodide client and its wheels, the /admin console) then starts the process:
# 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
A console application: the same package, without HTTP, rendered in a terminal. The same screens, the same administration console and the same site, served by the same components:
# 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
To go from one process to several, write one host per process (see Communication): the application does not change.
Python 3.10 or later and uv. Every module of the framework is a repository publishing a wheel with each release; an application declares those it uses. A host starts directly, or through the build tool when it has fronts to assemble:
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
A component is an ordinary class, testable with test doubles; for an integration test, the framework really starts on a temporary application, and each module provides in-memory doubles.
What it builds on
Proven building blocks, and Python everywhere, down to the browser.
Server
Python 3.10 and later, iPOPO (Pelix) for the components and services, aiohttp for HTTP and WebSocket, MongoDB through pymongo for storage, PyJWT for the tokens, PyYAML for the configuration.
Browser and terminal
Pyodide (CPython compiled to WebAssembly) runs the framework in the browser; Leaflet and the OpenStreetMap tiles draw the maps; textual draws the console applications.
Build
uv builds one wheel per repository; the framework's build tool assembles a host's fronts from its configuration alone.
Examples
This site runs on YCappuccino: it is an app module whose texts, in French and English, are records of the storage. ui_static generates its static pages, rebuilt when a text changes in the console, and hosts serves them.
A real example built with the framework:
Coming soon: a complete example site, as a sandbox, to try the framework directly online.
© 2026 YCappuccino