Shipeasy server SDK for Python — feature flags, configs, experiments, metrics.
Project description
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
from shipeasy import Client
client = Client(api_key="sdk_server_...")
client.init() # background poll; use init_once() for serverless
if client.get_flag("new_checkout", {"user_id": "u_123", "country": "US"}):
...
config = client.get_config("billing_copy")
result = client.get_experiment(
"checkout_button",
user={"user_id": "u_123"},
default_params={"color": "blue"},
)
print(result.in_experiment, result.group, result.params)
client.track("u_123", "purchase", {"amount": 49})
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
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.
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:
# default is returned only if the client isn't initialized OR the gate isn't
# in the blob. A gate that evaluates to False returns False, not the default.
client.get_flag("new_checkout", {"user_id": "u_123"}, 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 = client.get_flag_detail("new_checkout", {"user_id": "u_123"})
print(d.value, d.reason) # e.g. True RULE_MATCH
| reason | meaning |
|---|---|
OVERRIDE |
a local override_flag forced the value (no telemetry) |
CLIENT_NOT_READY |
init()/init_once() hasn't run 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 fetches new data (a 200, not a 304). It returns an unsubscribe callable. Listeners never fire in test/offline mode.
unsubscribe = client.on_change(lambda: print("flags changed, rebuild cache"))
...
unsubscribe() # stop listening
Offline snapshot
Run fully offline from a JSON snapshot — handy for tests, local dev, or
air-gapped CI. Evaluations run the real eval logic against the snapshot;
no network is ever touched (init()/init_once()/track() are no-ops) and
override_* setters still apply on top.
# From a file: { "flags": <body of /sdk/flags>, "experiments": <body of /sdk/experiments> }
client = Client.from_file("shipeasy-snapshot.json")
# Or from in-memory blobs
client = Client.from_snapshot(
flags={"gates": {...}, "configs": {...}},
experiments={"experiments": {...}, "universes": {...}},
)
client.get_flag("new_checkout", {"user_id": "u_123"})
Testing
Use Client.for_testing() for unit tests: it does zero network, needs no
api_key, disables telemetry, and makes init()/init_once()/track() no-ops.
Seed every entity with the override_* setters (Statsig-style local overrides) —
an override always wins over whatever the client would otherwise resolve.
from shipeasy import Client
client = Client.for_testing() # no key, no network, immediately usable
# Flags
client.override_flag("new_checkout", True)
assert client.get_flag("new_checkout", {"user_id": "u_123"}) is True
# Configs (decode is optional and still applies)
client.override_config("billing_copy", {"title": "Welcome"})
assert client.get_config("billing_copy") == {"title": "Welcome"}
assert client.get_config("billing_copy", decode=lambda v: v["title"]) == "Welcome"
# Experiments → ExperimentResult(in_experiment=True, group=..., params=...)
client.override_experiment("checkout_button", group="treatment", params={"color": "green"})
result = client.get_experiment(
"checkout_button",
user={"user_id": "u_123"},
default_params={"color": "blue"},
)
assert result.in_experiment and result.group == "treatment"
assert result.params == {"color": "green"}
# track() is a no-op in test mode — safe to call, sends nothing
client.track("u_123", "purchase", {"amount": 49})
# Reset between cases
client.clear_overrides()
The same override_* / clear_overrides() setters also work on a normal
Client if you want to pin a value in a live client.
Evaluation
Tested against the cross-language MurmurHash3 vectors in experiment-platform/04-evaluation.md.
Project details
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.5.0.tar.gz.
File metadata
- Download URL: shipeasy-0.5.0.tar.gz
- Upload date:
- Size: 30.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 |
35587f1fc7c149983d8d0a992b3c23e9492a98a85b8c73f64f972eab2dc2ba0d
|
|
| MD5 |
732520053192e53419f69bb3a1a2e275
|
|
| BLAKE2b-256 |
7fe81d5a94fe162c94ae45c731ead28288c304b23da06ae02f05f799eb1cf9e7
|
Provenance
The following attestation bundles were made for shipeasy-0.5.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.5.0.tar.gz -
Subject digest:
35587f1fc7c149983d8d0a992b3c23e9492a98a85b8c73f64f972eab2dc2ba0d - Sigstore transparency entry: 1870920610
- Sigstore integration time:
-
Permalink:
shipeasy-ai/sdk-python@ac3e2a6d97cf1f809e320b1e37410de1d36b755d -
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@ac3e2a6d97cf1f809e320b1e37410de1d36b755d -
Trigger Event:
push
-
Statement type:
File details
Details for the file shipeasy-0.5.0-py3-none-any.whl.
File metadata
- Download URL: shipeasy-0.5.0-py3-none-any.whl
- Upload date:
- Size: 21.6 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 |
1e8a8f6da93740bcfefe4ce2496061f3b27c1f25a989e005136b5f888fbb5719
|
|
| MD5 |
39fae97f518c045efd5c76c830747e32
|
|
| BLAKE2b-256 |
a630df76e773c80d5756b86aedb8b823cc7fb108565e05626afa0f2cba15d810
|
Provenance
The following attestation bundles were made for shipeasy-0.5.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.5.0-py3-none-any.whl -
Subject digest:
1e8a8f6da93740bcfefe4ce2496061f3b27c1f25a989e005136b5f888fbb5719 - Sigstore transparency entry: 1870920720
- Sigstore integration time:
-
Permalink:
shipeasy-ai/sdk-python@ac3e2a6d97cf1f809e320b1e37410de1d36b755d -
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@ac3e2a6d97cf1f809e320b1e37410de1d36b755d -
Trigger Event:
push
-
Statement type: