fireweave 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fireweave/__init__.py ADDED
@@ -0,0 +1,126 @@
1
+ """Fireweave SDK for Python (spec v0.1.0).
2
+
3
+ Core installs with zero runtime dependencies. Optional extras:
4
+
5
+ - ``fireweave[posthog]`` — PostHog backend adapter (``fireweave.adapters.posthog``)
6
+ - ``fireweave[openfeature]`` — OpenFeature provider (``fireweave.openfeature``)
7
+
8
+ Quick start (in-memory, offline)::
9
+
10
+ from fireweave import FireweaveClient, FireweaveRuntime, InMemoryAdapter
11
+
12
+ adapter = InMemoryAdapter({"my-flag": {"type": "boolean", "enabled": True,
13
+ "variant": "on", "value": True}})
14
+ runtime = FireweaveRuntime(adapter)
15
+ runtime.initialize()
16
+ client = FireweaveClient(runtime)
17
+ client.control_points.get_boolean_value("my-flag", False)
18
+ client.shutdown()
19
+
20
+ There are no hidden global clients: everything is constructed explicitly and
21
+ injectable for tests.
22
+ """
23
+
24
+ from ._version import SPEC_VERSION, __version__
25
+ from .adapters import (
26
+ BackendAdapter,
27
+ FlagResolution,
28
+ FireweaveLocalAdapter,
29
+ FireweaveRemoteAdapter,
30
+ InMemoryAdapter,
31
+ RegisterTargetOptions,
32
+ RegisterTargetResult,
33
+ )
34
+ from .capabilities import CANONICAL_CAPABILITIES, CapabilityRegistry
35
+ from .client import (
36
+ CapabilityResult,
37
+ ExposureResult,
38
+ FireweaveClient,
39
+ FlushResult,
40
+ ReleaseContext,
41
+ ReleaseResult,
42
+ SignalResult,
43
+ )
44
+ from .config import DEFAULT_ALLOWED_HOSTS, FireweaveConfig
45
+ from .context import ContextLimits, EvaluationContext, merge_contexts, validate_context
46
+ from .decision import Decision, Reason
47
+ from .errors import (
48
+ AlreadyClosedError,
49
+ AuthenticationError,
50
+ AuthorizationError,
51
+ BackendUnavailableError,
52
+ ConfigurationError,
53
+ ErrorKind,
54
+ FireweaveError,
55
+ FlagNotFoundError,
56
+ InternalError,
57
+ InvalidContextError,
58
+ MalformedResponseError,
59
+ NetworkError,
60
+ NotReadyError,
61
+ RateLimitedError,
62
+ TargetingKeyMissingError,
63
+ TimeoutError_,
64
+ TypeMismatchError,
65
+ UnsupportedCapabilityError,
66
+ )
67
+ from .runtime import EvaluationOptions, FireweaveRuntime, LifecycleState
68
+ from .types import FlagType, JsonValue
69
+
70
+ __all__ = [
71
+ "__version__",
72
+ "SPEC_VERSION",
73
+ # runtime / client
74
+ "FireweaveClient",
75
+ "FireweaveRuntime",
76
+ "FireweaveConfig",
77
+ "DEFAULT_ALLOWED_HOSTS",
78
+ "LifecycleState",
79
+ "EvaluationOptions",
80
+ # adapters
81
+ "BackendAdapter",
82
+ "FlagResolution",
83
+ "InMemoryAdapter",
84
+ "FireweaveLocalAdapter",
85
+ "FireweaveRemoteAdapter",
86
+ "RegisterTargetOptions",
87
+ "RegisterTargetResult",
88
+ # context / decisions
89
+ "ContextLimits",
90
+ "EvaluationContext",
91
+ "merge_contexts",
92
+ "validate_context",
93
+ "Decision",
94
+ "Reason",
95
+ "FlagType",
96
+ "JsonValue",
97
+ # capabilities
98
+ "CANONICAL_CAPABILITIES",
99
+ "CapabilityRegistry",
100
+ # extension results
101
+ "ReleaseContext",
102
+ "ReleaseResult",
103
+ "ExposureResult",
104
+ "FlushResult",
105
+ "SignalResult",
106
+ "CapabilityResult",
107
+ # errors
108
+ "ErrorKind",
109
+ "FireweaveError",
110
+ "NotReadyError",
111
+ "FlagNotFoundError",
112
+ "TypeMismatchError",
113
+ "InvalidContextError",
114
+ "TargetingKeyMissingError",
115
+ "AuthenticationError",
116
+ "AuthorizationError",
117
+ "RateLimitedError",
118
+ "TimeoutError_",
119
+ "NetworkError",
120
+ "BackendUnavailableError",
121
+ "MalformedResponseError",
122
+ "UnsupportedCapabilityError",
123
+ "ConfigurationError",
124
+ "AlreadyClosedError",
125
+ "InternalError",
126
+ ]
fireweave/_version.py ADDED
@@ -0,0 +1,5 @@
1
+ """Package version constants."""
2
+
3
+ __version__ = "0.1.0"
4
+ SPEC_VERSION = "0.1.0"
5
+ OPENFEATURE_SPEC_FLOOR = "0.8.0"
@@ -0,0 +1,23 @@
1
+ """Backend adapters: protocol, in-memory, local, remote (default), PostHog (optional)."""
2
+
3
+ from .base import (
4
+ BackendAdapter,
5
+ FlagResolution,
6
+ RegisterTargetOptions,
7
+ RegisterTargetResult,
8
+ TargetKind,
9
+ )
10
+ from .local import FireweaveLocalAdapter
11
+ from .memory import InMemoryAdapter
12
+ from .remote import FireweaveRemoteAdapter
13
+
14
+ __all__ = [
15
+ "BackendAdapter",
16
+ "FlagResolution",
17
+ "RegisterTargetOptions",
18
+ "RegisterTargetResult",
19
+ "TargetKind",
20
+ "InMemoryAdapter",
21
+ "FireweaveLocalAdapter",
22
+ "FireweaveRemoteAdapter",
23
+ ]
@@ -0,0 +1,117 @@
1
+ """BackendAdapter protocol and vendor-neutral resolution record.
2
+
3
+ Adapters translate a vendor backend (PostHog, in-memory fixtures, ...) into
4
+ :class:`FlagResolution` records. No vendor types cross this boundary — the
5
+ runtime and public API only ever see Fireweave-owned shapes.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Any, Dict, Literal, Optional, Protocol, runtime_checkable
12
+
13
+ from ..context import EvaluationContext
14
+ from ..errors import FireweaveError
15
+ from ..types import JsonValue
16
+
17
+ __all__ = [
18
+ "BackendAdapter",
19
+ "FlagResolution",
20
+ "TargetKind",
21
+ "RegisterTargetOptions",
22
+ "RegisterTargetResult",
23
+ ]
24
+
25
+ TargetKind = Literal["user", "device"]
26
+
27
+
28
+ @dataclass(frozen=True)
29
+ class RegisterTargetOptions:
30
+ """Options for ``POST /v1/targets/register`` (spec/remote-register-target).
31
+
32
+ Omitted fields are left off the wire rather than sent as null/default —
33
+ the server defaults ``kind`` to ``user`` when absent.
34
+ """
35
+
36
+ kind: Optional[TargetKind] = None
37
+ properties: Optional[Dict[str, JsonValue]] = None
38
+ environment: Optional[str] = None
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class RegisterTargetResult:
43
+ """Outcome of target registration.
44
+
45
+ ``ok=False`` means the target was NOT registered — rules that depend on its
46
+ properties will not match until a later attempt succeeds. Callers in a
47
+ login path normally ignore this; a careful caller logs it.
48
+ """
49
+
50
+ ok: bool
51
+ error: Optional[FireweaveError] = None
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class FlagResolution:
56
+ """Vendor-neutral outcome of resolving one flag.
57
+
58
+ ``matched`` is False when targeting conditions did not select the caller
59
+ (the runtime then serves the caller default with reason DEFAULT).
60
+ ``fireweave_reason`` lets adapters force a canonical reason (e.g. SPLIT).
61
+ """
62
+
63
+ value: JsonValue
64
+ variant: Optional[str] = None
65
+ enabled: bool = True
66
+ matched: bool = True
67
+ version: Optional[int] = None
68
+ vendor_flag_id: Optional[int] = None
69
+ reason_code: Optional[str] = None
70
+ condition_index: Optional[int] = None
71
+ payload: Optional[JsonValue] = None
72
+ fireweave_reason: Optional[str] = None
73
+ from_cache: bool = False
74
+ quota_limited: bool = False
75
+ extra_metadata: Dict[str, Any] = field(default_factory=dict)
76
+
77
+
78
+ @runtime_checkable
79
+ class BackendAdapter(Protocol):
80
+ """Protocol every Fireweave backend adapter implements.
81
+
82
+ ``resolve`` raises a :class:`fireweave.errors.FireweaveError` subtype on
83
+ failure (FlagNotFound, Network, Timeout, ...); the runtime converts errors
84
+ into default-valued decisions — evaluation APIs never propagate them.
85
+ """
86
+
87
+ def initialize(self) -> None:
88
+ """Bring the backend up; raise FireweaveError on fatal config."""
89
+
90
+ def resolve(self, flag_key: str, context: EvaluationContext) -> FlagResolution:
91
+ """Resolve one flag against a validated, merged context."""
92
+
93
+ def shutdown(self, timeout_ms: int) -> None:
94
+ """Deterministically flush and release resources within ``timeout_ms``.
95
+
96
+ Idempotent; must never block past the deadline.
97
+ """
98
+
99
+
100
+ # Optional adapter surface (duck-typed, ruling 17 / capabilities matrix):
101
+ #
102
+ # - ``backend_name: str`` — "fireweave" | "posthog" | "inmemory" | "none" | "other"
103
+ # (spec/capabilities.schema.json runtime.backend).
104
+ # - ``runtime_features() -> Dict[str, bool]`` — adapter-dependent runtime
105
+ # capability booleans merged into ``capabilities.get()``.
106
+ # - ``register_target(targeting_key, options) -> RegisterTargetResult`` —
107
+ # durable identity registration (``POST /v1/targets/register``). Returns
108
+ # ``ok=False`` rather than raising — registration sits in login paths.
109
+ # - ``send_exposures(events: list) -> None`` — telemetry sink for flushed
110
+ # exposure events.
111
+ # - ``deliver_signal(signal: Dict) -> None`` — telemetry sink for recorded
112
+ # signals.
113
+ # - ``deliver_release(event: Dict) -> None`` — telemetry sink for release
114
+ # transitions.
115
+ #
116
+ # All sink methods are best-effort: implementations swallow vendor errors —
117
+ # telemetry loss must never affect callers.
@@ -0,0 +1,76 @@
1
+ """Local development adapter — the DEV substrate for a scaffolded harness.
2
+
3
+ Counterpart to :class:`FireweaveRemoteAdapter`: prod evaluates control points
4
+ against fw-server; dev evaluates them here, in-process, with no network and no
5
+ credentials. Because it satisfies the same :class:`BackendAdapter` port, the
6
+ dev branch of a harness runs through the same :class:`FireweaveRuntime` as
7
+ prod — inheriting identical lifecycle gating and context canonicalization.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Dict, Mapping, Optional
13
+
14
+ from ..context import EvaluationContext
15
+ from ..errors import FlagNotFoundError
16
+ from .base import FlagResolution
17
+
18
+ __all__ = ["FireweaveLocalAdapter"]
19
+
20
+
21
+ class FireweaveLocalAdapter:
22
+ """In-process boolean overrides for local development.
23
+
24
+ Resolution policy:
25
+
26
+ - a key present in ``dev_flags`` resolves to its mapped value with reason
27
+ ``STATIC``;
28
+ - every other key raises :class:`FlagNotFoundError`, which the runtime
29
+ turns into an ERROR decision. :func:`make_fireweave_local_provider`
30
+ rewrites that single outcome to a clean DEFAULT for the OpenFeature path.
31
+ """
32
+
33
+ backend_name = "other"
34
+
35
+ def __init__(self, dev_flags: Optional[Mapping[str, bool]] = None) -> None:
36
+ self._dev_flags: Dict[str, bool] = dict(dev_flags or {})
37
+ self._closed = False
38
+
39
+ def initialize(self) -> None:
40
+ self._closed = False
41
+
42
+ def resolve(self, flag_key: str, context: EvaluationContext) -> FlagResolution:
43
+ # A ``dev_flags`` hit reports ``enabled=True`` alongside reason STATIC.
44
+ # Reporting ``enabled=False`` for an override of ``False`` would make
45
+ # the runtime label the decision DISABLED — "switched off upstream" —
46
+ # not what a local override expresses.
47
+ #
48
+ # Values are always boolean, so reading an overridden key as a string
49
+ # or number yields TYPE_MISMATCH rather than silently defaulting.
50
+ del context # unused; kept for BackendAdapter signature parity
51
+ if flag_key not in self._dev_flags:
52
+ raise FlagNotFoundError()
53
+ override = self._dev_flags[flag_key]
54
+ return FlagResolution(
55
+ value=override,
56
+ variant="on" if override else "off",
57
+ enabled=True,
58
+ matched=True,
59
+ fireweave_reason="STATIC",
60
+ )
61
+
62
+ def shutdown(self, timeout_ms: int) -> None:
63
+ del timeout_ms
64
+ self._closed = True
65
+
66
+ def runtime_features(self) -> Dict[str, bool]:
67
+ return {
68
+ "remoteEvaluation": False,
69
+ "localEvaluation": True,
70
+ "localOnly": True,
71
+ # No exposure sink exists locally; claiming otherwise would make
72
+ # capabilities.get() advertise emission that silently goes nowhere.
73
+ "exposureEmission": False,
74
+ "sideEffectFreeReads": True,
75
+ "groupAnalytics": False,
76
+ }
@@ -0,0 +1,115 @@
1
+ """Deterministic in-memory adapter (fixture-shaped flag definitions).
2
+
3
+ Resolution is purely definition-driven — no hashing, no percentage bucketing.
4
+ A flag definition is a dict in the shape used by ``contracts/`` fixtures:
5
+
6
+ .. code-block:: python
7
+
8
+ {
9
+ "type": "boolean", # boolean|string|integer|float|object
10
+ "enabled": True,
11
+ "variant": "on",
12
+ "value": True,
13
+ "payload": {...}, # optional
14
+ "reason": {"code": "condition_match", "condition_index": 0, ...},
15
+ "metadata": {"version": 1, "id": 42},
16
+ "fireweaveReason": "SPLIT", # optional canonical reason override
17
+ "fromCache": False, # optional stale-cache marker
18
+ "matchTargetingKey": "...", # optional deterministic conditions
19
+ "matchAttribute": {...},
20
+ "matchPerson": {...},
21
+ "matchGroups": {...},
22
+ }
23
+ """
24
+
25
+ from __future__ import annotations
26
+
27
+ import threading
28
+ from typing import Any, Dict, Mapping, Optional
29
+
30
+ from ..context import EvaluationContext
31
+ from ..errors import FlagNotFoundError
32
+ from .base import FlagResolution
33
+
34
+ __all__ = ["InMemoryAdapter"]
35
+
36
+
37
+ class InMemoryAdapter:
38
+ """Fixture-driven adapter; thread-safe; supports live flag replacement."""
39
+
40
+ backend_name = "inmemory"
41
+
42
+ def __init__(self, flags: Optional[Mapping[str, Dict[str, Any]]] = None) -> None:
43
+ self._lock = threading.Lock()
44
+ self._flags: Dict[str, Dict[str, Any]] = dict(flags or {})
45
+ self._closed = False
46
+
47
+ def initialize(self) -> None:
48
+ with self._lock:
49
+ self._closed = False
50
+
51
+ def set_flags(self, flags: Mapping[str, Dict[str, Any]]) -> None:
52
+ with self._lock:
53
+ self._flags = dict(flags)
54
+
55
+ def resolve(self, flag_key: str, context: EvaluationContext) -> FlagResolution:
56
+ with self._lock:
57
+ definition = self._flags.get(flag_key)
58
+ if definition is None:
59
+ raise FlagNotFoundError()
60
+
61
+ matched = self._conditions_match(definition, context)
62
+ reason = definition.get("reason") or {}
63
+ metadata = definition.get("metadata") or {}
64
+ return FlagResolution(
65
+ value=definition.get("value"),
66
+ variant=definition.get("variant"),
67
+ enabled=bool(definition.get("enabled", True)),
68
+ matched=matched,
69
+ version=metadata.get("version"),
70
+ vendor_flag_id=metadata.get("id"),
71
+ reason_code=reason.get("code"),
72
+ condition_index=reason.get("condition_index"),
73
+ payload=definition.get("payload"),
74
+ fireweave_reason=definition.get("fireweaveReason"),
75
+ from_cache=bool(definition.get("fromCache", False)),
76
+ )
77
+
78
+ @staticmethod
79
+ def _conditions_match(
80
+ definition: Mapping[str, Any], context: EvaluationContext
81
+ ) -> bool:
82
+ """All present match* conditions must hold (deterministic equality)."""
83
+ expected_key = definition.get("matchTargetingKey")
84
+ if expected_key is not None and context.targeting_key != expected_key:
85
+ return False
86
+
87
+ attrs = dict(context.attributes)
88
+
89
+ for cond_field in ("matchAttribute", "matchPerson"):
90
+ conditions = definition.get(cond_field)
91
+ if conditions:
92
+ for key, expected in conditions.items():
93
+ if attrs.get(key) != expected:
94
+ return False
95
+
96
+ match_groups = definition.get("matchGroups")
97
+ if match_groups:
98
+ groups = context.groups
99
+ for group_type, expected in match_groups.items():
100
+ if groups.get(group_type) != expected:
101
+ return False
102
+ return True
103
+
104
+ def shutdown(self, timeout_ms: int) -> None:
105
+ with self._lock:
106
+ self._closed = True
107
+
108
+ def runtime_features(self) -> Dict[str, bool]:
109
+ """Adapter runtime features merged into ``capabilities.get()``."""
110
+ return {
111
+ "remoteEvaluation": False,
112
+ "localEvaluation": True,
113
+ "exposureEmission": False,
114
+ "sideEffectFreeReads": True,
115
+ }