EnvPit — Python SDK
Official Python SDK for EnvPit — configuration & secrets management without enterprise complexity.
Quickstart
The fastest way in: make your EXISTING os.environ-reading code work untouched — no client
object, no new API to learn.
pip install envpit
export ENVPIT_API_KEY="epk_..." # an environment-pinned key: Project → API Keys → New key
import envpit
import os
envpit.load().populate_environ() # fetch once, merge into os.environ (opt-in, explicit)
print(os.environ["DATABASE_URL"]) # your existing os.environ-reading code, unmodified
populate_environ() never overwrites a variable your platform already set (same precedence as
python-dotenv) — pass override=True if EnvPit values should win instead. It's a one-shot,
boot-time snapshot: EnvPit's realtime refresh updates the client's own in-memory values
immediately, but can't retroactively update os.environ (nothing can — this is true of
os.environ/Spring @Value in every language, not an EnvPit limitation). Code that needs
guaranteed-live values should read through the client instead — see "Client API" below.
Secrets are excluded by default. The server labels each config key is_secret=true/false
(bd:envpit-durd); populate_environ() reads that flag and skips every secret-flagged key unless
you pass include_secrets=True — the zero-argument call is the safe one. Env vars are inherited
by every child process, are often serialized into crash dumps/APM//proc/<pid>/environ, and are
logged by some startup scripts, so opting a whole environment's secrets into os.environ is a
real exposure — naming include_secrets=True at the call site is the acknowledgment of that.
only=/exclude= additionally narrow the merge by name (an allowlist/denylist, mirroring the Go
SDK's WithOnly/WithExclude) — neither can pull a secret through without include_secrets=True
also being set.
result = envpit.load().populate_environ(
override=False, # default: existing os.environ values always win
include_secrets=False, # default: server-flagged secret keys are never merged
exclude={"LEGACY_KEY"}, # keep specific non-secret keys out too, by name
)
result.merged # sorted tuple of key NAMES actually written
result.skipped_existing # sorted tuple of key NAMES left alone (already present, no override)
result.skipped_secrets # sorted tuple of key NAMES excluded because they're secret-flagged
Client API (in-memory reads, no os.environ)
import envpit
client = envpit.load() # fetches your environment's config once (blocking)
print(client.get("DATABASE_URL")) # in-memory read — never a network call
Run it — you should see your value printed. That's it: client.get() reads from memory; the
snapshot auto-refreshes in the background.
Using FastAPI or another asyncio app? load() only blocks once, at startup — call it in your
lifespan/startup hook, or use await asyncio.to_thread(envpit.load) if you'd rather not block
the loop even there. Every read after that is a plain in-memory lookup.
Framework integrations
Each framework gets the mechanism its own developers actually expect (bd:envpit-yvyr) — not one generic API forced onto all three.
FastAPI — Pydantic Settings (pip install envpit[fastapi]). FastAPI's real idiom is
pydantic_settings.BaseSettings, not os.environ — EnvpitSettingsSource is a genuine
PydanticBaseSettingsSource, wired via settings_customise_sources():
from pydantic_settings import BaseSettings
from envpit.integrations.fastapi import EnvpitSettingsSource
class Settings(BaseSettings):
database_url: str
port: int = 8080
@classmethod
def settings_customise_sources(cls, settings_cls, init_settings, env_settings,
dotenv_settings, file_secret_settings):
return (EnvpitSettingsSource(settings_cls), init_settings, env_settings,
dotenv_settings, file_secret_settings)
settings = Settings() # database_url/port resolved from EnvPit, case-insensitive, env_prefix honored
Flask — app.config (pip install envpit[flask]):
from flask import Flask
from envpit.integrations.flask import init_app
app = Flask(__name__)
init_app(app) # merges the EnvPit snapshot into app.config
Django — settings.py, no extra install. Django has no plugin hook for external settings
sources (verified — its settings loader just imports settings.py as a plain module); the
accepted idiom (the one django-environ also uses) is populating values at the top of the file,
before the rest of it reads them:
# settings.py
import envpit
from envpit.integrations.django import load_into_settings
load_into_settings(globals(), client=envpit.load())
DEBUG = DEBUG == "true" # every EnvPit value is a string — typed post-processing is yours
All three integrations share populate_environ()'s precedence rules (existing value wins unless
override=True) and its secret-exclusion default: server-flagged secrets are excluded unless
include_secrets=True is passed to init_app()/load_into_settings(). FastAPI's
EnvpitSettingsSource follows the same default via its own include_secrets=True constructor
argument (an excluded secret field simply falls back to that field's own default, same as an
absent key).
API
import envpit
from envpit import EnvpitClient
client = EnvpitClient.load(
api_key=None, # falls back to the ENVPIT_API_KEY environment variable
host=None, # default: https://envpit.com
poll_interval=60.0, # seconds; 0 disables ALL background refresh, including realtime
timeout=5.0, # seconds, per request
logger=None, # a stdlib logging.Logger, or any object exposing a subset of
# debug/info/warn/error — absent = silent
)
client.get("DATABASE_URL") # str; raises MissingKeyError if absent
client.get("DATABASE_URL", "postgres://local") # str with a default — never raises
client.get_int("PORT", 8080) # int; raises TypeMismatchError if unparsable
client.get_bool("MAINTENANCE_MODE", False) # bool: true/1/yes/on, false/0/no/off (ci)
client.get_optional("PORT") # str | None — the only getter that never raises
client.snapshot() # dict[str, str | None] — defensive copy, in-memory only (values only)
client.known_secret_keys() # frozenset[str] — server-flagged secret key NAMES only
client.populate_environ( # see Quickstart above; returns a MergeResult
override=False, include_secrets=False, only=None, exclude=None, environ=None,
)
unsubscribe = client.on_change(lambda event: ...) # event: ChangeEvent (key names only)
client.on_connection(lambda event: ...) # event: ConnectionEvent
client.on_error(lambda event: ...) # event: an EnvpitError subclass instance
client.cache_info # CacheInfo — fetched_at / age_ms / last_error / etag / refresh_mode / ...
client.close() # stops background refresh; also a context manager:
with EnvpitClient.load() as client:
...
The module-level envpit.load() sets a default instance so envpit.get(...) /
envpit.get_int(...) / envpit.on_change(...) etc. delegate to it — convenience sugar over the
same client class shown above.
Listener registration only accepts plain (sync) callables — an async def callback raises a
TypeError immediately at registration, with a message telling you how to bridge it
(asyncio.run_coroutine_threadsafe). This is deliberate: a sync dispatcher silently never
awaiting your coroutine would otherwise fail invisibly.
Errors
Every SDK error is a subclass of envpit.EnvpitError:
| Class | When |
|---|---|
AuthenticationError |
No API key found, or the server rejected it (HTTP 401/403) |
NetworkError |
DNS/connect/timeout, a non-2xx response, or an invalid/oversized response |
MissingKeyError |
get*() called for a key that isn't set and no default was given (.key) |
TypeMismatchError |
get_int/get_bool couldn't parse the stored value (.key, .expected_type) |
try:
port = client.get_int("PORT")
except envpit.MissingKeyError as e:
...
except envpit.EnvpitError as e: # catch-all
...
No config value or API key ever appears in an error message or log line (one narrow, documented
exception: TypeMismatchError echoes the offending raw value itself, e.g. got "abc" — that
value is what you passed to a typed getter, never a secret by construction of the typed-getter
path).
Caching & resilience
- Memory-only. Nothing is ever written to disk — no cache file, no temp file, no pickle/shelve.
- Stale-while-revalidate: a background refresh failure never raises from
get*()— the last good snapshot keeps serving reads, and the failure is recorded onclient.cache_info. - Realtime push (SSE) is an optimization; the
poll_intervaltimer is always the correctness backstop, independent of the realtime channel's health. - One daemon poll thread + one daemon realtime thread — neither keeps your process alive.
Security notes
- Auth is sent as
X-Api-Key, neverAuthorization— a separate trust boundary from any session auth your app has. - No SDK option exists to skip TLS verification. Python's
urllibverifies certificates by default; this SDK never disables that. repr(client)/str(client)/print(client)never include the API key or any config value — both are always shown redacted.- Response bodies and realtime stream lines are size-capped (5 MiB / 64 KiB respectively) so a misbehaving or compromised server can't exhaust client memory.
- The config-resolve response must be the post-bd:envpit-durd
{values, secretKeys}envelope — a pre-durd bare{key: value}map is rejected as aNetworkError, not silently accepted (it would otherwise read as "no secrets here" and merge production secrets while reporting none excluded). There were zero published SDK releases before this shape shipped, so this only matters if you're pointed at a very old self-hosted EnvPit server.
Requirements
Python ≥ 3.10. Zero runtime dependencies — stdlib urllib/json/threading/hashlib only.
envpit.integrations.* is opt-in and never imported by the core package: envpit.integrations. fastapi needs pip install envpit[fastapi] (pydantic-settings), envpit.integrations.flask
needs pip install envpit[flask] (flask); envpit.integrations.django needs no extra install.
Release files for envpit 0.1.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 | |
|---|---|---|---|
| envpit-0.1.0.tar.gz | 66.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| envpit-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 109.6 kB
Release files / envpit-0.1.0.tar.gz
| Download URL | envpit-0.1.0.tar.gz |
|---|---|
| Size | 66.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9a549fd74b402cc7c257d23b213507543aa332f9ebb35d7e0e75997e8c01169f
|
|
BLAKE2b-256 checksum How to use checksums |
9dad91043f268d69c263b5b0b18ad8d37f04596b9436cbfb7864426603bd21d3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.4
|
Release files / envpit-0.1.0-py3-none-any.whl
| Download URL | envpit-0.1.0-py3-none-any.whl |
|---|---|
| Size | 43.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ba3ec99510eb81d612961d0d0f298c0984bc054bccf7aad36bc610fb7890174a
|
|
BLAKE2b-256 checksum How to use checksums |
fa20d1d6da5f3c311170040c393cfde0cafce2b057d71894d2987d9c3c6dd356
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.11.4
|