Skip to main content

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.

Server-side rendering (SSR)

Emit the request's evaluated flags as a declarative <script> tag so the browser SDK has them on first paint. 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.

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 = client.bootstrap_script_tag(user, anon_id=anon_id) \
     + client.i18n_script_tag(client_key, "en:prod")

# …or get the raw payload ({"flags", "configs", "experiments", "killswitches"}):
boot = client.evaluate(user)

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:

# 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

shipeasy-0.7.0.tar.gz (39.2 kB view details)

Uploaded Source

Built Distribution

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

shipeasy-0.7.0-py3-none-any.whl (28.9 kB view details)

Uploaded Python 3

File details

Details for the file shipeasy-0.7.0.tar.gz.

File metadata

  • Download URL: shipeasy-0.7.0.tar.gz
  • Upload date:
  • Size: 39.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for shipeasy-0.7.0.tar.gz
Algorithm Hash digest
SHA256 33994c2d5f507801cd4b8d54f92cdd563efaf9c0a5c6903a3cfe423873efd0b9
MD5 2266e06ba2d2b721e78744b02af8c7b6
BLAKE2b-256 8f62ffef46c94e75d6687e7c462476659e8e6cd991414344a19f597cc8135e0b

See more details on using hashes here.

Provenance

The following attestation bundles were made for shipeasy-0.7.0.tar.gz:

Publisher: publish.yml on shipeasy-ai/sdk-python

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

File details

Details for the file shipeasy-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: shipeasy-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 28.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for shipeasy-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 ea84bd6ba03a0837548e7caa76c0dd34ffc7b2d9ef2b368d840ebcb7f4ec0d96
MD5 efd97f9d7ead728416f715a79b4fafa7
BLAKE2b-256 0dc4981c5c8888496434a8f61eabfe826e2837679ebdafcb46a9ed939e8ecf39

See more details on using hashes here.

Provenance

The following attestation bundles were made for shipeasy-0.7.0-py3-none-any.whl:

Publisher: publish.yml on shipeasy-ai/sdk-python

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

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page