openfeature-provider-flagd 0.2.0__tar.gz → 0.2.2__tar.gz

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.
Files changed (85) hide show
  1. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/CHANGELOG.md +22 -0
  2. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/PKG-INFO +1 -1
  3. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/pyproject.toml +2 -2
  4. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/config.py +30 -0
  5. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/provider.py +29 -1
  6. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +5 -2
  7. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +2 -2
  8. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +3 -2
  9. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +45 -6
  10. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +2 -4
  11. openfeature_provider_flagd-0.2.2/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py +14 -0
  12. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.py +1 -1
  13. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.py +1 -1
  14. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.py +1 -1
  15. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.py +1 -1
  16. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/file/conftest.py +1 -0
  17. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/step/event_steps.py +4 -2
  18. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/step/provider_steps.py +3 -1
  19. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/testfilter.py +3 -1
  20. openfeature_provider_flagd-0.2.2/tests/test_in_process.py +218 -0
  21. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/.gitignore +0 -0
  22. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/LICENSE +0 -0
  23. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/README.md +0 -0
  24. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/pytest.ini +0 -0
  25. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
  26. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
  27. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/__init__.py +0 -0
  28. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/__init__.py +0 -0
  29. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +0 -0
  30. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/process/targeting.py +0 -0
  31. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/contrib/provider/flagd/resolvers/protocol.py +0 -0
  32. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.py +0 -0
  33. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.pyi +0 -0
  34. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.pyi +0 -0
  35. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.py +0 -0
  36. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.pyi +0 -0
  37. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.pyi +0 -0
  38. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.py +0 -0
  39. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.pyi +0 -0
  40. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.pyi +0 -0
  41. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.py +0 -0
  42. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.pyi +0 -0
  43. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.pyi +0 -0
  44. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/__init__.py +0 -0
  45. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/conftest.py +0 -0
  46. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/__init__.py +0 -0
  47. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/conftest.py +0 -0
  48. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/file/__init__.py +0 -0
  49. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/file/test_flaqd.py +0 -0
  50. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/flagd_container.py +0 -0
  51. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/inprocess/__init__.py +0 -0
  52. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/inprocess/conftest.py +0 -0
  53. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/inprocess/test_flaqd.py +0 -0
  54. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/parsers.py +0 -0
  55. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/paths.py +0 -0
  56. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/rpc/__init__.py +0 -0
  57. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/rpc/conftest.py +0 -0
  58. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/rpc/test_flaqd.py +0 -0
  59. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/step/_utils.py +0 -0
  60. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/step/config_steps.py +0 -0
  61. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/step/context_steps.py +0 -0
  62. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/e2e/step/flag_step.py +0 -0
  63. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-broken-default.json +0 -0
  64. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-broken-state.json +0 -0
  65. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-broken-targeting.json +0 -0
  66. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-broken-variants.json +0 -0
  67. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-disabled.json +0 -0
  68. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-invalid.not-json +0 -0
  69. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-no-state.json +0 -0
  70. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-wrong-structure.json +0 -0
  71. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag-wrong-variant.json +0 -0
  72. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag.json +0 -0
  73. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/basic-flag.yaml +0 -0
  74. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-fractional-args-wrong-content.json +0 -0
  75. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-fractional-args.json +0 -0
  76. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-fractional-weights-strings.json +0 -0
  77. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-fractional-weights.json +0 -0
  78. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-semver-args.json +0 -0
  79. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-semver-op.json +0 -0
  80. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/flags/invalid-stringcomp-args.json +0 -0
  81. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/test_config.py +0 -0
  82. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/test_errors.py +0 -0
  83. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/test_file_store.py +0 -0
  84. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/test_flagd.py +0 -0
  85. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.2}/tests/test_targeting.py +0 -0
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.2](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.1...openfeature-provider-flagd/v0.2.2) (2025-03-18)
4
+
5
+
6
+ ### 🐛 Bug Fixes
7
+
8
+ * **flagd:** handle falsy target values correctly ([#214](https://github.com/open-feature/python-sdk-contrib/issues/214)) ([fafd099](https://github.com/open-feature/python-sdk-contrib/commit/fafd099f07365a7d0032e8215477b51bfe90c01a))
9
+
10
+
11
+ ### 🧹 Chore
12
+
13
+ * **deps:** update dependency grpcio-health-checking to v1.71.0 ([#209](https://github.com/open-feature/python-sdk-contrib/issues/209)) ([345e793](https://github.com/open-feature/python-sdk-contrib/commit/345e7934b9de3879d3aff45c8213ece1a98e3711))
14
+ * **deps:** update pre-commit hook astral-sh/ruff-pre-commit to v0.11.0 ([#212](https://github.com/open-feature/python-sdk-contrib/issues/212)) ([1b9b5f1](https://github.com/open-feature/python-sdk-contrib/commit/1b9b5f128a7fe08ffbf84cbc7de2986f95dc01f5))
15
+ * **flagd:** Add sync metadata disabled ([#211](https://github.com/open-feature/python-sdk-contrib/issues/211)) ([2f85057](https://github.com/open-feature/python-sdk-contrib/commit/2f850574943cc92d55d198c8ccd91e80583a2ee6))
16
+
17
+ ## [0.2.1](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.0...openfeature-provider-flagd/v0.2.1) (2025-03-10)
18
+
19
+
20
+ ### ✨ New Features
21
+
22
+ * **flagd:** Add features to customize auth to Sync API servers ([#203](https://github.com/open-feature/python-sdk-contrib/issues/203)) ([5151e94](https://github.com/open-feature/python-sdk-contrib/commit/5151e941d229101bdbcc5b40f570f69d77ddda7b))
23
+ * **flagd:** Context value hydration ([#195](https://github.com/open-feature/python-sdk-contrib/issues/195)) ([4fa619b](https://github.com/open-feature/python-sdk-contrib/commit/4fa619b93faf1d1f62a9ead99f33baa21c04e267))
24
+
3
25
  ## [0.2.0](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.1.5...openfeature-provider-flagd/v0.2.0) (2025-02-18)
4
26
 
5
27
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openfeature-provider-flagd
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: OpenFeature provider for the flagd flag evaluation engine
5
5
  Project-URL: Homepage, https://github.com/open-feature/python-sdk-contrib
6
6
  Author-email: OpenFeature <openfeature-core@groups.io>
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "openfeature-provider-flagd"
8
- version = "0.2.0"
8
+ version = "0.2.2"
9
9
  description = "OpenFeature provider for the flagd flag evaluation engine"
10
10
  readme = "README.md"
11
11
  authors = [{ name = "OpenFeature", email = "openfeature-core@groups.io" }]
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "pytest-bdd",
41
41
  "testcontainers",
42
42
  "asserts",
43
- "grpcio-health-checking==1.70.0",
43
+ "grpcio-health-checking==1.71.0",
44
44
  ]
45
45
  pre-install-commands = [
46
46
  "hatch build",
@@ -3,6 +3,8 @@ import os
3
3
  import typing
4
4
  from enum import Enum
5
5
 
6
+ import grpc
7
+
6
8
 
7
9
  class ResolverType(Enum):
8
10
  RPC = "rpc"
@@ -45,9 +47,11 @@ ENV_VAR_RETRY_BACKOFF_MS = "FLAGD_RETRY_BACKOFF_MS"
45
47
  ENV_VAR_RETRY_BACKOFF_MAX_MS = "FLAGD_RETRY_BACKOFF_MAX_MS"
46
48
  ENV_VAR_RETRY_GRACE_PERIOD_SECONDS = "FLAGD_RETRY_GRACE_PERIOD"
47
49
  ENV_VAR_SELECTOR = "FLAGD_SOURCE_SELECTOR"
50
+ ENV_VAR_PROVIDER_ID = "FLAGD_SOURCE_PROVIDER_ID"
48
51
  ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS"
49
52
  ENV_VAR_TLS = "FLAGD_TLS"
50
53
  ENV_VAR_TLS_CERT = "FLAGD_SERVER_CERT_PATH"
54
+ ENV_VAR_DEFAULT_AUTHORITY = "FLAGD_DEFAULT_AUTHORITY"
51
55
 
52
56
  T = typing.TypeVar("T")
53
57
 
@@ -81,6 +85,7 @@ class Config:
81
85
  port: typing.Optional[int] = None,
82
86
  tls: typing.Optional[bool] = None,
83
87
  selector: typing.Optional[str] = None,
88
+ provider_id: typing.Optional[str] = None,
84
89
  resolver: typing.Optional[ResolverType] = None,
85
90
  offline_flag_source_path: typing.Optional[str] = None,
86
91
  offline_poll_interval_ms: typing.Optional[int] = None,
@@ -93,6 +98,9 @@ class Config:
93
98
  cache: typing.Optional[CacheType] = None,
94
99
  max_cache_size: typing.Optional[int] = None,
95
100
  cert_path: typing.Optional[str] = None,
101
+ default_authority: typing.Optional[str] = None,
102
+ channel_credentials: typing.Optional[grpc.ChannelCredentials] = None,
103
+ sync_metadata_disabled: typing.Optional[bool] = None,
96
104
  ):
97
105
  self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host
98
106
 
@@ -227,3 +235,25 @@ class Config:
227
235
  self.selector = (
228
236
  env_or_default(ENV_VAR_SELECTOR, None) if selector is None else selector
229
237
  )
238
+
239
+ self.provider_id = (
240
+ env_or_default(ENV_VAR_PROVIDER_ID, None)
241
+ if provider_id is None
242
+ else provider_id
243
+ )
244
+
245
+ self.default_authority = (
246
+ env_or_default(ENV_VAR_DEFAULT_AUTHORITY, None)
247
+ if default_authority is None
248
+ else default_authority
249
+ )
250
+
251
+ self.channel_credentials = channel_credentials
252
+
253
+ # TODO: remove the metadata call entirely after https://github.com/open-feature/flagd/issues/1584
254
+ # This is a temporary stop-gap solutions to support servers that don't implement sync.GetMetadata
255
+ # (see: https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata).
256
+ # Using this option disables call to sync.GetMetadata
257
+ # Disabling will prevent static context from flagd being used in evaluations.
258
+ # GetMetadata and this option will be removed.
259
+ self.sync_metadata_disabled = sync_metadata_disabled
@@ -24,13 +24,18 @@
24
24
  import typing
25
25
  import warnings
26
26
 
27
+ import grpc
28
+
27
29
  from openfeature.evaluation_context import EvaluationContext
30
+ from openfeature.event import ProviderEventDetails
28
31
  from openfeature.flag_evaluation import FlagResolutionDetails
32
+ from openfeature.hook import Hook
29
33
  from openfeature.provider import AbstractProvider
30
34
  from openfeature.provider.metadata import Metadata
31
35
 
32
36
  from .config import CacheType, Config, ResolverType
33
37
  from .resolvers import AbstractResolver, GrpcResolver, InProcessResolver
38
+ from .sync_metadata_hook import SyncMetadataHook
34
39
 
35
40
  T = typing.TypeVar("T")
36
41
 
@@ -47,6 +52,7 @@ class FlagdProvider(AbstractProvider):
47
52
  timeout: typing.Optional[int] = None,
48
53
  retry_backoff_ms: typing.Optional[int] = None,
49
54
  selector: typing.Optional[str] = None,
55
+ provider_id: typing.Optional[str] = None,
50
56
  resolver_type: typing.Optional[ResolverType] = None,
51
57
  offline_flag_source_path: typing.Optional[str] = None,
52
58
  stream_deadline_ms: typing.Optional[int] = None,
@@ -56,6 +62,9 @@ class FlagdProvider(AbstractProvider):
56
62
  retry_backoff_max_ms: typing.Optional[int] = None,
57
63
  retry_grace_period: typing.Optional[int] = None,
58
64
  cert_path: typing.Optional[str] = None,
65
+ default_authority: typing.Optional[str] = None,
66
+ channel_credentials: typing.Optional[grpc.ChannelCredentials] = None,
67
+ sync_metadata_disabled: typing.Optional[bool] = None,
59
68
  ):
60
69
  """
61
70
  Create an instance of the FlagdProvider
@@ -88,6 +97,7 @@ class FlagdProvider(AbstractProvider):
88
97
  retry_backoff_max_ms=retry_backoff_max_ms,
89
98
  retry_grace_period=retry_grace_period,
90
99
  selector=selector,
100
+ provider_id=provider_id,
91
101
  resolver=resolver_type,
92
102
  offline_flag_source_path=offline_flag_source_path,
93
103
  stream_deadline_ms=stream_deadline_ms,
@@ -95,9 +105,20 @@ class FlagdProvider(AbstractProvider):
95
105
  cache=cache,
96
106
  max_cache_size=max_cache_size,
97
107
  cert_path=cert_path,
108
+ default_authority=default_authority,
109
+ channel_credentials=channel_credentials,
110
+ sync_metadata_disabled=sync_metadata_disabled,
98
111
  )
112
+ self.enriched_context: dict = {}
99
113
 
100
114
  self.resolver = self.setup_resolver()
115
+ self.hooks: list[Hook] = [SyncMetadataHook(self.get_enriched_context)]
116
+
117
+ def get_enriched_context(self) -> EvaluationContext:
118
+ return EvaluationContext(attributes=self.enriched_context)
119
+
120
+ def get_provider_hooks(self) -> list[Hook]:
121
+ return self.hooks
101
122
 
102
123
  def setup_resolver(self) -> AbstractResolver:
103
124
  if self.config.resolver == ResolverType.RPC:
@@ -114,7 +135,7 @@ class FlagdProvider(AbstractProvider):
114
135
  ):
115
136
  return InProcessResolver(
116
137
  self.config,
117
- self.emit_provider_ready,
138
+ self.emit_provider_ready_with_context,
118
139
  self.emit_provider_error,
119
140
  self.emit_provider_stale,
120
141
  self.emit_provider_configuration_changed,
@@ -184,3 +205,10 @@ class FlagdProvider(AbstractProvider):
184
205
  return self.resolver.resolve_object_details(
185
206
  key, default_value, evaluation_context
186
207
  )
208
+
209
+ def emit_provider_ready_with_context(
210
+ self, details: ProviderEventDetails, context: dict
211
+ ) -> None:
212
+ self.enriched_context = context
213
+ self.emit_provider_ready(details)
214
+ pass
@@ -137,7 +137,10 @@ class GrpcResolver:
137
137
 
138
138
  def _state_change_callback(self, new_state: ChannelConnectivity) -> None:
139
139
  logger.debug(f"gRPC state change: {new_state}")
140
- if new_state == ChannelConnectivity.READY:
140
+ if (
141
+ new_state == grpc.ChannelConnectivity.READY
142
+ or new_state == grpc.ChannelConnectivity.IDLE
143
+ ):
141
144
  if not self.thread or not self.thread.is_alive():
142
145
  self.thread = threading.Thread(
143
146
  target=self.listen,
@@ -276,7 +279,7 @@ class GrpcResolver:
276
279
  return cached_flag
277
280
 
278
281
  context = self._convert_context(evaluation_context)
279
- call_args = {"timeout": self.deadline}
282
+ call_args = {"timeout": self.deadline, "wait_for_ready": True}
280
283
  try:
281
284
  request: Message
282
285
  if flag_type == FlagType.BOOLEAN:
@@ -21,7 +21,7 @@ class InProcessResolver:
21
21
  def __init__(
22
22
  self,
23
23
  config: Config,
24
- emit_provider_ready: typing.Callable[[ProviderEventDetails], None],
24
+ emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None],
25
25
  emit_provider_error: typing.Callable[[ProviderEventDetails], None],
26
26
  emit_provider_stale: typing.Callable[[ProviderEventDetails], None],
27
27
  emit_provider_configuration_changed: typing.Callable[
@@ -121,7 +121,7 @@ class InProcessResolver:
121
121
  )
122
122
 
123
123
  variant, value = flag.get_variant(variant)
124
- if not value:
124
+ if value is None:
125
125
  raise ParseError(f"Resolved variant {variant} not in variants config.")
126
126
 
127
127
  return FlagResolutionDetails(
@@ -24,7 +24,7 @@ class FileWatcher(FlagStateConnector):
24
24
  self,
25
25
  config: Config,
26
26
  flag_store: FlagStore,
27
- emit_provider_ready: typing.Callable[[ProviderEventDetails], None],
27
+ emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None],
28
28
  emit_provider_error: typing.Callable[[ProviderEventDetails], None],
29
29
  ):
30
30
  if config.offline_flag_source_path is None:
@@ -94,7 +94,8 @@ class FileWatcher(FlagStateConnector):
94
94
  self.emit_provider_ready(
95
95
  ProviderEventDetails(
96
96
  message="Reloading file contents recovered from error state"
97
- )
97
+ ),
98
+ {},
98
99
  )
99
100
  self.should_emit_ready_on_success = False
100
101
 
@@ -5,6 +5,8 @@ import time
5
5
  import typing
6
6
 
7
7
  import grpc
8
+ from google.protobuf.json_format import MessageToDict
9
+ from google.protobuf.struct_pb2 import Struct
8
10
 
9
11
  from openfeature.evaluation_context import EvaluationContext
10
12
  from openfeature.event import ProviderEventDetails
@@ -26,7 +28,7 @@ class GrpcWatcher(FlagStateConnector):
26
28
  self,
27
29
  config: Config,
28
30
  flag_store: FlagStore,
29
- emit_provider_ready: typing.Callable[[ProviderEventDetails], None],
31
+ emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None],
30
32
  emit_provider_error: typing.Callable[[ProviderEventDetails], None],
31
33
  emit_provider_stale: typing.Callable[[ProviderEventDetails], None],
32
34
  ):
@@ -41,6 +43,7 @@ class GrpcWatcher(FlagStateConnector):
41
43
  self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001
42
44
  self.deadline = config.deadline_ms * 0.001
43
45
  self.selector = config.selector
46
+ self.provider_id = config.provider_id
44
47
  self.emit_provider_ready = emit_provider_ready
45
48
  self.emit_provider_error = emit_provider_error
46
49
  self.emit_provider_stale = emit_provider_stale
@@ -54,13 +57,23 @@ class GrpcWatcher(FlagStateConnector):
54
57
  def _generate_channel(self, config: Config) -> grpc.Channel:
55
58
  target = f"{config.host}:{config.port}"
56
59
  # Create the channel with the service config
57
- options = [
60
+ options: list[tuple[str, typing.Any]] = [
58
61
  ("grpc.keepalive_time_ms", config.keep_alive_time),
59
62
  ("grpc.initial_reconnect_backoff_ms", config.retry_backoff_ms),
60
63
  ("grpc.max_reconnect_backoff_ms", config.retry_backoff_max_ms),
61
64
  ("grpc.min_reconnect_backoff_ms", config.stream_deadline_ms),
62
65
  ]
63
- if config.tls:
66
+ if config.default_authority is not None:
67
+ options.append(("grpc.default_authority", config.default_authority))
68
+
69
+ if config.channel_credentials is not None:
70
+ channel_args = {
71
+ "options": options,
72
+ "credentials": config.channel_credentials,
73
+ }
74
+ channel = grpc.secure_channel(target, **channel_args)
75
+
76
+ elif config.tls:
64
77
  channel_args = {
65
78
  "options": options,
66
79
  "credentials": grpc.ssl_channel_credentials(),
@@ -106,7 +119,10 @@ class GrpcWatcher(FlagStateConnector):
106
119
 
107
120
  def _state_change_callback(self, new_state: grpc.ChannelConnectivity) -> None:
108
121
  logger.debug(f"gRPC state change: {new_state}")
109
- if new_state == grpc.ChannelConnectivity.READY:
122
+ if (
123
+ new_state == grpc.ChannelConnectivity.READY
124
+ or new_state == grpc.ChannelConnectivity.IDLE
125
+ ):
110
126
  if not self.thread or not self.thread.is_alive():
111
127
  self.thread = threading.Thread(
112
128
  target=self.listen,
@@ -147,16 +163,38 @@ class GrpcWatcher(FlagStateConnector):
147
163
  self.active = False
148
164
  self.channel.close()
149
165
 
166
+ def _create_request_args(self) -> dict:
167
+ request_args = {}
168
+ if self.selector is not None:
169
+ request_args["selector"] = self.selector
170
+ if self.provider_id is not None:
171
+ request_args["provider_id"] = self.provider_id
172
+
173
+ return request_args
174
+
150
175
  def listen(self) -> None:
151
176
  call_args = (
152
177
  {"timeout": self.streamline_deadline_seconds}
153
178
  if self.streamline_deadline_seconds > 0
154
179
  else {}
155
180
  )
156
- request_args = {"selector": self.selector} if self.selector is not None else {}
181
+ request_args = self._create_request_args()
157
182
 
158
183
  while self.active:
159
184
  try:
185
+ context_values_response: sync_pb2.GetMetadataResponse
186
+ if self.config.sync_metadata_disabled:
187
+ context_values_response = sync_pb2.GetMetadataResponse(
188
+ metadata=Struct()
189
+ )
190
+ else:
191
+ context_values_request = sync_pb2.GetMetadataRequest()
192
+ context_values_response = self.stub.GetMetadata(
193
+ context_values_request, wait_for_ready=True
194
+ )
195
+
196
+ context_values = MessageToDict(context_values_response)
197
+
160
198
  request = sync_pb2.SyncFlagsRequest(**request_args)
161
199
 
162
200
  logger.debug("Setting up gRPC sync flags connection")
@@ -173,7 +211,8 @@ class GrpcWatcher(FlagStateConnector):
173
211
  self.emit_provider_ready(
174
212
  ProviderEventDetails(
175
213
  message="gRPC sync connection established"
176
- )
214
+ ),
215
+ context_values["metadata"],
177
216
  )
178
217
  self.connected = True
179
218
 
@@ -72,10 +72,8 @@ class Flag:
72
72
  data["default_variant"] = data["defaultVariant"]
73
73
  del data["defaultVariant"]
74
74
 
75
- if "source" in data:
76
- del data["source"]
77
- if "selector" in data:
78
- del data["selector"]
75
+ data.pop("source", None)
76
+ data.pop("selector", None)
79
77
  try:
80
78
  flag = cls(key=key, **data)
81
79
  return flag
@@ -0,0 +1,14 @@
1
+ import typing
2
+
3
+ from openfeature.evaluation_context import EvaluationContext
4
+ from openfeature.hook import Hook, HookContext, HookHints
5
+
6
+
7
+ class SyncMetadataHook(Hook):
8
+ def __init__(self, context_supplier: typing.Callable[[], EvaluationContext]):
9
+ self.context_supplier = context_supplier
10
+
11
+ def before(
12
+ self, hook_context: HookContext, hints: HookHints
13
+ ) -> typing.Optional[EvaluationContext]:
14
+ return self.context_supplier()
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from openfeature.schemas.protobuf.flagd.evaluation.v1 import evaluation_pb2 as openfeature_dot_schemas_dot_protobuf_dot_flagd_dot_evaluation_dot_v1_dot_evaluation__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.70.0'
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from openfeature.schemas.protobuf.flagd.sync.v1 import sync_pb2 as openfeature_dot_schemas_dot_protobuf_dot_flagd_dot_sync_dot_v1_dot_sync__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.70.0'
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from openfeature.schemas.protobuf.schema.v1 import schema_pb2 as openfeature_dot_schemas_dot_protobuf_dot_schema_dot_v1_dot_schema__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.70.0'
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -5,7 +5,7 @@ import warnings
5
5
 
6
6
  from openfeature.schemas.protobuf.sync.v1 import sync_service_pb2 as openfeature_dot_schemas_dot_protobuf_dot_sync_dot_v1_dot_sync__service__pb2
7
7
 
8
- GRPC_GENERATED_VERSION = '1.70.0'
8
+ GRPC_GENERATED_VERSION = '1.71.0'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -12,6 +12,7 @@ feature_list = {
12
12
  "~sync",
13
13
  "~caching",
14
14
  "~grace",
15
+ "~contextEnrichment",
15
16
  }
16
17
 
17
18
 
@@ -8,6 +8,8 @@ from pytest_bdd import given, parsers, then, when
8
8
  from openfeature.client import OpenFeatureClient
9
9
  from openfeature.event import ProviderEvent
10
10
 
11
+ logger = logging.getLogger("openfeature.contrib.tests")
12
+
11
13
  events = {
12
14
  "ready": ProviderEvent.PROVIDER_READY,
13
15
  "error": ProviderEvent.PROVIDER_ERROR,
@@ -28,7 +30,7 @@ def event_handles() -> list:
28
30
  )
29
31
  def add_event_handler(client: OpenFeatureClient, event_type: str, event_handles: list):
30
32
  def handler(event):
31
- logging.warning((event_type, event))
33
+ logger.warning((event_type, event))
32
34
  event_handles.append(
33
35
  {
34
36
  "type": event_type,
@@ -38,7 +40,7 @@ def add_event_handler(client: OpenFeatureClient, event_type: str, event_handles:
38
40
 
39
41
  client.add_handler(events[event_type], handler)
40
42
 
41
- logging.warning(("handler added", event_type))
43
+ logger.warning(("handler added", event_type))
42
44
 
43
45
 
44
46
  def assert_handlers(handles, event_type: str, max_wait: int = 2):
@@ -22,6 +22,8 @@ KEY_FLAGS = "flags"
22
22
 
23
23
  MERGED_FILE = "merged_file"
24
24
 
25
+ logger = logging.getLogger("openfeature.contrib.tests")
26
+
25
27
 
26
28
  class TestProviderType(Enum):
27
29
  UNAVAILABLE = "unavailable"
@@ -133,7 +135,7 @@ def container(request):
133
135
  try:
134
136
  container.stop()
135
137
  except: # noqa: E722 - we want to ensure all containers are stopped, even if we do have an exception here
136
- logging.debug("container was not running anymore")
138
+ logger.debug("container was not running anymore")
137
139
 
138
140
  # Teardown code
139
141
  request.addfinalizer(fin)
@@ -1,6 +1,8 @@
1
1
  import logging
2
2
  import os
3
3
 
4
+ logger = logging.getLogger("openfeature.contrib.tests")
5
+
4
6
 
5
7
  class TestFilter:
6
8
  def __init__(self, config, feature_list=None, resolver=None, base_path=None):
@@ -40,7 +42,7 @@ class TestFilter:
40
42
  all_tags = self._get_item_tags(item)
41
43
 
42
44
  # Debug: Print collected tags for each item
43
- logging.debug(f"Item: {item.nodeid}, Tags: {all_tags}")
45
+ logger.debug(f"Item: {item.nodeid}, Tags: {all_tags}")
44
46
 
45
47
  # Include-only logic: Skip items that do not match include_tags
46
48
  if (
@@ -0,0 +1,218 @@
1
+ from unittest.mock import Mock, create_autospec
2
+
3
+ import pytest
4
+
5
+ from openfeature.contrib.provider.flagd.config import Config
6
+ from openfeature.contrib.provider.flagd.resolvers.in_process import InProcessResolver
7
+ from openfeature.contrib.provider.flagd.resolvers.process.flags import Flag, FlagStore
8
+ from openfeature.evaluation_context import EvaluationContext
9
+ from openfeature.exception import FlagNotFoundError, ParseError
10
+
11
+
12
+ def targeting():
13
+ return {
14
+ "if": [
15
+ {"==": [{"var": "targetingKey"}, "target_variant"]},
16
+ "target_variant",
17
+ None,
18
+ ]
19
+ }
20
+
21
+
22
+ def context(targeting_key):
23
+ return EvaluationContext(targeting_key=targeting_key)
24
+
25
+
26
+ @pytest.fixture
27
+ def config():
28
+ return create_autospec(Config)
29
+
30
+
31
+ @pytest.fixture
32
+ def flag_store():
33
+ return create_autospec(FlagStore)
34
+
35
+
36
+ @pytest.fixture
37
+ def flag():
38
+ return Flag(
39
+ key="flag",
40
+ state="ENABLED",
41
+ variants={"default_variant": False, "target_variant": True},
42
+ default_variant="default_variant",
43
+ targeting=targeting(),
44
+ )
45
+
46
+
47
+ @pytest.fixture
48
+ def resolver(config):
49
+ config.offline_flag_source_path = "flag.json"
50
+ config.deadline_ms = 100
51
+ return InProcessResolver(
52
+ config=config,
53
+ emit_provider_ready=Mock(),
54
+ emit_provider_error=Mock(),
55
+ emit_provider_stale=Mock(),
56
+ emit_provider_configuration_changed=Mock(),
57
+ )
58
+
59
+
60
+ def test_resolve_boolean_details_flag_not_found(resolver):
61
+ resolver.flag_store.get_flag = Mock(return_value=None)
62
+ with pytest.raises(FlagNotFoundError):
63
+ resolver.resolve_boolean_details("nonexistent_flag", False)
64
+
65
+
66
+ def test_resolve_boolean_details_disabled_flag(flag, resolver):
67
+ flag.state = "DISABLED"
68
+ resolver.flag_store.get_flag = Mock(return_value=flag)
69
+
70
+ result = resolver.resolve_boolean_details("disabled_flag", False)
71
+
72
+ assert result.reason == "DISABLED"
73
+ assert result.variant is None
74
+ assert not result.value
75
+
76
+
77
+ def test_resolve_boolean_details_invalid_variant(resolver, flag):
78
+ flag.targeting = {"var": ["targetingKey", "invalid_variant"]}
79
+
80
+ resolver.flag_store.get_flag = Mock(return_value=flag)
81
+
82
+ with pytest.raises(ParseError):
83
+ resolver.resolve_boolean_details("flag", False)
84
+
85
+
86
+ @pytest.mark.parametrize(
87
+ "input_config, resolve_config, expected",
88
+ [
89
+ (
90
+ {
91
+ "variants": {"default_variant": False, "target_variant": True},
92
+ "targeting": None,
93
+ },
94
+ {
95
+ "context": None,
96
+ "method": "resolve_boolean_details",
97
+ "default_value": False,
98
+ },
99
+ {"reason": "STATIC", "variant": "default_variant", "value": False},
100
+ ),
101
+ (
102
+ {
103
+ "variants": {"default_variant": False, "target_variant": True},
104
+ "targeting": targeting(),
105
+ },
106
+ {
107
+ "context": context("no_target_variant"),
108
+ "method": "resolve_boolean_details",
109
+ "default_value": False,
110
+ },
111
+ {"reason": "DEFAULT", "variant": "default_variant", "value": False},
112
+ ),
113
+ (
114
+ {
115
+ "variants": {"default_variant": False, "target_variant": True},
116
+ "targeting": targeting(),
117
+ },
118
+ {
119
+ "context": context("target_variant"),
120
+ "method": "resolve_boolean_details",
121
+ "default_value": False,
122
+ },
123
+ {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": True},
124
+ ),
125
+ (
126
+ {
127
+ "variants": {"default_variant": "default", "target_variant": "target"},
128
+ "targeting": targeting(),
129
+ },
130
+ {
131
+ "context": context("target_variant"),
132
+ "method": "resolve_string_details",
133
+ "default_value": "placeholder",
134
+ },
135
+ {
136
+ "reason": "TARGETING_MATCH",
137
+ "variant": "target_variant",
138
+ "value": "target",
139
+ },
140
+ ),
141
+ (
142
+ {
143
+ "variants": {"default_variant": 1.0, "target_variant": 2.0},
144
+ "targeting": targeting(),
145
+ },
146
+ {
147
+ "context": context("target_variant"),
148
+ "method": "resolve_float_details",
149
+ "default_value": 0.0,
150
+ },
151
+ {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": 2.0},
152
+ ),
153
+ (
154
+ {
155
+ "variants": {"default_variant": True, "target_variant": False},
156
+ "targeting": targeting(),
157
+ },
158
+ {
159
+ "context": context("target_variant"),
160
+ "method": "resolve_boolean_details",
161
+ "default_value": True,
162
+ },
163
+ {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": False},
164
+ ),
165
+ (
166
+ {
167
+ "variants": {"default_variant": 10, "target_variant": 0},
168
+ "targeting": targeting(),
169
+ },
170
+ {
171
+ "context": context("target_variant"),
172
+ "method": "resolve_integer_details",
173
+ "default_value": 1,
174
+ },
175
+ {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": 0},
176
+ ),
177
+ (
178
+ {
179
+ "variants": {"default_variant": {}, "target_variant": {}},
180
+ "targeting": targeting(),
181
+ },
182
+ {
183
+ "context": context("target_variant"),
184
+ "method": "resolve_object_details",
185
+ "default_value": {},
186
+ },
187
+ {"reason": "TARGETING_MATCH", "variant": "target_variant", "value": {}},
188
+ ),
189
+ ],
190
+ ids=[
191
+ "static_flag",
192
+ "boolean_default_fallback",
193
+ "boolean_targeting_match",
194
+ "string_targeting_match",
195
+ "float_targeting_match",
196
+ "boolean_falsy_target",
197
+ "integer_falsy_target",
198
+ "object_falsy_target",
199
+ ],
200
+ )
201
+ def test_resolver_details(
202
+ resolver,
203
+ flag,
204
+ input_config,
205
+ resolve_config,
206
+ expected,
207
+ ):
208
+ flag.variants = input_config["variants"]
209
+ flag.targeting = input_config["targeting"]
210
+ resolver.flag_store.get_flag = Mock(return_value=flag)
211
+
212
+ result = getattr(resolver, resolve_config["method"])(
213
+ "flag", resolve_config["default_value"], resolve_config["context"]
214
+ )
215
+
216
+ assert result.reason == expected["reason"]
217
+ assert result.variant == expected["variant"]
218
+ assert result.value == expected["value"]