Skip to main content

papollo

Python client for Apollo config center, with sync and async support.

from papollo import Apollo

client = Apollo("http://apollo-config:8080", "demo-app")

client.get("timeout")                          # "30", or None if missing
client.get("timeout", "10")                    # with default
client.get("db.url", namespace="database")
client.namespace("app.json")["content"]        # non-properties namespace
client.refresh()                               # pull latest releases
from papollo import AsyncApollo

client = AsyncApollo("http://apollo-config:8080", "demo-app")

await client.get("timeout")
await client.refresh()

Namespaces are fetched on first access and then served from memory. refresh() refetches every loaded namespace, and refresh(name) also loads a namespace that was not loaded yet, which is handy for failing fast at startup. Failures raise ApolloError and keep the previously cached config.

A client is meant to live as long as the process, usually as a module level object. It holds an httpx connection pool, so call close() (or await client.aclose()) if you create short lived clients. Both also work as context managers. An AsyncApollo is bound to the event loop it is first used in.

To pick up new releases without calling refresh(), set max_age in seconds. A read of a namespace older than that refetches it first, which is cheap when nothing changed as the server answers 304. There is no background thread, a namespace nobody reads is not refetched. While one reader refreshes, others are served the cached config. If the refresh fails the cached config is served and a warning is logged to the papollo logger, and the next try waits another max_age. Apollo suggests polling no more often than every 30 seconds.

To get new releases within a second or so, pass watch=True. The client then long polls Apollo's notification endpoint for every loaded namespace, and refetches a namespace as soon as the server reports a new release for it. Polling starts once a namespace is loaded, not when the client is created, so a client created at import time in a process that never reads it runs nothing. An Apollo polls in a daemon thread, an AsyncApollo in a task of its event loop. A namespace loaded later aborts the poll in flight so the next one covers it too. Failures are logged as warnings to the papollo logger and retried after a delay doubling from 1 up to 120 seconds. close() and aclose() stop polling right away. The poller keeps the client alive until then, so always close a watching client that does not live as long as the process. max_age can be combined with watch as a fallback in case notifications are lost.

As the server holds each poll for up to 60 seconds, polling does not use http_client but an httpx client of its own. Pass watch_http_client when polling needs settings too, such as verify for an internal CA, a proxy or headers. papollo does not close it. An Apollo sends every poll on a new HTTP/1.1 connection so that it can abort it, so give it a client used for nothing else and without http2=True.

client = Apollo("http://apollo-config:8080", "demo-app", watch=True)

@client.add_listener
def on_change(namespace, old, new):
    print(namespace, old.get("timeout"), "->", new.get("timeout"))

Listeners registered with add_listener() are called with the namespace name and its old and new configurations whenever a loaded namespace changes, whether through watch, refresh() or max_age. They are not called on first load, nor when a refetch finds nothing changed. An Apollo calls them in the thread that fetched the change, a listener may read the client. An AsyncApollo also accepts coroutine functions and awaits them. Exceptions raised by a listener are logged and do not stop other listeners. remove_listener() unregisters one.

To start while the config service is down, set cache_dir, like the Java client's local cache. Every new release fetched is also written to {app_id}+{cluster}+{namespace}.json in that directory, which is created if missing. Files are replaced atomically, so processes can share a directory, and on Unix are only readable by their owner as configs may hold secrets. File names do not include the server URL, ip or label, so clients of different environments or gray releases should use separate directories. When a namespace fails to load because the server is unreachable or answers with a 5xx, a read serves the cached file instead and logs a warning. Errors such as a wrong secret, an unknown namespace or a server URL without http:// are raised as usual, a stale cache would only hide them. refresh(name) loads the cached file too but still raises, so a fail fast check at startup notices, and reads after it are served from the cache. From then on the namespace counts as loaded with the cached release, which refresh(), max_age or watch refetch like any other, and listeners are called when the server has a newer one. A cache file that can not be read is ignored and failing to write one never fails a fetch, both are logged as warnings.

Clients survive os.fork(), so one created before gunicorn or a multiprocessing pool forks its workers keeps working in them. A child keeps the configs already loaded and gets a new connection pool, as sharing the parent's sockets would mix up responses. An AsyncApollo can then be used in the child's own event loop. Polling does not survive the fork, a child starts its own on its next read. An http_client or watch_http_client you pass in is left alone, create it after the fork. If the parent is already polling when it forks, Python 3.12 and later emit a DeprecationWarning because the process has a thread. papollo resets its own state in the child, but other libraries may not, so prefer to read config only in the workers, for example from gunicorn's post_fork hook, and not in the master process.

Other options: cluster, secret (access key), ip and label (gray release), timeout, and http_client to bring your own httpx.Client / httpx.AsyncClient. When timeout is not set, the httpx client's own timeout is used. ip is not detected automatically, so IP based gray release only works when it is set.

Testing

Most tests run against a real Apollo at http://localhost:8080, the rest cover pure functions and failures a real server can not produce on demand. The easiest server is the quick start all in one jar with an in memory H2 database:

SPRING_PROFILES_ACTIVE=github,database-discovery,auth SPRING_PROFILES_GROUP_GITHUB=h2 \
LOGGING_FILE_NAME=/tmp/apollo.log java -jar apollo-all-in-one.jar

uv run pytest

Tests that need Apollo are skipped when it is not reachable. Set PAPOLLO_REQUIRE_APOLLO=1 to make them fail instead, which is what CI should do. The portal at http://localhost:8070 is used to create apps and publish releases. Use APOLLO_CONFIG_URL, APOLLO_PORTAL_URL, APOLLO_PORTAL_USER, APOLLO_PORTAL_PASSWORD and APOLLO_ENV to point elsewhere.

Metadata

Release files for papollo 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for papollo 0.2.0
File Size Uploaded
papollo-0.2.0.tar.gz 14.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for papollo 0.2.0
File Interpreter ABI Platform
papollo-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 34.9 kB

Release files / papollo-0.2.0.tar.gz

Download URL papollo-0.2.0.tar.gz
Size 14.7 kB
Tags Source
SHA-256 checksum
How to use checksums
3fa63e58059fc18d448df0d9afecdcf3b9c89b3944c116e86596a66a3b45373c
BLAKE2b-256 checksum
How to use checksums
133444174630eb2242c39f3eea856ad1b5fcd9b5fababbb2b4e80f908f85568a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / papollo-0.2.0-py3-none-any.whl

Download URL papollo-0.2.0-py3-none-any.whl
Size 20.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
cfc49fe72a755791a48c582a5621066e22da64cee0ef97a294e513225f2e734c
BLAKE2b-256 checksum
How to use checksums
ee55fbcb55eee741d1f3f744c291d9f52bfb599853e0157f136d8e47ab7d3be9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.12.19 {"installer":{"name":"uv","version":"0.12.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release 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