openfeature-provider-flagd 0.2.0__tar.gz → 0.2.1__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 (84) hide show
  1. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/CHANGELOG.md +8 -0
  2. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/PKG-INFO +1 -1
  3. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/pyproject.toml +1 -1
  4. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/config.py +21 -0
  5. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/provider.py +27 -1
  6. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +5 -2
  7. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +1 -1
  8. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/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.1}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +32 -6
  10. openfeature_provider_flagd-0.2.1/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py +14 -0
  11. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/file/conftest.py +1 -0
  12. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/.gitignore +0 -0
  13. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/LICENSE +0 -0
  14. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/README.md +0 -0
  15. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/pytest.ini +0 -0
  16. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
  17. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
  18. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/__init__.py +0 -0
  19. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/__init__.py +0 -0
  20. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +0 -0
  21. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +0 -0
  22. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/process/targeting.py +0 -0
  23. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/contrib/provider/flagd/resolvers/protocol.py +0 -0
  24. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.py +0 -0
  25. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.pyi +0 -0
  26. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.py +0 -0
  27. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.pyi +0 -0
  28. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.py +0 -0
  29. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.pyi +0 -0
  30. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.py +0 -0
  31. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.pyi +0 -0
  32. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.py +0 -0
  33. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.pyi +0 -0
  34. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.py +0 -0
  35. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.pyi +0 -0
  36. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.py +0 -0
  37. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.pyi +0 -0
  38. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.py +0 -0
  39. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.pyi +0 -0
  40. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/__init__.py +0 -0
  41. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/conftest.py +0 -0
  42. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/__init__.py +0 -0
  43. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/conftest.py +0 -0
  44. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/file/__init__.py +0 -0
  45. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/file/test_flaqd.py +0 -0
  46. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/flagd_container.py +0 -0
  47. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/inprocess/__init__.py +0 -0
  48. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/inprocess/conftest.py +0 -0
  49. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/inprocess/test_flaqd.py +0 -0
  50. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/parsers.py +0 -0
  51. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/paths.py +0 -0
  52. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/rpc/__init__.py +0 -0
  53. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/rpc/conftest.py +0 -0
  54. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/rpc/test_flaqd.py +0 -0
  55. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/step/_utils.py +0 -0
  56. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/step/config_steps.py +0 -0
  57. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/step/context_steps.py +0 -0
  58. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/step/event_steps.py +0 -0
  59. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/step/flag_step.py +0 -0
  60. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/step/provider_steps.py +0 -0
  61. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/e2e/testfilter.py +0 -0
  62. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-broken-default.json +0 -0
  63. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-broken-state.json +0 -0
  64. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-broken-targeting.json +0 -0
  65. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-broken-variants.json +0 -0
  66. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-disabled.json +0 -0
  67. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-invalid.not-json +0 -0
  68. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-no-state.json +0 -0
  69. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-wrong-structure.json +0 -0
  70. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag-wrong-variant.json +0 -0
  71. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag.json +0 -0
  72. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/basic-flag.yaml +0 -0
  73. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-fractional-args-wrong-content.json +0 -0
  74. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-fractional-args.json +0 -0
  75. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-fractional-weights-strings.json +0 -0
  76. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-fractional-weights.json +0 -0
  77. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-semver-args.json +0 -0
  78. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-semver-op.json +0 -0
  79. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/flags/invalid-stringcomp-args.json +0 -0
  80. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/test_config.py +0 -0
  81. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/test_errors.py +0 -0
  82. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/test_file_store.py +0 -0
  83. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/test_flagd.py +0 -0
  84. {openfeature_provider_flagd-0.2.0 → openfeature_provider_flagd-0.2.1}/tests/test_targeting.py +0 -0
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## [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)
4
+
5
+
6
+ ### ✨ New Features
7
+
8
+ * **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))
9
+ * **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))
10
+
3
11
  ## [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
12
 
5
13
 
@@ -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.1
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.1"
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" }]
@@ -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,8 @@ 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,
96
103
  ):
97
104
  self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host
98
105
 
@@ -227,3 +234,17 @@ class Config:
227
234
  self.selector = (
228
235
  env_or_default(ENV_VAR_SELECTOR, None) if selector is None else selector
229
236
  )
237
+
238
+ self.provider_id = (
239
+ env_or_default(ENV_VAR_PROVIDER_ID, None)
240
+ if provider_id is None
241
+ else provider_id
242
+ )
243
+
244
+ self.default_authority = (
245
+ env_or_default(ENV_VAR_DEFAULT_AUTHORITY, None)
246
+ if default_authority is None
247
+ else default_authority
248
+ )
249
+
250
+ self.channel_credentials = channel_credentials
@@ -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,8 @@ 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,
59
67
  ):
60
68
  """
61
69
  Create an instance of the FlagdProvider
@@ -88,6 +96,7 @@ class FlagdProvider(AbstractProvider):
88
96
  retry_backoff_max_ms=retry_backoff_max_ms,
89
97
  retry_grace_period=retry_grace_period,
90
98
  selector=selector,
99
+ provider_id=provider_id,
91
100
  resolver=resolver_type,
92
101
  offline_flag_source_path=offline_flag_source_path,
93
102
  stream_deadline_ms=stream_deadline_ms,
@@ -95,9 +104,19 @@ class FlagdProvider(AbstractProvider):
95
104
  cache=cache,
96
105
  max_cache_size=max_cache_size,
97
106
  cert_path=cert_path,
107
+ default_authority=default_authority,
108
+ channel_credentials=channel_credentials,
98
109
  )
110
+ self.enriched_context: dict = {}
99
111
 
100
112
  self.resolver = self.setup_resolver()
113
+ self.hooks: list[Hook] = [SyncMetadataHook(self.get_enriched_context)]
114
+
115
+ def get_enriched_context(self) -> EvaluationContext:
116
+ return EvaluationContext(attributes=self.enriched_context)
117
+
118
+ def get_provider_hooks(self) -> list[Hook]:
119
+ return self.hooks
101
120
 
102
121
  def setup_resolver(self) -> AbstractResolver:
103
122
  if self.config.resolver == ResolverType.RPC:
@@ -114,7 +133,7 @@ class FlagdProvider(AbstractProvider):
114
133
  ):
115
134
  return InProcessResolver(
116
135
  self.config,
117
- self.emit_provider_ready,
136
+ self.emit_provider_ready_with_context,
118
137
  self.emit_provider_error,
119
138
  self.emit_provider_stale,
120
139
  self.emit_provider_configuration_changed,
@@ -184,3 +203,10 @@ class FlagdProvider(AbstractProvider):
184
203
  return self.resolver.resolve_object_details(
185
204
  key, default_value, evaluation_context
186
205
  )
206
+
207
+ def emit_provider_ready_with_context(
208
+ self, details: ProviderEventDetails, context: dict
209
+ ) -> None:
210
+ self.enriched_context = context
211
+ self.emit_provider_ready(details)
212
+ 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[
@@ -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,7 @@ import time
5
5
  import typing
6
6
 
7
7
  import grpc
8
+ from google.protobuf.json_format import MessageToDict
8
9
 
9
10
  from openfeature.evaluation_context import EvaluationContext
10
11
  from openfeature.event import ProviderEventDetails
@@ -26,7 +27,7 @@ class GrpcWatcher(FlagStateConnector):
26
27
  self,
27
28
  config: Config,
28
29
  flag_store: FlagStore,
29
- emit_provider_ready: typing.Callable[[ProviderEventDetails], None],
30
+ emit_provider_ready: typing.Callable[[ProviderEventDetails, dict], None],
30
31
  emit_provider_error: typing.Callable[[ProviderEventDetails], None],
31
32
  emit_provider_stale: typing.Callable[[ProviderEventDetails], None],
32
33
  ):
@@ -41,6 +42,7 @@ class GrpcWatcher(FlagStateConnector):
41
42
  self.streamline_deadline_seconds = config.stream_deadline_ms * 0.001
42
43
  self.deadline = config.deadline_ms * 0.001
43
44
  self.selector = config.selector
45
+ self.provider_id = config.provider_id
44
46
  self.emit_provider_ready = emit_provider_ready
45
47
  self.emit_provider_error = emit_provider_error
46
48
  self.emit_provider_stale = emit_provider_stale
@@ -54,13 +56,23 @@ class GrpcWatcher(FlagStateConnector):
54
56
  def _generate_channel(self, config: Config) -> grpc.Channel:
55
57
  target = f"{config.host}:{config.port}"
56
58
  # Create the channel with the service config
57
- options = [
59
+ options: list[tuple[str, typing.Any]] = [
58
60
  ("grpc.keepalive_time_ms", config.keep_alive_time),
59
61
  ("grpc.initial_reconnect_backoff_ms", config.retry_backoff_ms),
60
62
  ("grpc.max_reconnect_backoff_ms", config.retry_backoff_max_ms),
61
63
  ("grpc.min_reconnect_backoff_ms", config.stream_deadline_ms),
62
64
  ]
63
- if config.tls:
65
+ if config.default_authority is not None:
66
+ options.append(("grpc.default_authority", config.default_authority))
67
+
68
+ if config.channel_credentials is not None:
69
+ channel_args = {
70
+ "options": options,
71
+ "credentials": config.channel_credentials,
72
+ }
73
+ channel = grpc.secure_channel(target, **channel_args)
74
+
75
+ elif config.tls:
64
76
  channel_args = {
65
77
  "options": options,
66
78
  "credentials": grpc.ssl_channel_credentials(),
@@ -106,7 +118,10 @@ class GrpcWatcher(FlagStateConnector):
106
118
 
107
119
  def _state_change_callback(self, new_state: grpc.ChannelConnectivity) -> None:
108
120
  logger.debug(f"gRPC state change: {new_state}")
109
- if new_state == grpc.ChannelConnectivity.READY:
121
+ if (
122
+ new_state == grpc.ChannelConnectivity.READY
123
+ or new_state == grpc.ChannelConnectivity.IDLE
124
+ ):
110
125
  if not self.thread or not self.thread.is_alive():
111
126
  self.thread = threading.Thread(
112
127
  target=self.listen,
@@ -153,10 +168,20 @@ class GrpcWatcher(FlagStateConnector):
153
168
  if self.streamline_deadline_seconds > 0
154
169
  else {}
155
170
  )
156
- request_args = {"selector": self.selector} if self.selector is not None else {}
171
+ request_args = {}
172
+ if self.selector is not None:
173
+ request_args["selector"] = self.selector
174
+ if self.provider_id is not None:
175
+ request_args["provider_id"] = self.provider_id
157
176
 
158
177
  while self.active:
159
178
  try:
179
+ context_values_request = sync_pb2.GetMetadataRequest()
180
+ context_values_response: sync_pb2.GetMetadataResponse = (
181
+ self.stub.GetMetadata(context_values_request, wait_for_ready=True)
182
+ )
183
+ context_values = MessageToDict(context_values_response)
184
+
160
185
  request = sync_pb2.SyncFlagsRequest(**request_args)
161
186
 
162
187
  logger.debug("Setting up gRPC sync flags connection")
@@ -173,7 +198,8 @@ class GrpcWatcher(FlagStateConnector):
173
198
  self.emit_provider_ready(
174
199
  ProviderEventDetails(
175
200
  message="gRPC sync connection established"
176
- )
201
+ ),
202
+ context_values["metadata"],
177
203
  )
178
204
  self.connected = True
179
205
 
@@ -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()
@@ -12,6 +12,7 @@ feature_list = {
12
12
  "~sync",
13
13
  "~caching",
14
14
  "~grace",
15
+ "~contextEnrichment",
15
16
  }
16
17
 
17
18