sillydev
The Python client for the Silly Development panel API: start, stop and
restart your bots, send console commands, manage files and backups, and deploy from Git, all from
your own code. It also comes with a sillydev command line tool, an MCP server for AI assistants
and opt-in application monitoring.
Full API docs: https://sillydev.co.uk/docs/panel
Install
pip install sillydev
Python 3.9 or later. The only dependency is httpx. For the
console's websocket, add the console extra (pip install 'sillydev[console]'); without it the
console streams or polls over plain HTTPS instead.
Quick start
Create an API key in the panel under Account, API keys and put it in the SILLYDEV_API_KEY
environment variable. Keys start with sdevc_ (older ptlc_ keys keep working).
from sillydev import SillyDev
client = SillyDev() # or SillyDev(api_key="sdevc_...")
for server in client.panel.servers.list():
print(server["identifier"], server["name"])
client.panel.servers.power("1a7ce997", "restart")
client.panel.servers.command("1a7ce997", "say Back in a minute")
Each server has an 8-character identifier, shown in the panel and in the identifier field of
every server. Every method takes it (or the full UUID) as its first argument.
Examples
from sillydev import SillyDev
client = SillyDev()
panel = client.panel
server = "1a7ce997"
# Live state and resource use
stats = panel.servers.resources(server)
print(stats["current_state"], stats["resources"]["memory_bytes"])
# Restart and wait until it's running again (raises WaitTimeoutError after 120 seconds)
panel.servers.restart(server)
panel.servers.wait_for_state(server, "running", timeout=120)
# Files
for f in panel.files.list(server, directory="/"):
print(f["name"], f["size"])
config = panel.files.read(server, "/config.json")
panel.files.write(server, "/config.json", config.replace("debug", "info"))
# Backups
backup = panel.backups.create(server, name="before-update")
print(backup["uuid"])
# Deploy the linked Git repository
deployment = panel.git.deploy(server)
print(deployment["status"]) # "queued"
# Startup variables
panel.startup.set_variable(server, "MAIN_FILE", "bot.py")
Responses
Methods return plain dicts and lists. The API's {"object": ..., "attributes": {...}} envelopes
are removed, so panel.servers.get(server)["name"] just works.
Pagination
panel.servers.list(), panel.backups.list(), panel.servers.activity() and
panel.account.activity() return iterators that fetch the next page as you go. For one page at a
time, with the list's meta, use the *_page methods:
page = panel.servers.list_page(page=1, per_page=25)
print(page.total, page.has_next, page.meta.get("pending_orders"))
Async
import asyncio
from sillydev import AsyncSillyDev
async def main():
async with AsyncSillyDev() as client:
async for server in client.panel.servers.list():
print(server["name"])
await client.panel.servers.restart("1a7ce997")
asyncio.run(main())
Errors
Every error is a sillydev.SillyDevError. Errors the API answers with have their own class:
| Status | Exception |
|---|---|
| 400 | BadRequestError |
| 401 | AuthenticationError |
| 403 | PermissionDeniedError |
| 404 | NotFoundError |
| 409 | ConflictError |
| 422 | ValidationError (.errors has every problem, .fields groups them by field) |
| 429 | RateLimitError (.retry_after in seconds) |
| 5xx | ServerError |
If the API cannot be reached at all you get APIConnectionError.
from sillydev import ConflictError, NotFoundError
try:
panel.servers.restart(server)
except ConflictError:
print("The server is installing or being transferred; try again later")
except NotFoundError:
print("No such server, or the key cannot reach it")
Retries and limits
Each account may make 768 requests a minute. Requests answered 429 are retried
twice by default, and reads answered 502, 503 or 504 too (a command or power action is not
repeated, since the server may already have run it), waiting as long as the Retry-After header asks. Change it with
SillyDev(max_retries=...); 0 turns retries off.
Ordering a paid server (panel.billing.checkout(...)) always sends an Idempotency-Key, so a
retried request never buys twice. Pass idempotency_key= yourself to make retries safe across runs.
Everything else
client.panel has one attribute for each area of the API:
| Attribute | What it covers |
|---|---|
servers |
list, get, delete, resources, activity, permissions, power, command, share logs, console credentials and relay |
console |
the live console and its recent output (below) |
settings |
rename, reinstall, Docker image, status badge |
startup |
startup command and variables |
files |
list, read, write, rename, copy, compress, decompress, delete, folders, chmod, pull, signed URLs |
backups |
list, create, get, delete, download, lock, restore |
schedules |
schedules and their tasks |
network |
allocations (ports) |
subusers |
other accounts with access to a server |
git |
link a repository, deploy, deployments, sillydev.conf |
account |
the key's account and its activity |
server_creation |
create free servers |
databases |
free MariaDB and Postgres databases |
billing |
paid servers and subscriptions |
monitoring |
monitoring projects, keys, issues, events and usage |
Every method's docstring names the API operation it calls, so you can look it up in the API reference.
Console
client.panel.console connects to a server's live console: its output, state and resource use as
they happen, with commands and power actions sent back.
with client.panel.console.connect("1a7ce997") as console:
console.command("say Hello")
for message in console: # Every message since the node accepted the token
if message.event == "console output":
print(message.args[0])
console.messages(timeout=5) stops after five quiet seconds instead of waiting for ever.
Listeners work too: console.on("console output", lambda m: print(m.args[0])). With the sync
client the connection runs on a background thread, which is where listeners are called. With
AsyncSillyDev it is a coroutine away: console = await client.panel.console.connect(...), then
async for message in console, await console.command(...) and await console.close().
The recent output (about the last 150 lines; empty while the server is offline):
lines = client.panel.console.history("1a7ce997")
There are four ways to reach a console, and by default (transport="auto") each is tried in turn
until one works:
transport |
How |
|---|---|
websocket |
A websocket to the server's node. The quickest. Needs sillydev[console]; without it auto skips it. |
stream |
The same messages streamed over plain HTTPS from a session on the node, for networks that block websockets. |
poll |
The same session read by long polling, for proxies that hold streams back. |
relay |
The panel reads a session on the node for you, for networks that can't reach the node at all. A second or two slower. |
Name one, as in connect(server, transport="poll"), to use only that. console.transport says
which is in use. Messages are the websocket's on every way: ConsoleMessage(event, args) with
events such as console output, status, stats (args[0] is JSON), daemon message and
install output; see the API reference for the full
list. send(event, *args) sends any of them.
The connection renews its token before it expires and reconnects (trying each way again) if it
drops, up to max_reconnect_attempts times in a row (default 5; reconnect=False turns it off).
When it gives up, or the server is suspended or deleted, the console ends with a ConsoleError
whose reason says why (suspended, booting, token, timeout, network, deleted,
throttled or unavailable): iterating raises it, on_close(listener) receives it, and
console.close_reason holds it. When connect() itself fails with auto, error.attempts says
why each way failed.
A console counts towards the 30 console connections a server may have open, and connecting gets a token from the API (limited to 5 a minute per server), so keep one open rather than connecting again and again.
Command line
export SILLYDEV_API_KEY="sdevc_..."
sillydev servers list
sillydev status 1a7ce997
sillydev power 1a7ce997 restart
sillydev command 1a7ce997 "say hi"
sillydev logs 1a7ce997
sillydev console 1a7ce997
sillydev deploy 1a7ce997
sillydev files ls 1a7ce997 /src
sillydev files cat 1a7ce997 /config.json
sillydev backups list 1a7ce997
sillydev backups create 1a7ce997 --name before-update
Add --json to any command for JSON output. python -m sillydev works too. console follows the
console live and sends each line you type as a command; Ctrl+C stops it. logs and console take
--transport (as above).
MCP server
The MCP server lets an AI assistant (Claude Desktop, Claude Code, Cursor and others) check on and control your servers. It needs Python 3.10 or later:
pip install 'sillydev[mcp]'
Then add it to your assistant's MCP settings, for example:
{
"mcpServers": {
"sillydev": {
"command": "sillydev-mcp",
"env": { "SILLYDEV_API_KEY": "sdevc_..." }
}
}
}
Its tools are: list servers, server status, power action, send command, read console (recent output), list files, read file, Git deploy and list backups. There is nothing for billing or for deleting anything. Give it a key limited to the servers and permissions you want the assistant to have.
Monitoring
sillydev.monitoring sends your application's metrics, errors, events and slow operations to
Silly Development monitoring. It is opt-in: using
the SDK never sends monitoring data by itself. import sillydev does not load it, and importing
sillydev.monitoring starts no thread, installs no hook and reads no environment variable. Only
monitoring.init() does, and only when it is given a key.
- In the panel, create a monitoring project and a server key (
sdevm_<n>_…, shown once). - Put it in
SILLYDEV_MONITORING_KEY(servers linked to a project get it automatically) and callinit()once at start-up:
from sillydev import monitoring
monitoring.init() # reads SILLYDEV_MONITORING_KEY; without a key it does nothing
monitoring.increment("orders.created", tags={"plan": "free"})
monitoring.gauge("queue.size", 12)
monitoring.timing("payments.charge", 182.5) # milliseconds
with monitoring.timer("reports.render"):
render()
@monitoring.timed # sync or async functions
async def sync_inventory(): ...
monitoring.event("deploy", {"version": "1.4.2"})
monitoring.record_slow("nightly-export", 42_000, kind="task")
try:
risky()
except Exception:
monitoring.capture_exception(tags={"job": "import"}) # the exception being handled
monitoring.capture_message("Disk almost full", level="warning")
Every function is a no-op that never raises until init() has a key, so libraries can call them
freely. Unhandled exceptions (sys.excepthook, threading.excepthook and the asyncio exception
handler of instrumented loops) are reported and then passed to the previous hook, so tracebacks are
still printed and the process still exits as before.
Metrics are added up in 10-second windows in your process and sent by a background thread every
flush_interval seconds (gzipped, at most 500 items a request), along with process metrics:
process.cpu.percent, process.memory.rss, process.threads, process.uptime,
runtime.gc.count, runtime.gc.pause and, for asyncio apps, runtime.event_loop.lag (the loop
running when init() is called, or any loop passed to monitoring.instrument_loop(loop)).
Buffered data is sent on exit (for at most 2 seconds). If the ingest is down or rate limits, data is
kept (up to max_buffer_bytes, oldest dropped first) and retried with backoff; if the key is
revoked (401/403) the SDK logs one warning and stops.
| Option | Environment variable | Default |
|---|---|---|
key |
SILLYDEV_MONITORING_KEY |
none: monitoring stays off |
endpoint |
SILLYDEV_MONITORING_URL |
https://ingest<n>.sillydev.co.uk, from the key |
service |
SILLYDEV_MONITORING_SERVICE |
default |
environment |
SILLYDEV_MONITORING_ENVIRONMENT |
production |
release |
SILLYDEV_MONITORING_RELEASE |
none |
enabled |
SILLYDEV_MONITORING_DISABLED=1 turns it off |
True |
runtime_metrics |
True |
|
capture_unhandled |
True |
|
sample_rate |
1.0 (share of errors and events kept; metrics are never sampled) |
|
before_send |
none: before_send(item_type, item) returns the item, changed or not, or None to drop it ("error", "event", "slow") |
|
flush_interval |
10 seconds | |
max_buffer_bytes |
1,000,000 | |
slow_threshold_ms |
1000 | |
debug |
False (logs every payload on the sillydev.monitoring logger) |
Privacy. No local variables are ever sent. Tag, context and attribute keys that look like
password, secret, token, authorization, cookie or api_key have their values replaced
with [redacted]. Use before_send to remove anything else.
Integrations
Each one is imported on its own and needs nothing installed beyond the framework you already use:
# FastAPI / Starlette: http.server.duration by route template, method and status class
from sillydev.monitoring.asgi import MonitoringMiddleware
app.add_middleware(MonitoringMiddleware)
# Flask (or Django: wrap get_wsgi_application(), and add
# "sillydev.monitoring.wsgi.DjangoRouteMiddleware" to MIDDLEWARE for route templates)
from sillydev.monitoring.wsgi import MonitoringMiddleware
app.wsgi_app = MonitoringMiddleware(app.wsgi_app)
# aiohttp
from sillydev.monitoring.aiohttp import middleware
app = web.Application(middlewares=[middleware])
# discord.py: discord.command.duration (command, outcome), command and event errors,
# discord.gateway.latency and discord.guilds
from sillydev.monitoring.discord import monitor_bot
bot = monitor_bot(commands.Bot(command_prefix="!", intents=intents))
Requests and commands slower than slow_threshold_ms are also reported as slow operations.
Keep routes few: each distinct route (and tag value) is a separate series, and projects have a
series limit. Paths without a route template have their ids replaced with :id.
Configuration
| Option | Environment variable | Default |
|---|---|---|
api_key |
SILLYDEV_API_KEY |
(required) |
base_url |
SILLYDEV_BASE_URL |
https://sillydev.co.uk/panel/api/client |
timeout |
60 seconds | |
max_retries |
2 |
License
MIT
Metadata
Release files for sillydev 1.2.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 | Provenance | |
|---|---|---|---|---|
| sillydev-1.2.0.tar.gz | 71.0 kB | With provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| sillydev-1.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 154.3 kB
Release files / sillydev-1.2.0.tar.gz
| Download URL | sillydev-1.2.0.tar.gz |
|---|---|
| Size | 71.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
40ca2d1603f892bbbd17dc171d27022ec6f512bf3f80d273a3ad8f4af66b2784
|
|
BLAKE2b-256 checksum How to use checksums |
efaedff46b618d7f24ee618d8a2ec56bb66424ddd714c1105cb223c9b3f20d42
|
| 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 Oct 22, 2026.
Transparency logRelease files / sillydev-1.2.0-py3-none-any.whl
| Download URL | sillydev-1.2.0-py3-none-any.whl |
|---|---|
| Size | 83.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
507afd062e33b19c4379f688442dc6ab686655829ba87f5f7d44f8e643d6671b
|
|
BLAKE2b-256 checksum How to use checksums |
b0cd97129bb981b7707e4e3faea31737f8608576f53907fdf36a4291e8ba096b
|
| 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 Oct 22, 2026.
Transparency logSecurity for sillydev 1.2.0
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.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for sillydev-1.2.0.tar.gz
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 Oct 22, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.