Guapy
Embed browser-based remote desktops (RDP, VNC, SSH, Telnet) into your Python
web application. Guapy is a framework-free asyncio library for the Guacamole
protocol and its guacd daemon — no Java webapp, no Node.js sidecar.
It speaks the Guacamole wire protocol end to end: immutable instructions, an
incremental bounded parser, version negotiation, the full guacd handshake,
an asyncio TCP transport, and a mountable ASGI WebSocket tunnel for FastAPI,
Starlette, and Django Channels.
Why Guapy?
Apache Guacamole renders remote desktops in the browser, driven by a small C
daemon called guacd. To use it from a Python application you traditionally
either run Guacamole's full Java webapp (its own auth model, its own database)
or hand-write the protocol glue. Guapy is the missing Python layer:
| Guapy | guacamole-lite (Node) | Apache Guacamole (Java) | |
|---|---|---|---|
| Language | Python | Node.js | Java |
| Async IO | asyncio | event loop | servlet threads |
| Runs inside your app | ✅ ASGI mount | ✅ | ❌ separate webapp |
| Per-connection authorization | ✅ resolver you control | encrypted token | its own DB |
| Runtime dependencies | zero | ws, deep-extend |
servlet container |
| Typed exceptions with HTTP/WS mappings | ✅ | ❌ | ✅ |
Guapy deliberately ships no authentication, token crypto, session registry, or database. Your app authorizes each tunnel through a one-method resolver; Guapy handles everything below that.
Quick start
Mount the tunnel in FastAPI and decide per request which remote host a user gets — the resolver sees the query string, headers (cookies!), and client address:
from fastapi import FastAPI
from guapy import (
GuacamoleConfiguration,
GuacamoleUnauthorizedError,
GuacdEndpoint,
QueryWhitelistResolver,
ResolvedConnection,
TunnelRequest,
)
from guapy.server import GuacamoleASGIApp
app = FastAPI()
class DashboardResolver:
"""Authorize tunnels with your own session machinery."""
async def resolve(self, request: TunnelRequest) -> ResolvedConnection:
session_id = request.headers.get("cookie", "")
if not is_logged_in(session_id): # your application logic
raise GuacamoleUnauthorizedError("sign in first")
vm = lookup_vm_for_user(session_id) # your application logic
return ResolvedConnection(
GuacamoleConfiguration(
protocol="rdp",
parameters={
"hostname": vm.host,
"port": "3389",
"username": vm.username,
"password": vm.password,
},
)
)
tunnel = GuacamoleASGIApp(
endpoint=GuacdEndpoint(host="guacd.internal", port=4822),
resolver=DashboardResolver(),
)
app.mount("/guacamole", tunnel)
Point guacamole-common-js
in the browser at /guacamole/webSocket and the remote desktop renders.
Prefer the safe default? QueryWhitelistResolver wraps a fixed connection and
lets clients override only what you whitelist (display size, color scheme…):
from guapy import QueryWhitelistResolver
resolver = QueryWhitelistResolver(
GuacamoleConfiguration(
protocol="ssh", parameters={"hostname": "bastion.internal", "port": "22"}
),
allowed=("width", "height", "dpi", "color-scheme"),
)
Using the client directly
Guapy also works as a plain guacd client, no web layer involved:
import asyncio
from guapy import GuacamoleClient, GuacamoleConfiguration, GuacdEndpoint, Instruction
async def main() -> None:
configuration = GuacamoleConfiguration(
protocol="ssh",
parameters={
"hostname": "ssh.example.internal",
"port": "22",
"username": "alice",
"password": "provided-by-your-application",
},
)
client = GuacamoleClient()
async with await client.connect(GuacdEndpoint(), configuration) as session:
await session.send(Instruction.create("sync", "0"))
instruction = await session.receive()
if instruction is not None:
print(instruction)
asyncio.run(main())
Applications remain responsible for which configuration is authorized. Never send connection credentials to an untrusted client or log them.
Features
- Immutable Guacamole instructions and a bounded incremental parser that survives arbitrary TCP fragmentation and split UTF-8 code points.
- Complete
guacdhandshake with protocol version negotiation, client capabilities (screen, audio/video/image formats, timezone, name), and support for joining existing sessions by connection ID. - Pluggable transports — asyncio TCP adapter included; TLS to guacd is a constructor flag. Bring your own connector for proxies or recording.
- Ordered instruction filter pipelines for inspecting, rewriting, or dropping instructions on read and write paths.
- Typed status model — every error carries its Guacamole status code and the mapped HTTP status and WebSocket close code, so tunnels close exactly the way the official Java server closes them.
- Production-shaped ASGI tunnel: per-connection resolvers, connection
limits, inactivity timeouts, open/close hooks,
GET /health, ASGI lifespan support, and graceful drain on shutdown.
Guapy requires Python 3.10+ and installs with zero dependencies.
Try it in Docker
The e2e fixture is a complete Guacamole deployment in one command — Guapy,
official guacd, a containerized SSH server (demo/demo), and a browser
client:
docker compose -f e2e/docker-compose.yml up --build
Open http://localhost:9090, select New Connection → SSH →
Connect. The same stack backs the integration tests:
GUAPY_TEST_GUACD_HOST=127.0.0.1 GUAPY_TEST_SSH_HOST=sshd uv run pytest tests/integration
CI runs exactly these tests against real guacd and sshd containers on
every push — the handshake is verified against the actual daemon, not mocks.
Logging
Guapy uses the standard-library logger named guapy and never configures
handlers or levels. It logs lifecycle events (tunnel open/close, protocol
disconnects, guacd EOF) and never logs instructions or credentials.
Development
uv sync --dev
uv run pytest # unit tests (integration tests skip without guacd)
uv run ruff check .
uv run mypy src
See Architecture.md for the design, layering, and the boundaries Guapy deliberately does not cross. Contributions welcome — start with CONTRIBUTING.md.
License
MIT. See LICENSE.
Metadata
Release files for guapy 2.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| guapy-2.0.0.tar.gz | 169.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| guapy-2.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 198.9 kB
Release files / guapy-2.0.0.tar.gz
| Download URL | guapy-2.0.0.tar.gz |
|---|---|
| Size | 169.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7147220bd85e4fab93bade9a3ae35372c2f2f8cb143bfbe131ad131124db31fb
|
|
BLAKE2b-256 checksum How to use checksums |
7c2ff08988b58ec926b86d91a692a8e7e8d2004bfb00ae9012748486785eef27
|
| 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 1, 2026.
Transparency logRelease files / guapy-2.0.0-py3-none-any.whl
| Download URL | guapy-2.0.0-py3-none-any.whl |
|---|---|
| Size | 29.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
9ceeca50b8e5adfdda083ee77b4b349320af6f323824d232b18a280d8b590e6b
|
|
BLAKE2b-256 checksum How to use checksums |
9ddd6189ba65f877f95242e977fc43c68d7a3f5e8db0c2c7712bc8e81369209c
|
| 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 1, 2026.
Transparency log