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.

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.6.0.tar.gz (36.6 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.6.0-py3-none-any.whl (26.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for shipeasy-0.6.0.tar.gz
Algorithm Hash digest
SHA256 4519693a27bbe1725afaffa6d540d8aaa3941f35bb2463cc9fa2fd9e2b3fda25
MD5 804503602958ea2e32b350b6aed84539
BLAKE2b-256 c1f374805b412a352f3e8af2c4f230be1c7afd6cfade68c326a5d97e08e1944a

See more details on using hashes here.

Provenance

The following attestation bundles were made for shipeasy-0.6.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.6.0-py3-none-any.whl.

File metadata

  • Download URL: shipeasy-0.6.0-py3-none-any.whl
  • Upload date:
  • Size: 26.7 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.6.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d8bdedf7e167a0830db69a6ea96352382d958e60f7a5c697154abf7f3dc154f4
MD5 2b509fc2f53d9210de49b8b759a0755a
BLAKE2b-256 44a0f67cf3fa969b81e3f8b3e5841de8d36c4a62664a82005a260813114f7b61

See more details on using hashes here.

Provenance

The following attestation bundles were made for shipeasy-0.6.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