Skip to content

Commit 55f8b69

Browse files
authored
Merge branch 'main' into fix/flagd-sync-port-config
2 parents a74547d + daafd73 commit 55f8b69

65 files changed

Lines changed: 3694 additions & 1142 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,15 @@ jobs:
5757
providers/openfeature-provider-unleash:
5858
- 'providers/openfeature-provider-unleash/**'
5959
- 'uv.lock'
60+
tools/openfeature-flagd-api:
61+
- 'tools/openfeature-flagd-api/**'
62+
- 'uv.lock'
63+
tools/openfeature-flagd-core:
64+
- 'tools/openfeature-flagd-core/**'
65+
- 'uv.lock'
66+
tools/openfeature-flagd-api-testkit:
67+
- 'tools/openfeature-flagd-api-testkit/**'
68+
- 'uv.lock'
6069
6170
build:
6271
needs: changes

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
url = https://github.com/open-feature/spec
77
[submodule "providers/openfeature-provider-flagd/openfeature/test-harness"]
88
path = providers/openfeature-provider-flagd/openfeature/test-harness
9-
url = https://github.com/open-feature/flagd-testbed.git
9+
url = https://github.com/open-feature/flagd-testbed.git

.release-please-manifest.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
{
22
"hooks/openfeature-hooks-opentelemetry": "0.3.1",
33
"providers/openfeature-provider-aws-ssm": "0.1.1",
4-
"providers/openfeature-provider-flagd": "0.4.0",
5-
"providers/openfeature-provider-ofrep": "0.2.0",
4+
"providers/openfeature-provider-ofrep": "0.3.0",
5+
"providers/openfeature-provider-flagd": "0.4.1",
66
"providers/openfeature-provider-flipt": "0.1.3",
77
"providers/openfeature-provider-env-var": "0.1.1",
8-
"providers/openfeature-provider-unleash": "0.1.2"
8+
"providers/openfeature-provider-unleash": "0.1.2",
9+
"tools/openfeature-flagd-api": "1.0.0",
10+
"tools/openfeature-flagd-core": "1.0.0",
11+
"tools/openfeature-flagd-api-testkit": "0.1.0"
912
}

