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 +126 -0
- fireweave/_version.py +5 -0
- fireweave/adapters/__init__.py +23 -0
- fireweave/adapters/base.py +117 -0
- fireweave/adapters/local.py +76 -0
- fireweave/adapters/memory.py +115 -0
- fireweave/adapters/posthog.py +426 -0
- fireweave/adapters/remote.py +310 -0
- fireweave/aio.py +176 -0
- fireweave/capabilities.py +59 -0
- fireweave/client.py +809 -0
- fireweave/config.py +99 -0
- fireweave/context.py +199 -0
- fireweave/decision.py +42 -0
- fireweave/errors.py +255 -0
- fireweave/openfeature/__init__.py +22 -0
- fireweave/openfeature/local_provider.py +211 -0
- fireweave/openfeature/provider.py +163 -0
- fireweave/py.typed +0 -0
- fireweave/runtime.py +413 -0
- fireweave/types.py +34 -0
- fireweave-0.1.0.dist-info/METADATA +97 -0
- fireweave-0.1.0.dist-info/RECORD +26 -0
- fireweave-0.1.0.dist-info/WHEEL +5 -0
- fireweave-0.1.0.dist-info/licenses/LICENSE +21 -0
- fireweave-0.1.0.dist-info/top_level.txt +1 -0
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,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
|
+
}
|