hexastack-flags 0.0.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.
Potentially problematic release.
This version of hexastack-flags might be problematic. Click here for more details.
- hexastack_flags/__init__.py +12 -0
- hexastack_flags/adapters/__init__.py +5 -0
- hexastack_flags/adapters/openfeature.py +211 -0
- hexastack_flags/adapters/providers/__init__.py +5 -0
- hexastack_flags/adapters/providers/factory.py +88 -0
- hexastack_flags/domain/__init__.py +9 -0
- hexastack_flags/domain/models.py +35 -0
- hexastack_flags/infra/__init__.py +8 -0
- hexastack_flags/infra/bootstrap.py +52 -0
- hexastack_flags/infra/config.py +49 -0
- hexastack_flags/ports/__init__.py +11 -0
- hexastack_flags-0.0.0.dist-info/METADATA +96 -0
- hexastack_flags-0.0.0.dist-info/RECORD +15 -0
- hexastack_flags-0.0.0.dist-info/WHEEL +4 -0
- hexastack_flags-0.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from hexastack_flags import adapters, domain, infra, ports
|
|
2
|
+
from hexastack_flags.adapters.openfeature import OpenFeatureFlagAdapter
|
|
3
|
+
from hexastack_flags.infra.bootstrap import FeatureFlagBootstrapper
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"adapters",
|
|
7
|
+
"domain",
|
|
8
|
+
"FeatureFlagBootstrapper",
|
|
9
|
+
"infra",
|
|
10
|
+
"OpenFeatureFlagAdapter",
|
|
11
|
+
"ports",
|
|
12
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""CNCF OpenFeature flag adapter implementation.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Adapts the OpenFeature Python SDK (openfeature.api) to Hexastack's FeatureFlagPort.
|
|
5
|
+
Translates ambient UserContext / EvaluationContext seamlessly to OpenFeature
|
|
6
|
+
EvaluationContext and evaluates boolean, string, int, float, and JSON flags.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import openfeature.api
|
|
13
|
+
from openfeature.client import OpenFeatureClient
|
|
14
|
+
from openfeature.evaluation_context import EvaluationContext as OfEvaluationContext
|
|
15
|
+
|
|
16
|
+
from hexastack_core.domain.feature_flags import (
|
|
17
|
+
EvaluationContext,
|
|
18
|
+
FlagEvaluationDetails,
|
|
19
|
+
FlagEvaluationReason,
|
|
20
|
+
)
|
|
21
|
+
from hexastack_core.ports.feature_flags import FeatureFlagPort
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"OpenFeatureFlagAdapter",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class OpenFeatureFlagAdapter(FeatureFlagPort):
|
|
29
|
+
"""FeatureFlagPort implementation delegating evaluations to OpenFeature SDK.
|
|
30
|
+
|
|
31
|
+
Notes/Architectural Intent:
|
|
32
|
+
Wraps an OpenFeature Client instance, connecting Hexastack application
|
|
33
|
+
layers (CQRS, FastAPI, CLI, GraphQL) to any configured OpenFeature provider.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
client: OpenFeatureClient | None = None,
|
|
39
|
+
client_name: str | None = None,
|
|
40
|
+
) -> None:
|
|
41
|
+
self._client = client or openfeature.api.get_client(client_name)
|
|
42
|
+
|
|
43
|
+
def get_all_flags(self) -> dict[str, Any]:
|
|
44
|
+
"""Introspect active flags from the underlying OpenFeature provider if supported.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Dictionary mapping flag keys to their configured status/values.
|
|
48
|
+
"""
|
|
49
|
+
flags: dict[str, Any] = {}
|
|
50
|
+
try:
|
|
51
|
+
provider: Any = openfeature.api.provider_registry.get_provider(None)
|
|
52
|
+
provider_flags = getattr(provider, "_flags", None)
|
|
53
|
+
if isinstance(provider_flags, dict):
|
|
54
|
+
for k, v in provider_flags.items():
|
|
55
|
+
# If InMemoryFlag object, resolve current variant/value
|
|
56
|
+
if hasattr(v, "variants") and hasattr(v, "default_variant"):
|
|
57
|
+
flags[str(k)] = v.variants.get(
|
|
58
|
+
v.default_variant, v.default_variant
|
|
59
|
+
)
|
|
60
|
+
elif hasattr(v, "state"):
|
|
61
|
+
flags[str(k)] = str(v.state)
|
|
62
|
+
else:
|
|
63
|
+
flags[str(k)] = v
|
|
64
|
+
except Exception:
|
|
65
|
+
pass
|
|
66
|
+
return flags
|
|
67
|
+
|
|
68
|
+
def get_boolean_details(
|
|
69
|
+
self,
|
|
70
|
+
flag_key: str,
|
|
71
|
+
default: bool = False,
|
|
72
|
+
context: EvaluationContext | None = None,
|
|
73
|
+
) -> FlagEvaluationDetails[bool]:
|
|
74
|
+
"""Evaluate a boolean flag returning comprehensive evaluation metadata."""
|
|
75
|
+
of_ctx = _to_openfeature_context(
|
|
76
|
+
context or EvaluationContext.from_current_context()
|
|
77
|
+
)
|
|
78
|
+
details = self._client.get_boolean_details(
|
|
79
|
+
flag_key=flag_key,
|
|
80
|
+
default_value=default,
|
|
81
|
+
evaluation_context=of_ctx,
|
|
82
|
+
)
|
|
83
|
+
reason: FlagEvaluationReason = FlagEvaluationReason.UNKNOWN
|
|
84
|
+
if details.reason:
|
|
85
|
+
try:
|
|
86
|
+
reason = FlagEvaluationReason(str(details.reason).upper())
|
|
87
|
+
except ValueError:
|
|
88
|
+
reason = FlagEvaluationReason.UNKNOWN
|
|
89
|
+
|
|
90
|
+
return FlagEvaluationDetails(
|
|
91
|
+
flag_key=details.flag_key,
|
|
92
|
+
value=details.value,
|
|
93
|
+
reason=reason,
|
|
94
|
+
variant=details.variant,
|
|
95
|
+
error_code=str(details.error_code) if details.error_code else None,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def get_boolean_value(
|
|
99
|
+
self,
|
|
100
|
+
flag_key: str,
|
|
101
|
+
default: bool = False,
|
|
102
|
+
context: EvaluationContext | None = None,
|
|
103
|
+
) -> bool:
|
|
104
|
+
"""Evaluate a boolean feature flag."""
|
|
105
|
+
of_ctx = _to_openfeature_context(
|
|
106
|
+
context or EvaluationContext.from_current_context()
|
|
107
|
+
)
|
|
108
|
+
return self._client.get_boolean_value(
|
|
109
|
+
flag_key=flag_key,
|
|
110
|
+
default_value=default,
|
|
111
|
+
evaluation_context=of_ctx,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def get_float_value(
|
|
115
|
+
self,
|
|
116
|
+
flag_key: str,
|
|
117
|
+
default: float = 0.0,
|
|
118
|
+
context: EvaluationContext | None = None,
|
|
119
|
+
) -> float:
|
|
120
|
+
"""Evaluate a floating-point feature flag."""
|
|
121
|
+
of_ctx = _to_openfeature_context(
|
|
122
|
+
context or EvaluationContext.from_current_context()
|
|
123
|
+
)
|
|
124
|
+
return self._client.get_float_value(
|
|
125
|
+
flag_key=flag_key,
|
|
126
|
+
default_value=default,
|
|
127
|
+
evaluation_context=of_ctx,
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
def get_integer_value(
|
|
131
|
+
self,
|
|
132
|
+
flag_key: str,
|
|
133
|
+
default: int = 0,
|
|
134
|
+
context: EvaluationContext | None = None,
|
|
135
|
+
) -> int:
|
|
136
|
+
"""Evaluate an integer feature flag."""
|
|
137
|
+
of_ctx = _to_openfeature_context(
|
|
138
|
+
context or EvaluationContext.from_current_context()
|
|
139
|
+
)
|
|
140
|
+
return self._client.get_integer_value(
|
|
141
|
+
flag_key=flag_key,
|
|
142
|
+
default_value=default,
|
|
143
|
+
evaluation_context=of_ctx,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def get_object_value(
|
|
147
|
+
self,
|
|
148
|
+
flag_key: str,
|
|
149
|
+
default: Mapping[str, Any] | None = None,
|
|
150
|
+
context: EvaluationContext | None = None,
|
|
151
|
+
) -> Mapping[str, Any]:
|
|
152
|
+
"""Evaluate a structured JSON object feature flag."""
|
|
153
|
+
of_ctx = _to_openfeature_context(
|
|
154
|
+
context or EvaluationContext.from_current_context()
|
|
155
|
+
)
|
|
156
|
+
default_val = dict(default) if default is not None else {}
|
|
157
|
+
res = self._client.get_object_value(
|
|
158
|
+
flag_key=flag_key,
|
|
159
|
+
default_value=default_val,
|
|
160
|
+
evaluation_context=of_ctx,
|
|
161
|
+
)
|
|
162
|
+
return res if isinstance(res, Mapping) else default_val
|
|
163
|
+
|
|
164
|
+
def get_string_value(
|
|
165
|
+
self,
|
|
166
|
+
flag_key: str,
|
|
167
|
+
default: str = "",
|
|
168
|
+
context: EvaluationContext | None = None,
|
|
169
|
+
) -> str:
|
|
170
|
+
"""Evaluate a string/variant feature flag."""
|
|
171
|
+
of_ctx = _to_openfeature_context(
|
|
172
|
+
context or EvaluationContext.from_current_context()
|
|
173
|
+
)
|
|
174
|
+
return self._client.get_string_value(
|
|
175
|
+
flag_key=flag_key,
|
|
176
|
+
default_value=default,
|
|
177
|
+
evaluation_context=of_ctx,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
def is_enabled(
|
|
181
|
+
self,
|
|
182
|
+
flag_key: str,
|
|
183
|
+
default: bool = False,
|
|
184
|
+
context: EvaluationContext | None = None,
|
|
185
|
+
) -> bool:
|
|
186
|
+
"""Evaluate whether a boolean feature flag is enabled."""
|
|
187
|
+
return self.get_boolean_value(
|
|
188
|
+
flag_key=flag_key, default=default, context=context
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _to_openfeature_context(
|
|
193
|
+
context: EvaluationContext | None,
|
|
194
|
+
) -> OfEvaluationContext | None:
|
|
195
|
+
"""Convert Hexastack EvaluationContext to OpenFeature EvaluationContext."""
|
|
196
|
+
if context is None:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
targeting_key = context.targeting_key or context.user_id or ""
|
|
200
|
+
attributes: dict[str, Any] = dict(context.attributes)
|
|
201
|
+
if context.user_id:
|
|
202
|
+
attributes["user_id"] = context.user_id
|
|
203
|
+
if context.tenant_id:
|
|
204
|
+
attributes["tenant_id"] = context.tenant_id
|
|
205
|
+
if context.roles:
|
|
206
|
+
attributes["roles"] = list(context.roles)
|
|
207
|
+
|
|
208
|
+
return OfEvaluationContext(
|
|
209
|
+
targeting_key=targeting_key,
|
|
210
|
+
attributes=attributes,
|
|
211
|
+
)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Factory for initializing OpenFeature provider backends.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Decouples application setup from concrete OpenFeature provider implementations
|
|
5
|
+
(Flagd, In-Memory, etc.), dynamically importing optional dependencies on demand.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import openfeature.api
|
|
13
|
+
from openfeature.provider.in_memory_provider import InMemoryFlag, InMemoryProvider
|
|
14
|
+
|
|
15
|
+
from hexastack_core.domain.exceptions import MissingDependencyError
|
|
16
|
+
from hexastack_flags.domain.models import (
|
|
17
|
+
FeatureFlagProviderType,
|
|
18
|
+
FlagProviderOptions,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def initialize_openfeature_provider(
|
|
23
|
+
provider_type: FeatureFlagProviderType | str = FeatureFlagProviderType.IN_MEMORY,
|
|
24
|
+
options: FlagProviderOptions | None = None,
|
|
25
|
+
in_memory_flags: dict[str, Any] | None = None,
|
|
26
|
+
) -> None:
|
|
27
|
+
"""Initialize and register an OpenFeature provider backend globally.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
provider_type: Target backend ("flagd", "in_memory", "env", etc.).
|
|
31
|
+
options: Configuration options (host, port, cache, etc.).
|
|
32
|
+
in_memory_flags: Initial flags dict when using in_memory provider.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
MissingDependencyError: If a provider extra (e.g. openfeature-provider-flagd) is missing.
|
|
36
|
+
"""
|
|
37
|
+
opts = options or FlagProviderOptions()
|
|
38
|
+
p_type = str(provider_type).lower()
|
|
39
|
+
|
|
40
|
+
if p_type == FeatureFlagProviderType.FLAGD:
|
|
41
|
+
try:
|
|
42
|
+
import importlib
|
|
43
|
+
|
|
44
|
+
flagd_mod = importlib.import_module("openfeature.contrib.provider.flagd")
|
|
45
|
+
config_mod = importlib.import_module(
|
|
46
|
+
"openfeature.contrib.provider.flagd.config"
|
|
47
|
+
)
|
|
48
|
+
flagd_cls = flagd_mod.FlagdProvider
|
|
49
|
+
cache_type_cls = config_mod.CacheType
|
|
50
|
+
|
|
51
|
+
cache_val = cache_type_cls.LRU if opts.cache else cache_type_cls.DISABLED
|
|
52
|
+
|
|
53
|
+
provider = flagd_cls(
|
|
54
|
+
host=opts.host,
|
|
55
|
+
port=opts.port,
|
|
56
|
+
cache=cache_val,
|
|
57
|
+
timeout=opts.timeout_ms,
|
|
58
|
+
)
|
|
59
|
+
openfeature.api.set_provider(provider)
|
|
60
|
+
except (ImportError, AttributeError) as e:
|
|
61
|
+
raise MissingDependencyError(
|
|
62
|
+
"openfeature-provider-flagd is required for Flagd provider. "
|
|
63
|
+
"Install with 'pip install hexastack-flags[flagd]'."
|
|
64
|
+
) from e
|
|
65
|
+
|
|
66
|
+
elif p_type == FeatureFlagProviderType.IN_MEMORY:
|
|
67
|
+
flags_dict: dict[str, InMemoryFlag[Any]] = {}
|
|
68
|
+
if in_memory_flags:
|
|
69
|
+
for k, v in in_memory_flags.items():
|
|
70
|
+
if isinstance(v, bool):
|
|
71
|
+
flags_dict[k] = InMemoryFlag(
|
|
72
|
+
state=InMemoryFlag.State.ENABLED,
|
|
73
|
+
default_variant="on" if v else "off",
|
|
74
|
+
variants={"on": True, "off": False},
|
|
75
|
+
)
|
|
76
|
+
else:
|
|
77
|
+
flags_dict[k] = InMemoryFlag(
|
|
78
|
+
state=InMemoryFlag.State.ENABLED,
|
|
79
|
+
default_variant="default",
|
|
80
|
+
variants={"default": v},
|
|
81
|
+
)
|
|
82
|
+
provider = InMemoryProvider(flags_dict)
|
|
83
|
+
openfeature.api.set_provider(provider)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
__all__ = [
|
|
87
|
+
"initialize_openfeature_provider",
|
|
88
|
+
]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Domain models and enumerations for OpenFeature integration.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Encapsulates OpenFeature provider types, provider options, and evaluation
|
|
5
|
+
metadata conforming to the CNCF OpenFeature specification.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from pydantic import BaseModel, Field
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FeatureFlagProviderType",
|
|
15
|
+
"FlagProviderOptions",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class FeatureFlagProviderType(StrEnum):
|
|
20
|
+
"""Supported CNCF OpenFeature provider backends."""
|
|
21
|
+
|
|
22
|
+
FLAGD = "flagd"
|
|
23
|
+
IN_MEMORY = "in_memory"
|
|
24
|
+
ENV = "env"
|
|
25
|
+
CUSTOM = "custom"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FlagProviderOptions(BaseModel):
|
|
29
|
+
"""Configuration options for OpenFeature provider initialization."""
|
|
30
|
+
|
|
31
|
+
host: str = "localhost"
|
|
32
|
+
port: int = 8013
|
|
33
|
+
cache: bool = True
|
|
34
|
+
timeout_ms: int = 5000
|
|
35
|
+
extra: dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Bootstrapper extension for hexastack-flags.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Configures the OpenFeature provider and registers OpenFeatureFlagAdapter
|
|
5
|
+
into the rodi.Container as the singleton FeatureFlagPort at order=14.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from hexastack_core.infra.bootstrap import BootstrapContext
|
|
9
|
+
from hexastack_core.infra.registries.config import ConfigRegistry
|
|
10
|
+
from hexastack_core.ports.bootstrap import BootstrapperPort
|
|
11
|
+
from hexastack_core.ports.feature_flags import FeatureFlagPort
|
|
12
|
+
from hexastack_flags.adapters.openfeature import OpenFeatureFlagAdapter
|
|
13
|
+
from hexastack_flags.adapters.providers.factory import initialize_openfeature_provider
|
|
14
|
+
from hexastack_flags.domain.models import FlagProviderOptions
|
|
15
|
+
from hexastack_flags.infra.config import HexastackFlagsConfig, register_flags_config
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"FeatureFlagBootstrapper",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class FeatureFlagBootstrapper(BootstrapperPort):
|
|
23
|
+
"""Bootstrapper that configures CNCF OpenFeature and binds OpenFeatureFlagAdapter."""
|
|
24
|
+
|
|
25
|
+
name: str = "feature_flags"
|
|
26
|
+
order: int = 14 # Runs before CQRS (20), FastAPI (30), etc.
|
|
27
|
+
|
|
28
|
+
def configure(self, context: BootstrapContext) -> None:
|
|
29
|
+
"""Initialize OpenFeature provider and register OpenFeatureFlagAdapter in DI."""
|
|
30
|
+
config = context.get_config("flags", HexastackFlagsConfig)
|
|
31
|
+
|
|
32
|
+
options = FlagProviderOptions(
|
|
33
|
+
host=config.host,
|
|
34
|
+
port=config.port,
|
|
35
|
+
cache=config.cache,
|
|
36
|
+
timeout_ms=config.timeout_ms,
|
|
37
|
+
extra=config.options,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
initialize_openfeature_provider(
|
|
41
|
+
provider_type=config.provider,
|
|
42
|
+
options=options,
|
|
43
|
+
in_memory_flags=config.flags,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
adapter = OpenFeatureFlagAdapter()
|
|
47
|
+
context.container.add_instance(adapter, declared_class=FeatureFlagPort)
|
|
48
|
+
context.flags = adapter
|
|
49
|
+
|
|
50
|
+
def register_config(self, registry: ConfigRegistry) -> None:
|
|
51
|
+
"""Register HexastackFlagsConfig under [hexastack.flags]."""
|
|
52
|
+
register_flags_config(registry)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Configuration schemas for hexastack-flags.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Defines Pydantic configuration schemas loaded from [hexastack.flags].
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, Field
|
|
10
|
+
|
|
11
|
+
from hexastack_core.infra.decorators import config_section
|
|
12
|
+
from hexastack_core.infra.registries.config import ConfigRegistry
|
|
13
|
+
from hexastack_flags.domain.models import FeatureFlagProviderType
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"HexastackFlagsConfig",
|
|
17
|
+
"register_flags_config",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@config_section("flags")
|
|
22
|
+
class HexastackFlagsConfig(BaseModel):
|
|
23
|
+
"""Configuration options under [hexastack.flags]."""
|
|
24
|
+
|
|
25
|
+
provider: str = Field(
|
|
26
|
+
default=FeatureFlagProviderType.IN_MEMORY.value,
|
|
27
|
+
description="Feature flag provider backend: 'flagd', 'in_memory', 'env'",
|
|
28
|
+
)
|
|
29
|
+
host: str = Field(default="localhost", description="Flagd server hostname")
|
|
30
|
+
port: int = Field(default=8013, description="Flagd server port")
|
|
31
|
+
cache: bool = Field(
|
|
32
|
+
default=True, description="Enable local in-memory evaluation cache"
|
|
33
|
+
)
|
|
34
|
+
timeout_ms: int = Field(
|
|
35
|
+
default=5000, description="Evaluation timeout in milliseconds"
|
|
36
|
+
)
|
|
37
|
+
flags: dict[str, Any] = Field(
|
|
38
|
+
default_factory=dict,
|
|
39
|
+
description="Static in-memory feature flags mapping (key -> bool/value)",
|
|
40
|
+
)
|
|
41
|
+
options: dict[str, Any] = Field(
|
|
42
|
+
default_factory=dict,
|
|
43
|
+
description="Additional provider-specific configuration options",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def register_flags_config(registry: ConfigRegistry) -> None:
|
|
48
|
+
"""Register the flags configuration schema with the ConfigRegistry."""
|
|
49
|
+
registry.register_config_section("flags", HexastackFlagsConfig)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Ports module for hexastack_flags.
|
|
2
|
+
|
|
3
|
+
Notes/Architectural Intent:
|
|
4
|
+
Re-exports FeatureFlagPort from hexastack_core for hexagonal boundary consistency.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from hexastack_core.ports.feature_flags import FeatureFlagPort
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"FeatureFlagPort",
|
|
11
|
+
]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: hexastack-flags
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: CNCF OpenFeature provider adapters and enterprise feature flagging for Hexastack
|
|
5
|
+
Author: Richard West
|
|
6
|
+
Author-email: Richard West <dopplereffect.us@gmail.com>
|
|
7
|
+
Requires-Dist: hexastack-core
|
|
8
|
+
Requires-Dist: openfeature-sdk>=0.7.1
|
|
9
|
+
Requires-Dist: pydantic>=2.10.0
|
|
10
|
+
Requires-Dist: openfeature-provider-flagd>=0.2.0 ; extra == 'flagd'
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Provides-Extra: flagd
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
|
|
15
|
+
# hexastack-flags
|
|
16
|
+
|
|
17
|
+
**CNCF OpenFeature provider adapters and enterprise feature flagging for Hexastack.**
|
|
18
|
+
|
|
19
|
+
Part of the [Hexastack Framework](https://github.com/TheTrueSCU/hexastack).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. Architectural Overview
|
|
24
|
+
|
|
25
|
+
`hexastack-flags` provides the production-grade adapter connecting Hexastack's [`FeatureFlagPort`](file:///home/rjdw/Projects/hexastack/packages/hexastack_core/src/hexastack_core/ports/feature_flags.py) to the **CNCF OpenFeature Python SDK**.
|
|
26
|
+
|
|
27
|
+
It decouples your domain, CQRS handlers, REST routes, Typer commands, and GraphQL resolvers from proprietary feature flagging APIs, allowing seamless integration with **Flagd**, **Unleash**, **Flipt**, **GO Feature Flag**, or local in-memory providers.
|
|
28
|
+
|
|
29
|
+
```mermaid
|
|
30
|
+
graph TD
|
|
31
|
+
APP["Driving Adapters (FastAPI / CLI / GraphQL)"]
|
|
32
|
+
CQRS["CQRS Middleware Pipeline"]
|
|
33
|
+
PORT["FeatureFlagPort (hexastack-core)"]
|
|
34
|
+
ADAPTER["OpenFeatureFlagAdapter (hexastack-flags)"]
|
|
35
|
+
OF_SDK["CNCF OpenFeature SDK"]
|
|
36
|
+
|
|
37
|
+
subgraph Providers ["OpenFeature Providers"]
|
|
38
|
+
FLAGD["FlagdProvider (gRPC / Kubernetes)"]
|
|
39
|
+
UNLEASH["UnleashProvider"]
|
|
40
|
+
MEM["InMemoryProvider"]
|
|
41
|
+
ENV["EnvProvider"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
APP --> PORT
|
|
45
|
+
CQRS --> PORT
|
|
46
|
+
PORT -.-> ADAPTER
|
|
47
|
+
ADAPTER --> OF_SDK
|
|
48
|
+
OF_SDK --> FLAGD
|
|
49
|
+
OF_SDK --> UNLEASH
|
|
50
|
+
OF_SDK --> MEM
|
|
51
|
+
OF_SDK --> ENV
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 2. Quickstart
|
|
57
|
+
|
|
58
|
+
### Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Core OpenFeature adapter with In-Memory support
|
|
62
|
+
pip install hexastack-flags
|
|
63
|
+
|
|
64
|
+
# With Flagd provider
|
|
65
|
+
pip install "hexastack-flags[flagd]"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Configuration (`hexastack.toml` or `pyproject.toml`)
|
|
69
|
+
|
|
70
|
+
```toml
|
|
71
|
+
[hexastack.flags]
|
|
72
|
+
provider = "flagd" # "in_memory", "flagd", "env"
|
|
73
|
+
host = "localhost"
|
|
74
|
+
port = 8013
|
|
75
|
+
cache = true
|
|
76
|
+
timeout_ms = 5000
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## 3. Evaluation Examples
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from hexastack_core.ports.feature_flags import FeatureFlagPort
|
|
85
|
+
from hexastack_core.domain.feature_flags import EvaluationContext
|
|
86
|
+
|
|
87
|
+
flags = container.resolve(FeatureFlagPort)
|
|
88
|
+
|
|
89
|
+
# 1. Simple boolean toggle
|
|
90
|
+
if flags.is_enabled("features.checkout.v2", default=False):
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
# 2. Contextual evaluation (multi-tenant / user targeting)
|
|
94
|
+
ctx = EvaluationContext(user_id="usr_123", tenant_id="tenant-alpha")
|
|
95
|
+
theme = flags.get_string_value("features.ui.theme", default="light", context=ctx)
|
|
96
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
hexastack_flags/__init__.py,sha256=sILX7G9bFKzH8FXOMtylEyPK2Yqctt8-SGlXDpb9rZ0,331
|
|
2
|
+
hexastack_flags/adapters/__init__.py,sha256=zTMxW_o4w6cjS3GG6RlZXLpuP97mrnR1hJqhuSefops,117
|
|
3
|
+
hexastack_flags/adapters/openfeature.py,sha256=9NWacdLtSMU4OAy9ySBVdG_N_gc_2DvYPnrnZyhrEeE,7114
|
|
4
|
+
hexastack_flags/adapters/providers/__init__.py,sha256=ftKX_Y3qNNNKvLECwHim6Q5QMxXX2f1iTCnfftfLo-Y,141
|
|
5
|
+
hexastack_flags/adapters/providers/factory.py,sha256=TAxSNFMY_q5Ot1tuAH7b2rFvA0AW3wr1FIJglO-au1M,3201
|
|
6
|
+
hexastack_flags/domain/__init__.py,sha256=8oNhqS7YzqFbazlIjazuufiufUVIRZMWM71LAsGGNdE,173
|
|
7
|
+
hexastack_flags/domain/models.py,sha256=7G3MxJc1tWGWQ0OguE6wNp1iheFQm9ErZvIqOcXrULU,852
|
|
8
|
+
hexastack_flags/infra/__init__.py,sha256=M4f5otuwkSMj-R4ocHIoPxdWlJtomJBt5lLBvdKJZN8,256
|
|
9
|
+
hexastack_flags/infra/bootstrap.py,sha256=SEAUYDPWciyiels127MVHN6O1I7paLzTqVVdNgWFYkE,2004
|
|
10
|
+
hexastack_flags/infra/config.py,sha256=Q1OJcJzRk_IFD5vuWN0l9XCq9exywuYEzc8TCq9tZdc,1644
|
|
11
|
+
hexastack_flags/ports/__init__.py,sha256=YqsjRH_ch1AOx0TTNZhJzSez07vvABX-iXTBj6fDG7k,259
|
|
12
|
+
hexastack_flags-0.0.0.dist-info/WHEEL,sha256=EmLkUISDECbcUx3FMCYOqokNOJqNp2r0d4mJzjErvvs,80
|
|
13
|
+
hexastack_flags-0.0.0.dist-info/entry_points.txt,sha256=fQ38RyM0PSxp8D1cP1vRZ-gkNeAg2Gm7KuZEuX1MAdw,91
|
|
14
|
+
hexastack_flags-0.0.0.dist-info/METADATA,sha256=IDKaXc9IKMW2SAzyzQoIsYTrMuLePaS2OQJ9x2E94Z8,2733
|
|
15
|
+
hexastack_flags-0.0.0.dist-info/RECORD,,
|