providers/openfeature-provider-flagd/CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## [0.4.1](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.4.0...openfeature-provider-flagd/v0.4.1) (2026-04-30)
4+
5+
6+
### 🐛 Bug Fixes
7+
8+
* various custom operator conformance fixes ([#386](https://github.com/open-feature/python-sdk-contrib/issues/386)) ([c119a77](https://github.com/open-feature/python-sdk-contrib/commit/c119a774736dd4d48a4dc82d158aafc17cc3936f))
9+
10+
11+
### ✨ New Features
12+
13+
* **flagd:** extract evaluator into api, core, and testkit packages ([#377](https://github.com/open-feature/python-sdk-contrib/issues/377)) ([1995534](https://github.com/open-feature/python-sdk-contrib/commit/1995534c2545b8c65d2944e3e52ce19a552b4815))
14+
315
## [0.4.0](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.3.0...openfeature-provider-flagd/v0.4.0) (2026-04-01)
416

517

providers/openfeature-provider-flagd/pyproject.toml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
55

66
[project]
77
name = "openfeature-provider-flagd"
8-
version = "0.4.0"
8+
version = "0.4.1"
99
description = "OpenFeature provider for the flagd flag evaluation engine"
1010
readme = "README.md"
1111
authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]
@@ -18,11 +18,9 @@ classifiers = [
1818
keywords = []
1919
dependencies = [
2020
"openfeature-sdk>=0.8.2",
21+
"openfeature-flagd-core>=1.0.0,<2",
2122
"grpcio>=1.80.0",
2223
"protobuf>=6.30.0,<7.0.0",
23-
"mmh3>=5.0.0,<6.0.0",
24-
"panzi-json-logic>=1.0.1",
25-
"semver>=3,<4",
2624
"pyyaml>=6.0.1",
2725
"cachebox>=5.1.0,<6.0.0",
2826
]
@@ -33,7 +31,7 @@ Homepage = "https://github.com/open-feature/python-sdk-contrib"
3331

3432
[dependency-groups]
3533
dev = [
36-
"asserts>=0.13.0,<0.14.0",
34+
"asserts>=0.14.0,<0.15.0",
3735
"coverage[toml]>=7.10.0,<8.0.0",
3836
"grpcio-health-checking>=1.80.0,<2.0.0",
3937
"mypy>=1.18.0,<2.0.0",
@@ -109,6 +107,9 @@ module = [
109107
]
110108
warn_unused_ignores = false
111109

110+
[tool.uv.sources]
111+
openfeature-flagd-core = { workspace = true }
112+
112113
[tool.pytest]
113114
strict = true
114115

providers/openfeature-provider-flagd/pytest.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[pytest]
2+
addopts = -m "not fractional-v1"
23
markers =
34
rpc: tests for rpc mode.
45
in-process: tests for in-process mode.
Lines changed: 50 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,39 @@
1+
import json
12
import typing
23

3-
from openfeature.contrib.provider.flagd.resolvers.process.connector.file_watcher import (
4-
FileWatcher,
5-
)
4+
from openfeature.contrib.tools.flagd.core import FlagdCore
65
from openfeature.evaluation_context import EvaluationContext
76
from openfeature.event import ProviderEventDetails
8-
from openfeature.exception import FlagNotFoundError, GeneralError, ParseError
9-
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType, Reason
7+
from openfeature.flag_evaluation import FlagResolutionDetails, FlagValueType
108

119
from ..config import Config
1210
from .process.connector import FlagStateConnector
11+
from .process.connector.file_watcher import FileWatcher
1312
from .process.connector.grpc_watcher import GrpcWatcher
14-
from .process.flags import Flag, FlagStore
15-
from .process.targeting import targeting
1613

1714
T = typing.TypeVar("T")
1815

1916

20-
def _merge_metadata(
21-
flag_metadata: typing.Mapping[str, float | int | str | bool] | None,
22-
flag_set_metadata: typing.Mapping[str, float | int | str | bool] | None,
23-
) -> typing.Mapping[str, float | int | str | bool]:
24-
metadata = {} if flag_set_metadata is None else dict(flag_set_metadata)
17+
class _FlagStoreAdapter:
18+
"""Bridges FlagdCore with connectors that expect a FlagStore-like update() interface."""
2519

26-
if flag_metadata is not None:
27-
for key, value in flag_metadata.items():
28-
metadata[key] = value
29-
30-
return metadata
20+
def __init__(
21+
self,
22+
evaluator: FlagdCore,
23+
emit_provider_configuration_changed: typing.Callable[
24+
[ProviderEventDetails], None
25+
],
26+
):
27+
self.evaluator = evaluator
28+
self.emit_provider_configuration_changed = emit_provider_configuration_changed
29+
30+
def update(self, flags_data: dict) -> None:
31+
json_str = json.dumps(flags_data)
32+
changed_keys = self.evaluator.set_flags_and_get_changed_keys(json_str)
33+
metadata = self.evaluator.get_flag_set_metadata()
34+
self.emit_provider_configuration_changed(
35+
ProviderEventDetails(flags_changed=changed_keys, metadata=dict(metadata))
36+
)
3137

3238

3339
class InProcessResolver:
@@ -42,15 +48,25 @@ def __init__(
4248
],
4349
):
4450
self.config = config
45-
self.flag_store = FlagStore(emit_provider_configuration_changed)
51+
self.evaluator = FlagdCore()
52+
53+
# Adapter lets connectors push flag data to FlagdCore via the
54+
# same .update(dict) interface they used with the old FlagStore.
55+
flag_store_adapter = _FlagStoreAdapter(
56+
self.evaluator, emit_provider_configuration_changed
57+
)
58+
4659
self.connector: FlagStateConnector = (
4760
FileWatcher(
48-
self.config, self.flag_store, emit_provider_ready, emit_provider_error
61+
self.config,
62+
flag_store_adapter, # type: ignore[arg-type]
63+
emit_provider_ready,
64+
emit_provider_error,
4965
)
5066
if self.config.offline_flag_source_path
5167
else GrpcWatcher(
5268
self.config,
53-
self.flag_store,
69+
flag_store_adapter, # type: ignore[arg-type]
5470
emit_provider_ready,
5571
emit_provider_error,
5672
emit_provider_stale,
@@ -69,34 +85,39 @@ def resolve_boolean_details(
6985
default_value: bool,
7086
evaluation_context: EvaluationContext | None = None,
7187
) -> FlagResolutionDetails[bool]:
72-
return self._resolve(key, default_value, evaluation_context)
88+
return self.evaluator.resolve_boolean_value(
89+
key, default_value, evaluation_context
90+
)
7391

7492
def resolve_string_details(
7593
self,
7694
key: str,
7795
default_value: str,
7896
evaluation_context: EvaluationContext | None = None,
7997
) -> FlagResolutionDetails[str]:
80-
return self._resolve(key, default_value, evaluation_context)
98+
return self.evaluator.resolve_string_value(
99+
key, default_value, evaluation_context
100+
)
81101

82102
def resolve_float_details(
83103
self,
84104
key: str,
85105
default_value: float,
86106
evaluation_context: EvaluationContext | None = None,
87107
) -> FlagResolutionDetails[float]:
88-
result = self._resolve(key, default_value, evaluation_context)
89-
if isinstance(result.value, int):
90-
result.value = float(result.value)
91-
return result
108+
return self.evaluator.resolve_float_value(
109+
key, default_value, evaluation_context
110+
)
92111

93112
def resolve_integer_details(
94113
self,
95114
key: str,
96115
default_value: int,
97116
evaluation_context: EvaluationContext | None = None,
98117
) -> FlagResolutionDetails[int]:
99-
return self._resolve(key, default_value, evaluation_context)
118+
return self.evaluator.resolve_integer_value(
119+
key, default_value, evaluation_context
120+
)
100121

101122
def resolve_object_details(
102123
self,
@@ -107,75 +128,6 @@ def resolve_object_details(
107128
) -> FlagResolutionDetails[
108129
typing.Sequence[FlagValueType] | typing.Mapping[str, FlagValueType]
109130
]:
110-
return self._resolve(key, default_value, evaluation_context)
111-
112-
def _resolve(
113-
self,
114-
key: str,
115-
default_value: T,
116-
evaluation_context: EvaluationContext | None = None,
117-
) -> FlagResolutionDetails[T]:
118-
flag = self.flag_store.get_flag(key)
119-
if not flag:
120-
raise FlagNotFoundError(f"Flag with key {key} not present in flag store.")
121-
122-
metadata = _merge_metadata(flag.metadata, self.flag_store.flag_set_metadata)
123-
124-
if flag.state == "DISABLED":
125-
return FlagResolutionDetails(
126-
default_value, flag_metadata=metadata, reason=Reason.DISABLED
127-
)
128-
129-
if not flag.targeting:
130-
return _default_resolve(flag, metadata, Reason.STATIC, default_value)
131-
132-
try:
133-
variant = targeting(flag.key, flag.targeting, evaluation_context)
134-
if variant is None:
135-
return _default_resolve(flag, metadata, Reason.DEFAULT, default_value)
136-
137-
# convert to string to support shorthand (boolean in python is with capital T hence the special case)
138-
if isinstance(variant, bool):
139-
variant = str(variant).lower()
140-
elif not isinstance(variant, str):
141-
variant = str(variant)
142-
143-
if variant not in flag.variants:
144-
raise GeneralError(
145-
f"Resolved variant {variant} not in variants config."
146-
)
147-
148-
except ReferenceError:
149-
raise ParseError(f"Invalid targeting {targeting}") from ReferenceError
150-
151-
variant, value = flag.get_variant(variant)
152-
if value is None:
153-
raise GeneralError(f"Resolved variant {variant} not in variants config.")
154-
155-
return FlagResolutionDetails(
156-
value,
157-
variant=variant,
158-
reason=Reason.TARGETING_MATCH,
159-
flag_metadata=metadata,
160-
)
161-
162-
163-
def _default_resolve(
164-
flag: Flag,
165-
metadata: typing.Mapping[str, float | int | str | bool],
166-
reason: Reason,
167-
default_value: typing.Any = None,
168-
) -> FlagResolutionDetails:
169-
variant, value = flag.default
170-
if variant is None:
171-
return FlagResolutionDetails(
172-
default_value,
173-
variant=variant,
174-
reason=Reason.DEFAULT,
175-
flag_metadata=metadata,
131+
return self.evaluator.resolve_object_value(
132+
key, default_value, evaluation_context
176133
)
177-
if variant not in flag.variants:
178-
raise GeneralError(f"Resolved variant {variant} not in variants config.")
179-
return FlagResolutionDetails(
180-
value, variant=variant, flag_metadata=metadata, reason=reason
181-
)

0 commit comments

Comments
 (0)