shipeasy (Python)
Server SDK for Shipeasy — feature flags, remote configs, A/B experiments, and metric tracking. Server-key only, never embed in browsers.
pip install shipeasy
Documentation: Installation & configuration · full docs
Quick start — configure() once, then Client(user) per request
Configure the SDK once at process start with your server key and an optional
attributes transform (your user object → the Shipeasy attribute map). Then
construct a cheap, user-bound Client(user) per request — every call takes no
user argument, because the user is bound at construction.
import shipeasy
# Once, at startup. `attributes` maps YOUR user object to the attribute map
# Shipeasy targets on. Omit it if your user object is already that map.
shipeasy.configure(
api_key="sdk_server_...",
attributes=lambda u: {"user_id": u.id, "country": u.country, "plan": u.plan},
)
# Per request — bind the user once, then ask without re-passing it.
client = shipeasy.Client(current_user)
if client.get_flag("new_checkout"):
...
config = client.get_config("billing_copy")
result = client.get_experiment("checkout_button", default_params={"color": "blue"})
print(result.in_experiment, result.group, result.params)
client.log_exposure("checkout_button") # at the decision point
client.track("purchase", {"amount": 49}) # on conversion
configure() is first-config-wins and kicks off a one-shot fetch fire-and-forget,
so the first Client(user).get_flag(...) resolves against real rules. For a
long-running server that should keep the blob fresh, pass poll=True to start the
background poll — no lower-level object to manage:
shipeasy.configure(api_key="sdk_server_...", poll=True) # background poll
If your user object is already the attribute map, omit attributes (the default
is identity):
shipeasy.configure(api_key="sdk_server_...")
shipeasy.Client({"user_id": "u_123", "country": "US"}).get_flag("new_checkout")
Constructing Client(user) before configure() raises RuntimeError.
The bound Client
Everything per request is on Client(user) — no user argument on any call:
get_flag / get_flag_detail / get_config / get_killswitch /
get_experiment, plus log_exposure(experiment_name) and
track(event, properties=None). So an experiment is end-to-end Client-only.
For unit tests and offline evaluation, swap configure() for its drop-in
siblings — configure_for_testing / configure_for_offline
(below).
Anonymous visitors (zero-config bucketing)
For logged-out traffic you need a stable unit so a fractional rollout buckets
the same on the server and in the browser. The middleware mints a first-party
__se_anon_id cookie (shared with every Shipeasy SDK) for any request without
one; evaluations then default to it as anonymous_id, so get_flag on an
anonymous request just works — no per-call wiring.
# WSGI (Flask, Django, ...)
from shipeasy.middleware import AnonIdMiddleware
app.wsgi_app = AnonIdMiddleware(app.wsgi_app)
# ASGI (FastAPI, Starlette)
from shipeasy.middleware import AnonIdASGIMiddleware
app.add_middleware(AnonIdASGIMiddleware)
# logged-out request → buckets on the __se_anon_id cookie automatically
shipeasy.Client({}).get_flag("new_checkout")
An explicit user_id/anonymous_id always wins. The id is also on the request
(environ["shipeasy.anon_id"]). The cookie is non-HttpOnly by design so the
browser SDK buckets identically; a request with no unit still resolves a
fully-rolled (100%) gate as on. Cookie name + format are a cross-SDK contract —
see 18-identity-bucketing.md.
Server-side rendering (SSR)
Emit the request's evaluated flags as a declarative <script> tag so the
browser SDK has them on first paint. shipeasy.bootstrap_script_tag carries the
payload in data-* attributes (no key); the static se-bootstrap.js loader
hydrates window.__SE_BOOTSTRAP and writes the __se_anon_id cookie so the
browser buckets identically to the server. The tag helpers are package-level and
delegate to the engine you set up with configure().
import shipeasy
user = {"user_id": "u_123"}
# Two tags for the document <head>. The PUBLIC client key (not the server
# key) goes on the i18n loader tag.
head = shipeasy.bootstrap_script_tag(user, anon_id=anon_id) \
+ shipeasy.i18n_script_tag(client_key, "en:prod")
bootstrap_script_tag also accepts i18n_profile= and base_url=
(defaults to https://cdn.shipeasy.ai).
Default values
get_flag and get_config take a default that is returned only when the
value cannot be evaluated — never when it simply resolves off:
client = shipeasy.Client(current_user)
# default is returned only if Shipeasy isn't ready yet OR the gate isn't in the
# blob. A gate that evaluates to False returns False, not the default.
client.get_flag("new_checkout", default=True)
# default is returned when the config key is absent (or decode raises).
client.get_config("billing_copy", default={"title": "Welcome"})
client.get_config("limits", decode=lambda v: v["max"], default=0)
Evaluation detail
get_flag_detail returns a FlagDetail(value, reason) so you can log why a
flag resolved the way it did. reason is one of the exported constants:
from shipeasy import (
FlagDetail, CLIENT_NOT_READY, FLAG_NOT_FOUND, OFF, OVERRIDE, RULE_MATCH, DEFAULT,
)
d = shipeasy.Client(current_user).get_flag_detail("new_checkout")
print(d.value, d.reason) # e.g. True RULE_MATCH
| reason | meaning |
|---|---|
OVERRIDE |
a configure_for_testing override forced the value |
CLIENT_NOT_READY |
the first fetch hasn't completed yet → value=False |
FLAG_NOT_FOUND |
no gate by that name in the blob → value=False |
OFF |
the gate exists but is disabled → value=False |
RULE_MATCH |
evaluated on (targeting + rollout) |
DEFAULT |
evaluated off (fell through) |
get_flag delegates to get_flag_detail and returns .value (substituting
default for CLIENT_NOT_READY/FLAG_NOT_FOUND).
Change listeners
Register a callback fired after a background poll (configure(poll=True)) fetches
new data (a 200, not a 304). It returns an unsubscribe callable.
unsubscribe = shipeasy.on_change(lambda: print("flags changed, rebuild cache"))
...
unsubscribe() # stop listening
Testing
Use configure_for_testing() — the test-mode sibling of configure(). It does
zero network, needs no api_key, and seeds the values your code under test
should see via override args. Read them through the ordinary Client:
import shipeasy
shipeasy.configure_for_testing(
flags={"new_checkout": True},
configs={"billing_copy": {"title": "Welcome"}},
experiments={"checkout_button": ("treatment", {"color": "green"})},
)
client = shipeasy.Client({"user_id": "u_123"})
assert client.get_flag("new_checkout") is True
assert client.get_config("billing_copy") == {"title": "Welcome"}
result = client.get_experiment("checkout_button", default_params={"color": "blue"})
assert result.in_experiment and result.group == "treatment"
# track()/log_exposure() are no-ops in test mode
client.track("purchase", {"amount": 49})
configure_for_testing() replaces any prior configuration, so each test
reconfigures freely.
Offline snapshot
Run fully offline from a JSON snapshot — handy for local dev or air-gapped CI.
Use configure_for_offline(): evaluations run the real eval logic against
the snapshot; no network is touched, and override args still apply on top.
import shipeasy
# From a file: { "flags": <body of /sdk/flags>, "experiments": <body of /sdk/experiments> }
shipeasy.configure_for_offline(path="shipeasy-snapshot.json")
# Or from in-memory blobs, with optional overrides on top
shipeasy.configure_for_offline(
snapshot={
"flags": {"gates": {...}, "configs": {...}},
"experiments": {"experiments": {...}, "universes": {...}},
},
flags={"new_checkout": True},
)
shipeasy.Client({"user_id": "u_123"}).get_flag("new_checkout")
Evaluation
Tested against the cross-language MurmurHash3 vectors in experiment-platform/04-evaluation.md.
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 shipeasy-0.10.0.tar.gz.
File metadata
- Download URL: shipeasy-0.10.0.tar.gz
- Upload date:
- Size: 61.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0b10743df8f604fe0b01107930fb3ae8358fb2c0e17351a3c70da8bba4a2ccf5
|
|
| MD5 |
c8f73da3e14172ac9134814b225e4bf6
|
|
| BLAKE2b-256 |
d86c2e0deb3ce34948836013725cdd3f14b3533dfb74494123a8e2d827c3bc17
|
Provenance
The following attestation bundles were made for shipeasy-0.10.0.tar.gz:
Publisher:
publish.yml on shipeasy-ai/sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shipeasy-0.10.0.tar.gz -
Subject digest:
0b10743df8f604fe0b01107930fb3ae8358fb2c0e17351a3c70da8bba4a2ccf5 - Sigstore transparency entry: 1985219701
- Sigstore integration time:
-
Permalink:
shipeasy-ai/sdk-python@f5244577b60cb7802557aaec9c584962db7fdd6b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/shipeasy-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f5244577b60cb7802557aaec9c584962db7fdd6b -
Trigger Event:
push
-
Statement type:
File details
Details for the file shipeasy-0.10.0-py3-none-any.whl.
File metadata
- Download URL: shipeasy-0.10.0-py3-none-any.whl
- Upload date:
- Size: 33.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5707a305935b58cb816a318f659d28e65639fe3a38ee4aad4f598d25e3c56615
|
|
| MD5 |
f6d718d603e2646e5c25fe97747774ef
|
|
| BLAKE2b-256 |
7d50777bbae6f68ba7f55040ae1abf97f701334de08ca92e12bc4db3caacc494
|
Provenance
The following attestation bundles were made for shipeasy-0.10.0-py3-none-any.whl:
Publisher:
publish.yml on shipeasy-ai/sdk-python
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
shipeasy-0.10.0-py3-none-any.whl -
Subject digest:
5707a305935b58cb816a318f659d28e65639fe3a38ee4aad4f598d25e3c56615 - Sigstore transparency entry: 1985219776
- Sigstore integration time:
-
Permalink:
shipeasy-ai/sdk-python@f5244577b60cb7802557aaec9c584962db7fdd6b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/shipeasy-ai
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f5244577b60cb7802557aaec9c584962db7fdd6b -
Trigger Event:
push
-
Statement type: