Skip to main content

StayPresent

StayPresent Logo

PyPI version Python versions License: MIT

Downloads per week Downloads per month Total downloads

🛖 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


📖 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 waitress to 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.
  • Package-Aware Bot Launching: Bots that live inside a package and rely on relative imports can be launched as a proper module (python -m mypkg.bot) via bot_module, instead of failing the way a bare python bot.py would.
  • 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 markdown package required.
  • Light / Dark / Auto Theming: Every Markdown page can be forced to light or dark, or left on auto (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 relative favicon) 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 /health returning {"status": "ok"} (see Built-in Health Check below). It's a default, not a reservation — if you register your own response at /health via staypresent.web, yours is served instead.

Note on registering the same path twice: calling text()/json()/html()/markdown() again for a path you've already registered is a normal way to update it (e.g. calling json() repeatedly to refresh a status payload) — the newest call always wins, silently. But if the response type at a path changes (e.g. it was json and a later call registers text there instead), that's usually a sign two different bots — or two different parts of your code — didn't realize they were both claiming the same path, so StayPresent logs a one-line warning to make that visible instead of just quietly serving whichever one happened to run last.


🤖 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.

Note on bots with the same filename: if two bot files share a filename (e.g. shard_a/bot.py and shard_b/bot.py), they run as fully independent processes with no conflict — StayPresent tracks each by its position in the list, not its name. The only thing that changes is the log labels: instead of two ambiguous bot[0] 'bot.py'/bot[1] 'bot.py' lines, StayPresent automatically switches to showing each one's full path (e.g. bot[0] 'shard_a/bot.py') whenever a filename collision is detected, so crash/restart logs always tell them apart.

Bots that live inside a package (bot_module)

If your bot isn't a standalone script — it's a module inside a package that uses package-relative imports (from . import something) — running it via bot_file fails exactly the way python bot.py would from the command line:

ImportError: attempted relative import with no known parent package

Use bot_module instead, which launches it as python -m <module> (exactly like running it yourself from the command line):

import staypresent

staypresent.run(bot_module="mypkg.bot")

It works the same way as bot_file everywhere else — pass a list for multiple bots, use bot_args/env for shared configuration, or mix file- and module-based bots together with per-bot config via bots:

import staypresent

staypresent.run(bots=[
    {"file": "telegram_bot.py", "args": ["--verbose"]},        # a standalone script
    {"module": "discord_bot.worker", "env": {"SHARD": "0"}},   # a module inside a package
])

bot_module/"module" is mutually exclusive with bot_file/"file" — pass exactly one per bot. Unlike bot_file, a module path isn't checked for existence up front (verifying that safely would require importing it, which StayPresent deliberately avoids as a side effect) — a typo'd or missing module simply surfaces as that bot exiting non-zero, handled by the normal crash/restart logic above.

How failures are handled with multiple bots

  • Each bot has its own independent restart counter, so max_restarts is 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=False and 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 / SIGTERM stops 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. Launched as python <file> .... Mutually exclusive with bot_module and with bots.
bot_module str or list[str] None Dotted module path (e.g. "mypkg.bot") to run instead of a bare script, or a list of them. Launched as python -m <module> .... Use this when your bot lives inside a package and needs package-relative imports — see Bots that live inside a package. Mutually exclusive with bot_file and 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/bot_module (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/bot_module. Merges over the current environment. Ignored when bots is used.
bots list[dict] None Per-bot configuration: [{"file": "bot.py", ...}, {"module": "mypkg.bot", ...}, ...] — each entry needs exactly one of "file"/"module", plus optional "args"/"env". Mutually exclusive with bot_file/bot_module/bot_args/env — see Running Multiple Bots.

Note: port, threads, max_restarts, restart_delay, and restart_reset_after are validated up front — passing an invalid value (e.g. threads=0, a negative port) raises a ValueError immediately instead of failing silently or deep inside waitress. Likewise, every bot file (in bot_file or bots) 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 0 is 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) or SIGTERM shuts down the server and every bot process cleanly.
  • Smart Counters: The max_restarts limit applies to consecutive crashes, per bot. If a bot runs successfully for the duration of restart_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, or max_restarts was 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 exiting 0 as 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.md viewer 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

staypresent-1.5.6.tar.gz (43.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

staypresent-1.5.6-py3-none-any.whl (37.3 kB view details)

Uploaded Python 3

File details

Details for the file staypresent-1.5.6.tar.gz.

File metadata

  • Download URL: staypresent-1.5.6.tar.gz
  • Upload date:
  • Size: 43.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for staypresent-1.5.6.tar.gz
Algorithm Hash digest
SHA256 96089a3a051e5a1358771a019bf7e29912ac0c61d1a9a63b060678a2cf6a5ef2
MD5 309f30241910781d069a48fb12d09cc2
BLAKE2b-256 a132b144197e62c51607bb5196fb82fc31e9b4025ee16a6b187cf71dba068001

See more details on using hashes here.

Provenance

The following attestation bundles were made for staypresent-1.5.6.tar.gz:

Publisher: python-publish.yml on StayElite/StayPresent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file staypresent-1.5.6-py3-none-any.whl.

File metadata

  • Download URL: staypresent-1.5.6-py3-none-any.whl
  • Upload date:
  • Size: 37.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for staypresent-1.5.6-py3-none-any.whl
Algorithm Hash digest
SHA256 f2e4a981300190633b8c1073b949a6e4b7cc96ed90cc763a668a30e8119fa4ec
MD5 fd931a61132dc169b0b88900e118483c
BLAKE2b-256 a6427b1b765b7acabe25564227a15a108d5b9165ce20d8cea3136cb952243a81

See more details on using hashes here.

Provenance

The following attestation bundles were made for staypresent-1.5.6-py3-none-any.whl:

Publisher: python-publish.yml on StayElite/StayPresent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.6.0

2 files

1.5.16

2 files

1.5.14

2 files

1.5.11

2 files

1.5.10

2 files

This release

1.5.6 This release

2 files

1.5.4

2 files

1.4.4

2 files

1.4.2

2 files

1.4.1

2 files

1.4.0

2 files

1.3.4

2 files

1.3.0

2 files

1.2.0

2 files

1.0.4

2 files

1.0.1

2 files

1.0.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page