StayPresent
🛖 About
A lightweight Python package designed to keep your bots and background scripts alive by running a dedicated Flask web server alongside your main application(s).
Perfect for deploying on platforms like Render, Railway, Koyeb, Heroku, or any host that requires an active HTTP port to keep your service running.
Contents
- Features
- Installation
- Quickstart
- Usage Guide
- Custom Paths & Multiple Responses
- Running Multiple Bots
- Self-Ping / Keep-Warm
- API Reference
- Logging
- Requirements
- Use Cases
📖 Full Documentation: For complete documentation, API reference, deployment guides, and FAQs, open DOCUMENTATION.md.
✨ Features
- Zero-Friction Setup: Get running with just one line of code.
- Production-Ready by Default: Automatically detects and uses
waitressto avoid Flask's "development server" warnings. - Multiple Bot Support: Run several bot processes side-by-side under one web server, each monitored and restarted independently, with shared or per-bot arguments/environment variables.
- Auto-Restarts & Crash Recovery: Automatically respawns any bot process if it crashes, complete with customizable delays and consecutive-crash budgets — per bot.
- Flexible Responses: Serve custom plain text, JSON (default), full HTML templates, or beautifully rendered Markdown.
- Built-in Markdown Renderer — Zero Extra Dependencies: Headings, emphasis, strikethrough, links, images, autolinks, nested lists, nested blockquotes, tables, fenced/indented code blocks, raw HTML passthrough, and a GitHub-flavored stylesheet — no
markdownpackage required. - Light / Dark / Auto Theming: Every Markdown page can be forced to
lightordark, or left onauto(default) to automatically follow the visitor's own OS/browser preference. - Page Metadata for Markdown Pages: Set a custom favicon,
<title>, and meta/Open Graph description directly from Python — no template editing needed. - Custom Paths, Multiple Responses: Host more than one response at once at different paths (e.g.
/,/status,/dashboard) — handy for multi-bot setups where each bot gets its own endpoint. - Static Asset Serving: Automatically serves CSS, JS, images, and favicons located next to your HTML/Markdown files, scoped correctly even when several are hosted at different paths.
- Advanced Process Control: Easily pass custom command-line arguments and environment variables directly to your bot process(es), shared across all of them or configured individually per bot.
- Fail-Safe Logging: Logs a clear, dedicated error if the underlying web server dies unexpectedly, and never touches Python's root logger.
- Optional Self-Ping / Keep-Warm: Periodically ping your own public URL in the background to prevent free-tier hosts from spinning your service down due to inactivity — fully opt-in, off by default.
📦 Installation
Standard Installation:
pip install staypresent
Production Installation (Recommended):
To suppress Flask's development-server warning and use a production-grade WSGI server, install the prod extra. This automatically provisions waitress.
pip install staypresent[prod]
(Note: If waitress isn't installed, StayPresent gracefully falls back to Flask's built-in development server and logs a one-time warning. Nothing else is required — Markdown rendering, tables, theming, and everything else described below works out of the box with no additional packages.)
🚀 Quickstart
import staypresent
staypresent.run("bot.py")
That's it. This starts a background web server (defaulting to 0.0.0.0:8080, serving {"message": "I'm Present"} at /) and runs bot.py alongside it, automatically restarting it if it ever crashes.
📘 Usage Guide
By default, if you don't configure anything via staypresent.web, the root path (/) returns a JSON response: {"message": "I'm Present"}. Every function below accepts an optional path argument to host more than one response at once — see Custom Paths & Multiple Responses.
Text Response
import staypresent
staypresent.web.text("Service is Online")
staypresent.run("bot.py")
JSON Response
import staypresent
staypresent.web.json({
"status": "online",
"uptime": "24h"
})
staypresent.run("bot.py")
HTML Response
Serve a full HTML file. Any CSS/JS/images referenced next to it are served automatically.
import staypresent
staypresent.web.html("template/index.html")
staypresent.run("bot.py")
Markdown Response
Serve a .md file, rendered to clean, styled HTML — headings, emphasis, links, images, lists, blockquotes, tables, and fenced code blocks are all supported out of the box, with no extra dependency required. The file is re-read (and re-rendered) fresh on every request, so editing it on disk shows up immediately.
import staypresent
staypresent.web.markdown("CHANGELOG.md")
staypresent.run("bot.py")
The rendered page uses a GitHub-flavored stylesheet and automatically matches the visitor's light/dark preference. You can also customize the theme and page metadata:
import staypresent
staypresent.web.markdown(
"docs/guide.md",
path="/docs",
mode="dark", # "light", "dark", or "auto" (default)
favicon="favicon.png", # a file next to guide.md, or a direct URL
title="Project Docs", # page <title> + Open Graph title
description="Everything you need to get started.", # meta + Open Graph description
)
staypresent.run("bot.py")
| Parameter | Default | Description |
|---|---|---|
mode |
"auto" |
"auto" follows the visitor's OS/browser color-scheme preference automatically. "light"/"dark" force that scheme for every visitor regardless of their own setting. |
favicon |
None |
A direct URL (http://, https://, or //...) is used as-is. Anything else (e.g. "favicon.png") is treated as a file next to your Markdown file — the same way neighboring CSS/images already are — and must exist there to be served correctly. |
title |
None |
Sets the page <title> and Open Graph title. Defaults to the Markdown file's own filename when omitted. |
description |
None |
Adds a <meta name="description"> tag and an Open Graph description tag — useful for link-preview cards when the URL is shared on social media/chat apps. |
Files (images, etc.) next to your .md file are served automatically, exactly the same as with html().
Custom Host, Port, and Threads (Complete Example)
import staypresent
staypresent.web.json({"status": "running"})
staypresent.run(
"bot.py",
host="0.0.0.0",
port=5000,
threads=8
)
🧭 Custom Paths & Multiple Responses
Every staypresent.web function (text, json, html, markdown) accepts an optional path argument. It defaults to "/", matching the original behavior — but you can host as many independent responses as you like, each at its own path, all served by the same web server.
import staypresent
staypresent.web.json({"status": "online"}) # served at "/"
staypresent.web.text("bot #2 is alive", path="/bot2") # served at "/bot2"
staypresent.web.html("dashboard.html", path="/dashboard") # served at "/dashboard/"
staypresent.web.markdown("CHANGELOG.md", path="/changelog") # served at "/changelog/"
staypresent.run("bot.py")
Note on trailing slashes: for
html()/markdown()at any path other than"/", StayPresent automatically redirects/dashboard→/dashboard/. This isn't optional — it's what makes relative asset links inside your file (<link href="style.css">,<img src="images/logo.png">, a relativefavicon) resolve correctly against that file's own directory instead of its parent.text()/json()responses don't need this since they have no static assets to resolve.
A handful of small helpers make working with multiple paths easier:
staypresent.web.paths() # -> ['/', '/bot2', '/changelog', '/dashboard']
staypresent.web.get_all() # -> {'/': {...}, '/bot2': {...}, ...}
staypresent.web.get("/bot2") # -> {'type': 'text', 'value': 'bot #2 is alive'}
staypresent.web.remove("/bot2") # stop hosting a response, returns True/False
Note on
/health: StayPresent has a built-in default at/healthreturning{"status": "ok"}(see Built-in Health Check below). It's a default, not a reservation — if you register your own response at/healthviastaypresent.web, yours is served instead.
🤖 Running Multiple Bots
staypresent.run() can launch and independently supervise more than one bot process at once, all behind the same web server.
Same args/env for every bot
Pass a list of file paths instead of a single one:
import staypresent
staypresent.run(["telegram_bot.py", "discord_bot.py"])
Each bot is monitored and restarted on its own — one crashing (and getting restarted, per max_restarts/restart_delay) has no effect on the others. bot_args and env, if provided, are applied identically to every bot in the list.
Per-bot args/env
For finer control, use the bots argument instead — a list of dicts, one per bot:
import staypresent
staypresent.web.json({"status": "online"})
staypresent.run(bots=[
{"file": "telegram_bot.py", "args": ["--verbose"]},
{"file": "discord_bot.py", "env": {"SHARD": "0"}},
{"file": "worker.py"},
])
bots is mutually exclusive with bot_file/bot_args/env — pick whichever style fits: bot_file (+ optional shared bot_args/env) for the simple case, bots when each process needs its own arguments or environment.
How failures are handled with multiple bots
- Each bot has its own independent restart counter, so
max_restartsis a per-bot budget. staypresent.run()waits for every bot to finish before returning or exiting — it doesn't stop supervising the others just because one of them gave up.- If any bot ultimately fails to stay up (restarts exhausted, or
restart_on_crash=Falseand it crashed),staypresent.run()exits the whole process with a non-zero exit code once every bot has finished, the same fail-loud behavior as the single-bot case. Ctrl+C/SIGTERMstops the web server and all bot processes cleanly.
📡 Self-Ping / Keep-Warm
Some free-tier hosts spin your service down after a period of inactivity, even if staypresent.run() has an open port. staypresent.ping()/staypresent.cron() let you periodically hit your own public URL to keep it warm — entirely optional and off by default.
One-off ping
import staypresent
result = staypresent.ping("https://my-bot.onrender.com")
print(result)
# {'url': 'https://my-bot.onrender.com/', 'ok': True, 'status_code': 200, 'elapsed': 0.42, 'error': None}
Recurring keep-warm pings
import staypresent
staypresent.web.json({"status": "online"})
handle = staypresent.cron("https://my-bot.onrender.com", interval=300) # every 5 minutes
staypresent.run("bot.py")
cron() returns a CronHandle you can use to stop it later:
handle.stop() # stop the background pinger
handle.is_running # True/False
You can also pass on_success/on_failure callbacks to react to each ping's result (e.g. for your own logging/metrics), and pass host="127.0.0.1", port=8080 instead of a full URL if you just want to ping your own local server.
⚙️ API Reference
staypresent.run(...)
Launch your bot script(s) alongside the web server.
| Parameter | Type | Default | Description |
|---|---|---|---|
bot_file |
str or list[str] |
None |
Path to the Python script to run, or a list of paths to run several bots at once. Mutually exclusive with bots. |
host |
str |
"0.0.0.0" |
Host to bind the web server to. |
port |
int |
8080 |
Port to bind the web server to. |
production |
bool |
True |
Uses waitress if installed. Set to False to force the Flask dev server. |
threads |
int |
4 |
Number of worker threads for waitress. Increase this if serving real web traffic rather than just keep-alive pings. (Requires production=True and waitress). |
restart_on_crash |
bool |
True |
Relaunch a bot process if it exits with a non-zero exit code. |
max_restarts |
int |
5 |
Maximum restart attempts per bot after a crash before giving up. |
restart_delay |
float |
2.0 |
Seconds to wait before relaunching a bot process after a crash. |
restart_reset_after |
float |
60.0 |
Seconds a bot must stay alive to reset its consecutive crash counter back to 0. |
bot_args |
list |
None |
Extra command-line arguments passed to every bot in bot_file (e.g., ["--verbose"]). Must be a list — a bare string like "--flag" raises a clear error instead of silently exploding into individual characters. Ignored when bots is used. |
env |
dict |
None |
Extra environment variables for every bot in bot_file. Merges over the current environment. Ignored when bots is used. |
bots |
list[dict] |
None |
Per-bot configuration: [{"file": "bot.py", "args": [...], "env": {...}}, ...] (args/env optional per entry). Mutually exclusive with bot_file/bot_args/env — see Running Multiple Bots. |
Note:
port,threads,max_restarts,restart_delay, andrestart_reset_afterare validated up front — passing an invalid value (e.g.threads=0, a negativeport) raises aValueErrorimmediately instead of failing silently or deep insidewaitress. Likewise, every bot file (inbot_fileorbots) is checked to exist before the server starts.
Crash Recovery Details
StayPresent automatically monitors every bot process. If one exits with a non-zero exit code, StayPresent restarts it based on your configuration:
- Clean Exits: An exit code of
0is considered intentional and will not trigger a restart. - Independent Supervision: With multiple bots, each one is monitored and restarted completely independently — one crashing (or exhausting its restarts) doesn't pause or stop the others.
- Manual Shutdowns: Stopping StayPresent via
Ctrl+C(SIGINT) orSIGTERMshuts down the server and every bot process cleanly. - Smart Counters: The
max_restartslimit applies to consecutive crashes, per bot. If a bot runs successfully for the duration ofrestart_reset_after(default 60 seconds), its crash counter resets. - Non-Zero Exit on Giving Up:
staypresent.run()waits for every bot to finish. If any bot ultimately failed to stay up — restarts disabled and it crashed, ormax_restartswas exhausted for it —staypresent.run()then exits the whole process with a non-zero exit code instead of returning normally. This lets a hosting platform's own restart-on-crash policy (Render, Railway, Docker, systemd, etc.) kick in as a last resort, instead of the process quietly exiting0as if nothing went wrong.
staypresent.web
| Function | Description |
|---|---|
text(message, path="/") |
Serve plain text at path. |
json(data, path="/") |
Serve a JSON-serializable dict/list at path. A deep copy is stored. |
html(file_path, path="/") |
Serve an HTML file (read fresh every request) at path, plus its neighboring static assets. |
markdown(file_path, path="/", mode="auto", favicon=None, title=None, description=None) |
Serve a Markdown file, rendered to styled HTML (read + re-rendered fresh every request) at path, plus its neighboring static assets. See Markdown Response for the theming/metadata parameters. |
remove(path="/") |
Stop hosting a response at path. Returns True/False. |
get(path="/") |
Returns {"type": ..., "value": ...} (plus mode/favicon/title/description for a Markdown entry) for path, or {} if nothing is registered there. |
get_all() |
Returns every registered path and its state as a single dict. |
paths() |
Returns a sorted list of every currently-registered path. |
Built-in Health Check
A default /health endpoint is available out of the box, returning {"status": "ok"}. This is incredibly useful for platform pingers and uptime monitors that require a dedicated health-check path separate from your regular response(s). It's a default, not a reservation — registering your own response at /health via staypresent.web overrides it.
staypresent.ping(...) / staypresent.cron(...)
| Function | Description |
|---|---|
ping(host, port=None, path="/", timeout=10.0, https=None) |
Send a single HTTP request and return {"url", "ok", "status_code", "elapsed", "error"}. |
cron(host, port=None, path="/", interval=300.0, repeat=True, timeout=10.0, https=None, on_success=None, on_failure=None) |
Start a background thread that pings a URL on a schedule. Returns a CronHandle (.stop(), .is_running). |
🪵 Logging
StayPresent logs to its own "staypresent" logger (via a dedicated StreamHandler), never touching Python's root logger — so it won't clobber, duplicate, or reformat log output your bot script has already configured for its own loggers. Startup, restarts, crashes, and shutdowns are all logged with timestamps at the appropriate level (INFO/WARNING/ERROR).
🛠 Requirements
- Python 3.8+
- Flask
waitress(optional, but highly recommended for production —pip install staypresent[prod])
Markdown rendering, theming, tables, and everything else in this README works with no additional dependencies — StayPresent ships its own built-in Markdown-to-HTML renderer.
💡 Use Cases
- Keeping a Discord/Telegram/Slack bot alive on a free-tier host that requires an open HTTP port.
- Running several bots (e.g. a Telegram bot and a Discord bot) from a single deployed service.
- Exposing a lightweight status page, uptime dashboard, or
CHANGELOG.md/README.mdviewer for a background worker. - Giving a hosting platform's health-check probe something to hit while your real work happens in a separate process.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file staypresent-1.5.4.tar.gz.
File metadata
- Download URL: staypresent-1.5.4.tar.gz
- Upload date:
- Size: 39.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4cb0f1cc2487ab64d06ae3c0694fbb4ddf95cf2e7dc7126cea5431aa1bb883e6
|
|
| MD5 |
c588e8124ce33730154bb93f8430a102
|
|
| BLAKE2b-256 |
23d28b7979bde1c7796fdb009c7e8cdc3fcc17aba7932f832f58686cc89c4114
|
Provenance
The following attestation bundles were made for staypresent-1.5.4.tar.gz:
Publisher:
python-publish.yml on StayElite/StayPresent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
staypresent-1.5.4.tar.gz -
Subject digest:
4cb0f1cc2487ab64d06ae3c0694fbb4ddf95cf2e7dc7126cea5431aa1bb883e6 - Sigstore transparency entry: 2267100739
- Sigstore integration time:
-
Permalink:
StayElite/StayPresent@0d33e183a2929c3f102d4ad8054c968ce88b669f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/StayElite
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@0d33e183a2929c3f102d4ad8054c968ce88b669f -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file staypresent-1.5.4-py3-none-any.whl.
File metadata
- Download URL: staypresent-1.5.4-py3-none-any.whl
- Upload date:
- Size: 33.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/6.1.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ec19ab1a9deb27d9d081bb6a76f43e2848a3c1032c98dbeeedc3bf4bc3da44df
|
|
| MD5 |
549780b76d80436175b54f0dbd54ef7a
|
|
| BLAKE2b-256 |
c77bc324387eedc6f5a2d9eae277414753a95eb8c49d7fc35d8433bad490440c
|
Provenance
The following attestation bundles were made for staypresent-1.5.4-py3-none-any.whl:
Publisher:
python-publish.yml on StayElite/StayPresent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
staypresent-1.5.4-py3-none-any.whl -
Subject digest:
ec19ab1a9deb27d9d081bb6a76f43e2848a3c1032c98dbeeedc3bf4bc3da44df - Sigstore transparency entry: 2267101098
- Sigstore integration time:
-
Permalink:
StayElite/StayPresent@0d33e183a2929c3f102d4ad8054c968ce88b669f -
Branch / Tag:
refs/heads/main - Owner: https://github.com/StayElite
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@0d33e183a2929c3f102d4ad8054c968ce88b669f -
Trigger Event:
workflow_dispatch
-
Statement type: