openfeature-provider-flagd 0.2.1__tar.gz → 0.2.3__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 (93) hide show
  1. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/CHANGELOG.md +34 -0
  2. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/PKG-INFO +1 -1
  3. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/pyproject.toml +2 -2
  4. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/config.py +9 -0
  5. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/provider.py +12 -10
  6. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +30 -4
  7. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +15 -4
  8. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +22 -9
  9. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +41 -5
  10. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.py +1 -1
  11. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.py +1 -1
  12. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.py +1 -1
  13. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.py +1 -1
  14. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/rpc/conftest.py +1 -1
  15. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/step/event_steps.py +5 -3
  16. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/step/flag_step.py +13 -0
  17. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/step/provider_steps.py +20 -7
  18. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/testfilter.py +3 -1
  19. openfeature_provider_flagd-0.2.3/tests/flags/basic-flag-combined-metadata.json +29 -0
  20. openfeature_provider_flagd-0.2.3/tests/flags/basic-flag-metadata.json +19 -0
  21. openfeature_provider_flagd-0.2.3/tests/flags/basic-flag-set-metadata.json +19 -0
  22. openfeature_provider_flagd-0.2.3/tests/flags/invalid-flag-metadata-list.json +14 -0
  23. openfeature_provider_flagd-0.2.3/tests/flags/invalid-flag-metadata.json +24 -0
  24. openfeature_provider_flagd-0.2.3/tests/flags/invalid-flag-set-metadata-list.json +14 -0
  25. openfeature_provider_flagd-0.2.3/tests/flags/invalid-flag-set-metadata.json +21 -0
  26. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/test_errors.py +1 -1
  27. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/test_file_store.py +33 -0
  28. openfeature_provider_flagd-0.2.3/tests/test_in_process.py +218 -0
  29. openfeature_provider_flagd-0.2.3/tests/test_metadata.py +148 -0
  30. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/.gitignore +0 -0
  31. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/LICENSE +0 -0
  32. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/README.md +0 -0
  33. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/pytest.ini +0 -0
  34. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
  35. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
  36. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/__init__.py +0 -0
  37. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +0 -0
  38. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/__init__.py +0 -0
  39. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +0 -0
  40. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/process/targeting.py +0 -0
  41. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/resolvers/protocol.py +0 -0
  42. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py +0 -0
  43. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.py +0 -0
  44. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.pyi +0 -0
  45. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.pyi +0 -0
  46. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.py +0 -0
  47. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.pyi +0 -0
  48. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.pyi +0 -0
  49. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.py +0 -0
  50. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.pyi +0 -0
  51. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.pyi +0 -0
  52. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.py +0 -0
  53. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.pyi +0 -0
  54. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.pyi +0 -0
  55. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/__init__.py +0 -0
  56. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/conftest.py +0 -0
  57. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/__init__.py +0 -0
  58. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/conftest.py +0 -0
  59. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/file/__init__.py +0 -0
  60. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/file/conftest.py +0 -0
  61. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/file/test_flaqd.py +0 -0
  62. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/flagd_container.py +0 -0
  63. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/inprocess/__init__.py +0 -0
  64. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/inprocess/conftest.py +0 -0
  65. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/inprocess/test_flaqd.py +0 -0
  66. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/parsers.py +0 -0
  67. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/paths.py +0 -0
  68. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/rpc/__init__.py +0 -0
  69. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/rpc/test_flaqd.py +0 -0
  70. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/step/_utils.py +0 -0
  71. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/step/config_steps.py +0 -0
  72. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/e2e/step/context_steps.py +0 -0
  73. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-broken-default.json +0 -0
  74. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-broken-state.json +0 -0
  75. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-broken-targeting.json +0 -0
  76. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-broken-variants.json +0 -0
  77. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-disabled.json +0 -0
  78. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-invalid.not-json +0 -0
  79. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-no-state.json +0 -0
  80. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-wrong-structure.json +0 -0
  81. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag-wrong-variant.json +0 -0
  82. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag.json +0 -0
  83. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/basic-flag.yaml +0 -0
  84. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-fractional-args-wrong-content.json +0 -0
  85. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-fractional-args.json +0 -0
  86. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-fractional-weights-strings.json +0 -0
  87. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-fractional-weights.json +0 -0
  88. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-semver-args.json +0 -0
  89. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-semver-op.json +0 -0
  90. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/flags/invalid-stringcomp-args.json +0 -0
  91. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/test_config.py +0 -0
  92. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/test_flagd.py +0 -0
  93. {openfeature_provider_flagd-0.2.1 → openfeature_provider_flagd-0.2.3}/tests/test_targeting.py +0 -0
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.3](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.2...openfeature-provider-flagd/v0.2.3) (2025-04-11)
4
+
5
+
6
+ ### 🐛 Bug Fixes
7
+
8
+ * **flagd:** fix parameter name inconsistency with SDK version 0.8.1 ([#232](https://github.com/open-feature/python-sdk-contrib/issues/232)) ([55ee420](https://github.com/open-feature/python-sdk-contrib/commit/55ee42087bd9a948a130b08671395138baa33621))
9
+
10
+
11
+ ### ✨ New Features
12
+
13
+ * add support for flagd flag metadata ([#215](https://github.com/open-feature/python-sdk-contrib/issues/215)) ([6dc72c0](https://github.com/open-feature/python-sdk-contrib/commit/6dc72c0e16b01cd40e6c103884a2d457e95871d1))
14
+
15
+
16
+ ### 🧹 Chore
17
+
18
+ * **deps:** update dependency providers/openfeature-provider-flagd/openfeature/test-harness to v2.7.3 ([#226](https://github.com/open-feature/python-sdk-contrib/issues/226)) ([9a0971c](https://github.com/open-feature/python-sdk-contrib/commit/9a0971c1fd67998259903a3ea4b42772413fc259))
19
+ * **deps:** update providers/openfeature-provider-flagd/openfeature/spec digest to 130df3e ([#222](https://github.com/open-feature/python-sdk-contrib/issues/222)) ([fa7f429](https://github.com/open-feature/python-sdk-contrib/commit/fa7f4293e060a3d56b204db851c19a668076e7a7))
20
+ * **deps:** update providers/openfeature-provider-flagd/openfeature/spec digest to 27e4461 ([#223](https://github.com/open-feature/python-sdk-contrib/issues/223)) ([9bf2e42](https://github.com/open-feature/python-sdk-contrib/commit/9bf2e421e52922516afa2e5b8648b52a035a038d))
21
+ * **deps:** update providers/openfeature-provider-flagd/openfeature/spec digest to aad6193 ([#194](https://github.com/open-feature/python-sdk-contrib/issues/194)) ([277ad0e](https://github.com/open-feature/python-sdk-contrib/commit/277ad0e744764bbc4eb5c5ebb773287bc2607ac3))
22
+
23
+ ## [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)
24
+
25
+
26
+ ### 🐛 Bug Fixes
27
+
28
+ * **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))
29
+
30
+
31
+ ### 🧹 Chore
32
+
33
+ * **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))
34
+ * **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))
35
+ * **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))
36
+
3
37
  ## [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
38
 
5
39
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openfeature-provider-flagd
3
- Version: 0.2.1
3
+ Version: 0.2.3
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.1"
8
+ version = "0.2.3"
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",
@@ -100,6 +100,7 @@ class Config:
100
100
  cert_path: typing.Optional[str] = None,
101
101
  default_authority: typing.Optional[str] = None,
102
102
  channel_credentials: typing.Optional[grpc.ChannelCredentials] = None,
103
+ sync_metadata_disabled: typing.Optional[bool] = None,
103
104
  ):
104
105
  self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host
105
106
 
@@ -248,3 +249,11 @@ class Config:
248
249
  )
249
250
 
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
@@ -64,6 +64,7 @@ class FlagdProvider(AbstractProvider):
64
64
  cert_path: typing.Optional[str] = None,
65
65
  default_authority: typing.Optional[str] = None,
66
66
  channel_credentials: typing.Optional[grpc.ChannelCredentials] = None,
67
+ sync_metadata_disabled: typing.Optional[bool] = None,
67
68
  ):
68
69
  """
69
70
  Create an instance of the FlagdProvider
@@ -106,6 +107,7 @@ class FlagdProvider(AbstractProvider):
106
107
  cert_path=cert_path,
107
108
  default_authority=default_authority,
108
109
  channel_credentials=channel_credentials,
110
+ sync_metadata_disabled=sync_metadata_disabled,
109
111
  )
110
112
  self.enriched_context: dict = {}
111
113
 
@@ -156,52 +158,52 @@ class FlagdProvider(AbstractProvider):
156
158
 
157
159
  def resolve_boolean_details(
158
160
  self,
159
- key: str,
161
+ flag_key: str,
160
162
  default_value: bool,
161
163
  evaluation_context: typing.Optional[EvaluationContext] = None,
162
164
  ) -> FlagResolutionDetails[bool]:
163
165
  return self.resolver.resolve_boolean_details(
164
- key, default_value, evaluation_context
166
+ flag_key, default_value, evaluation_context
165
167
  )
166
168
 
167
169
  def resolve_string_details(
168
170
  self,
169
- key: str,
171
+ flag_key: str,
170
172
  default_value: str,
171
173
  evaluation_context: typing.Optional[EvaluationContext] = None,
172
174
  ) -> FlagResolutionDetails[str]:
173
175
  return self.resolver.resolve_string_details(
174
- key, default_value, evaluation_context
176
+ flag_key, default_value, evaluation_context
175
177
  )
176
178
 
177
179
  def resolve_float_details(
178
180
  self,
179
- key: str,
181
+ flag_key: str,
180
182
  default_value: float,
181
183
  evaluation_context: typing.Optional[EvaluationContext] = None,
182
184
  ) -> FlagResolutionDetails[float]:
183
185
  return self.resolver.resolve_float_details(
184
- key, default_value, evaluation_context
186
+ flag_key, default_value, evaluation_context
185
187
  )
186
188
 
187
189
  def resolve_integer_details(
188
190
  self,
189
- key: str,
191
+ flag_key: str,
190
192
  default_value: int,
191
193
  evaluation_context: typing.Optional[EvaluationContext] = None,
192
194
  ) -> FlagResolutionDetails[int]:
193
195
  return self.resolver.resolve_integer_details(
194
- key, default_value, evaluation_context
196
+ flag_key, default_value, evaluation_context
195
197
  )
196
198
 
197
199
  def resolve_object_details(
198
200
  self,
199
- key: str,
201
+ flag_key: str,
200
202
  default_value: typing.Union[dict, list],
201
203
  evaluation_context: typing.Optional[EvaluationContext] = None,
202
204
  ) -> FlagResolutionDetails[typing.Union[dict, list]]:
203
205
  return self.resolver.resolve_object_details(
204
- key, default_value, evaluation_context
206
+ flag_key, default_value, evaluation_context
205
207
  )
206
208
 
207
209
  def emit_provider_ready_with_context(
@@ -17,6 +17,23 @@ from .process.targeting import targeting
17
17
  T = typing.TypeVar("T")
18
18
 
19
19
 
20
+ def _merge_metadata(
21
+ flag_metadata: typing.Optional[
22
+ typing.Mapping[str, typing.Union[float, int, str, bool]]
23
+ ],
24
+ flag_set_metadata: typing.Optional[
25
+ typing.Mapping[str, typing.Union[float, int, str, bool]]
26
+ ],
27
+ ) -> typing.Mapping[str, typing.Union[float, int, str, bool]]:
28
+ metadata = {} if flag_set_metadata is None else dict(flag_set_metadata)
29
+
30
+ if flag_metadata is not None:
31
+ for key, value in flag_metadata.items():
32
+ metadata[key] = value
33
+
34
+ return metadata
35
+
36
+
20
37
  class InProcessResolver:
21
38
  def __init__(
22
39
  self,
@@ -103,29 +120,38 @@ class InProcessResolver:
103
120
  if not flag:
104
121
  raise FlagNotFoundError(f"Flag with key {key} not present in flag store.")
105
122
 
123
+ metadata = _merge_metadata(flag.metadata, self.flag_store.flag_set_metadata)
124
+
106
125
  if flag.state == "DISABLED":
107
- return FlagResolutionDetails(default_value, reason=Reason.DISABLED)
126
+ return FlagResolutionDetails(
127
+ default_value, flag_metadata=metadata, reason=Reason.DISABLED
128
+ )
108
129
 
109
130
  if not flag.targeting:
110
131
  variant, value = flag.default
111
- return FlagResolutionDetails(value, variant=variant, reason=Reason.STATIC)
132
+ return FlagResolutionDetails(
133
+ value, variant=variant, flag_metadata=metadata, reason=Reason.STATIC
134
+ )
112
135
 
113
136
  variant = targeting(flag.key, flag.targeting, evaluation_context)
114
137
 
115
138
  if variant is None:
116
139
  variant, value = flag.default
117
- return FlagResolutionDetails(value, variant=variant, reason=Reason.DEFAULT)
140
+ return FlagResolutionDetails(
141
+ value, variant=variant, flag_metadata=metadata, reason=Reason.DEFAULT
142
+ )
118
143
  if not isinstance(variant, (str, bool)):
119
144
  raise ParseError(
120
145
  "Parsed JSONLogic targeting did not return a string or bool"
121
146
  )
122
147
 
123
148
  variant, value = flag.get_variant(variant)
124
- if not value:
149
+ if value is None:
125
150
  raise ParseError(f"Resolved variant {variant} not in variants config.")
126
151
 
127
152
  return FlagResolutionDetails(
128
153
  value,
129
154
  variant=variant,
130
155
  reason=Reason.TARGETING_MATCH,
156
+ flag_metadata=metadata,
131
157
  )
@@ -14,7 +14,7 @@ from openfeature.contrib.provider.flagd.resolvers.process.connector import (
14
14
  from openfeature.contrib.provider.flagd.resolvers.process.flags import FlagStore
15
15
  from openfeature.evaluation_context import EvaluationContext
16
16
  from openfeature.event import ProviderEventDetails
17
- from openfeature.exception import ParseError, ProviderNotReadyError
17
+ from openfeature.exception import ErrorCode, ParseError, ProviderNotReadyError
18
18
 
19
19
  logger = logging.getLogger("openfeature.contrib")
20
20
 
@@ -76,8 +76,15 @@ class FileWatcher(FlagStateConnector):
76
76
  self.handle_error("Could not parse JSON flag data from file")
77
77
  except yaml.error.YAMLError:
78
78
  self.handle_error("Could not parse YAML flag data from file")
79
- except ParseError:
80
- self.handle_error("Could not parse flag data using flagd syntax")
79
+ except ParseError as e:
80
+ self.handle_error(
81
+ "Could not parse flag data using flagd syntax: "
82
+ + (
83
+ "no error message provided"
84
+ if e is None or e.error_message is None
85
+ else e.error_message
86
+ )
87
+ )
81
88
  except Exception:
82
89
  self.handle_error("Could not read flags from file")
83
90
 
@@ -104,4 +111,8 @@ class FileWatcher(FlagStateConnector):
104
111
  def handle_error(self, error_message: str) -> None:
105
112
  logger.exception(error_message)
106
113
  self.should_emit_ready_on_success = True
107
- self.emit_provider_error(ProviderEventDetails(message=error_message))
114
+ self.emit_provider_error(
115
+ ProviderEventDetails(
116
+ message=error_message, error_code=ErrorCode.PARSE_ERROR
117
+ )
118
+ )
@@ -6,6 +6,7 @@ import typing
6
6
 
7
7
  import grpc
8
8
  from google.protobuf.json_format import MessageToDict
9
+ from google.protobuf.struct_pb2 import Struct
9
10
 
10
11
  from openfeature.evaluation_context import EvaluationContext
11
12
  from openfeature.event import ProviderEventDetails
@@ -162,24 +163,36 @@ class GrpcWatcher(FlagStateConnector):
162
163
  self.active = False
163
164
  self.channel.close()
164
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
+
165
175
  def listen(self) -> None:
166
176
  call_args = (
167
177
  {"timeout": self.streamline_deadline_seconds}
168
178
  if self.streamline_deadline_seconds > 0
169
179
  else {}
170
180
  )
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
181
+ request_args = self._create_request_args()
176
182
 
177
183
  while self.active:
178
184
  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
- )
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
+
183
196
  context_values = MessageToDict(context_values_response)
184
197
 
185
198
  request = sync_pb2.SyncFlagsRequest(**request_args)
@@ -7,6 +7,21 @@ from openfeature.event import ProviderEventDetails
7
7
  from openfeature.exception import ParseError
8
8
 
9
9
 
10
+ def _validate_metadata(key: str, value: typing.Union[float, int, str, bool]) -> None:
11
+ if key is None:
12
+ raise ParseError("Metadata key must be set")
13
+ elif not isinstance(key, str):
14
+ raise ParseError(f"Metadata key {key} must be of type str, but is {type(key)}")
15
+ elif not key:
16
+ raise ParseError("key must not be empty")
17
+ if value is None:
18
+ raise ParseError(f"Metadata value for key {key} must be set")
19
+ elif not isinstance(value, (float, int, str, bool)):
20
+ raise ParseError(
21
+ f"Metadata value {value} for key {key} must be of type float, int, str or bool, but is {type(value)}"
22
+ )
23
+
24
+
10
25
  class FlagStore:
11
26
  def __init__(
12
27
  self,
@@ -16,12 +31,16 @@ class FlagStore:
16
31
  ):
17
32
  self.emit_provider_configuration_changed = emit_provider_configuration_changed
18
33
  self.flags: typing.Mapping[str, Flag] = {}
34
+ self.flag_set_metadata: typing.Mapping[
35
+ str, typing.Union[float, int, str, bool]
36
+ ] = {}
19
37
 
20
38
  def get_flag(self, key: str) -> typing.Optional["Flag"]:
21
39
  return self.flags.get(key)
22
40
 
23
41
  def update(self, flags_data: dict) -> None:
24
42
  flags = flags_data.get("flags", {})
43
+ metadata = flags_data.get("metadata", {})
25
44
  evaluators: typing.Optional[dict] = flags_data.get("$evaluators")
26
45
  if evaluators:
27
46
  transposed = json.dumps(flags)
@@ -33,10 +52,18 @@ class FlagStore:
33
52
 
34
53
  if not isinstance(flags, dict):
35
54
  raise ParseError("`flags` key of configuration must be a dictionary")
55
+ if not isinstance(metadata, dict):
56
+ raise ParseError("`metadata` key of configuration must be a dictionary")
57
+ for key, value in metadata.items():
58
+ _validate_metadata(key, value)
59
+
36
60
  self.flags = {key: Flag.from_dict(key, data) for key, data in flags.items()}
61
+ self.flag_set_metadata = metadata
37
62
 
38
63
  self.emit_provider_configuration_changed(
39
- ProviderEventDetails(flags_changed=list(self.flags.keys()))
64
+ ProviderEventDetails(
65
+ flags_changed=list(self.flags.keys()), metadata=metadata
66
+ )
40
67
  )
41
68
 
42
69
 
@@ -47,6 +74,9 @@ class Flag:
47
74
  variants: typing.Mapping[str, typing.Any]
48
75
  default_variant: typing.Union[bool, str]
49
76
  targeting: typing.Optional[dict] = None
77
+ metadata: typing.Optional[
78
+ typing.Mapping[str, typing.Union[float, int, str, bool]]
79
+ ] = None
50
80
 
51
81
  def __post_init__(self) -> None:
52
82
  if not self.state or not isinstance(self.state, str):
@@ -66,19 +96,25 @@ class Flag:
66
96
  if self.default_variant not in self.variants:
67
97
  raise ParseError("Default variant does not match set of variants")
68
98
 
99
+ if self.metadata:
100
+ if not isinstance(self.metadata, dict):
101
+ raise ParseError("Flag metadata is not a valid json object")
102
+ for key, value in self.metadata.items():
103
+ _validate_metadata(key, value)
104
+
69
105
  @classmethod
70
106
  def from_dict(cls, key: str, data: dict) -> "Flag":
71
107
  if "defaultVariant" in data:
72
108
  data["default_variant"] = data["defaultVariant"]
73
109
  del data["defaultVariant"]
74
110
 
75
- if "source" in data:
76
- del data["source"]
77
- if "selector" in data:
78
- del data["selector"]
111
+ data.pop("source", None)
112
+ data.pop("selector", None)
79
113
  try:
80
114
  flag = cls(key=key, **data)
81
115
  return flag
116
+ except ParseError as parseError:
117
+ raise parseError
82
118
  except Exception as err:
83
119
  raise ParseError from err
84
120
 
@@ -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
 
@@ -4,7 +4,7 @@ from openfeature.contrib.provider.flagd.config import ResolverType
4
4
  from tests.e2e.testfilter import TestFilter
5
5
 
6
6
  resolver = ResolverType.RPC
7
- feature_list = ["~targetURI", "~unixsocket", "~sync"]
7
+ feature_list = ["~targetURI", "~unixsocket", "~sync", "~metadata"]
8
8
 
9
9
 
10
10
  def pytest_collection_modifyitems(config, items):
@@ -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,11 +40,11 @@ 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):
45
- poll_interval = 1
47
+ poll_interval = 0.2
46
48
  while max_wait > 0:
47
49
  found = any(h["type"] == event_type for h in handles)
48
50
  if not found:
@@ -94,3 +94,16 @@ def resolve_details_reason(
94
94
  reason: str,
95
95
  ):
96
96
  assert_equal(details.reason, Reason(reason))
97
+
98
+
99
+ @then(parsers.cfparse("the resolved metadata should contain"))
100
+ def metadata_contains(details: FlagEvaluationDetails[JsonPrimitive], datatable):
101
+ assert_equal(len(details.flag_metadata), len(datatable) - 1) # skip table header
102
+ for i in range(1, len(datatable)):
103
+ key, metadata_type, expected = datatable[i]
104
+ assert_equal(details.flag_metadata[key], type_cast[metadata_type](expected))
105
+
106
+
107
+ @then("the resolved metadata is empty")
108
+ def empty_metadata(details: FlagEvaluationDetails[JsonPrimitive]):
109
+ assert_equal(len(details.flag_metadata), 0)
@@ -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"
@@ -29,6 +31,7 @@ class TestProviderType(Enum):
29
31
  UNSTABLE = "unstable"
30
32
  SSL = "ssl"
31
33
  SOCKET = "socket"
34
+ METADATA = "metadata"
32
35
 
33
36
 
34
37
  @given("a provider is registered", target_fixture="client")
@@ -41,7 +44,7 @@ def setup_provider_old(
41
44
 
42
45
 
43
46
  def get_default_options_for_provider(
44
- provider_type: str, resolver_type: ResolverType, container
47
+ provider_type: str, resolver_type: ResolverType, container, option_values: dict
45
48
  ) -> tuple[dict, bool]:
46
49
  launchpad = "default"
47
50
  t = TestProviderType(provider_type)
@@ -66,11 +69,20 @@ def get_default_options_for_provider(
66
69
  launchpad = "ssl"
67
70
  elif t == TestProviderType.SOCKET:
68
71
  return options, True
72
+ elif t == TestProviderType.METADATA:
73
+ launchpad = "metadata"
69
74
 
70
75
  if resolver_type == ResolverType.FILE:
71
- options["offline_flag_source_path"] = os.path.join(
72
- container.flagDir.name, "allFlags.json"
73
- )
76
+ if "selector" in option_values:
77
+ path = option_values["selector"]
78
+ path = path.replace("rawflags/", "")
79
+ options["offline_flag_source_path"] = os.path.join(
80
+ Path(__file__).parents[3], "openfeature", "test-harness", "flags", path
81
+ )
82
+ else:
83
+ options["offline_flag_source_path"] = os.path.join(
84
+ container.flagDir.name, "allFlags.json"
85
+ )
74
86
 
75
87
  requests.post(
76
88
  f"{container.get_launchpad_url()}/start?config={launchpad}", timeout=1
@@ -89,7 +101,7 @@ def setup_provider(
89
101
  option_values: dict,
90
102
  ) -> OpenFeatureClient:
91
103
  default_options, wait = get_default_options_for_provider(
92
- provider_type, resolver_type, container
104
+ provider_type, resolver_type, container, option_values
93
105
  )
94
106
 
95
107
  combined_options = {**default_options, **option_values}
@@ -118,7 +130,8 @@ def flagd_restart(
118
130
  resolver_type: ResolverType,
119
131
  ):
120
132
  requests.post(
121
- f"{container.get_launchpad_url()}/restart?seconds={seconds}", timeout=2
133
+ f"{container.get_launchpad_url()}/restart?seconds={seconds}",
134
+ timeout=float(seconds) + 2,
122
135
  )
123
136
  pass
124
137
 
@@ -133,7 +146,7 @@ def container(request):
133
146
  try:
134
147
  container.stop()
135
148
  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")
149
+ logger.debug("container was not running anymore")
137
150
 
138
151
  # Teardown code
139
152
  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,29 @@
1
+ {
2
+ "flags": {
3
+ "basic-flag": {
4
+ "state": "ENABLED",
5
+ "variants": {
6
+ "true": true,
7
+ "false": false
8
+ },
9
+ "defaultVariant": "false",
10
+ "targeting": {},
11
+ "metadata": {
12
+ "string": "a",
13
+ "integer": 1,
14
+ "float": 1.2,
15
+ "bool": true
16
+ }
17
+ }
18
+ },
19
+ "metadata": {
20
+ "string": "b",
21
+ "integer": 2,
22
+ "float": 2.2,
23
+ "bool": false,
24
+ "flag-set-string": "c",
25
+ "flag-set-integer": 3,
26
+ "flag-set-float": 3.2,
27
+ "flag-set-bool": false
28
+ }
29
+ }