openfeature-provider-flagd 0.2.4__tar.gz → 0.2.6__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 (94) hide show
  1. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/.gitignore +3 -0
  2. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/CHANGELOG.md +16 -0
  3. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/PKG-INFO +2 -2
  4. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/pyproject.toml +4 -3
  5. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +42 -17
  6. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +1 -1
  7. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +11 -14
  8. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/process/targeting.py +4 -0
  9. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.py +3 -3
  10. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.py +1 -1
  11. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.py +3 -3
  12. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.py +1 -1
  13. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.py +3 -3
  14. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.py +1 -1
  15. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.py +3 -3
  16. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.py +1 -1
  17. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/flagd_container.py +2 -2
  18. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/step/context_steps.py +12 -0
  19. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/step/flag_step.py +20 -1
  20. openfeature_provider_flagd-0.2.6/tests/flags/basic-flag-without-default.json +12 -0
  21. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_errors.py +32 -2
  22. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_file_store.py +1 -0
  23. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_in_process.py +2 -2
  24. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/LICENSE +0 -0
  25. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/README.md +0 -0
  26. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/pytest.ini +0 -0
  27. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
  28. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/config.py +0 -0
  29. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
  30. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/provider.py +0 -0
  31. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/__init__.py +0 -0
  32. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +0 -0
  33. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/__init__.py +0 -0
  34. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +0 -0
  35. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +0 -0
  36. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/resolvers/protocol.py +0 -0
  37. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/contrib/provider/flagd/sync_metadata_hook.py +0 -0
  38. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.pyi +0 -0
  39. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.pyi +0 -0
  40. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.pyi +0 -0
  41. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.pyi +0 -0
  42. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2.pyi +0 -0
  43. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.pyi +0 -0
  44. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2.pyi +0 -0
  45. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/src/openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.pyi +0 -0
  46. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/__init__.py +0 -0
  47. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/conftest.py +0 -0
  48. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/__init__.py +0 -0
  49. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/conftest.py +0 -0
  50. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/file/__init__.py +0 -0
  51. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/file/conftest.py +0 -0
  52. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/file/test_flaqd.py +0 -0
  53. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/inprocess/__init__.py +0 -0
  54. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/inprocess/conftest.py +0 -0
  55. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/inprocess/test_flaqd.py +0 -0
  56. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/parsers.py +0 -0
  57. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/paths.py +0 -0
  58. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/rpc/__init__.py +0 -0
  59. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/rpc/conftest.py +0 -0
  60. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/rpc/test_flaqd.py +0 -0
  61. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/step/_utils.py +0 -0
  62. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/step/config_steps.py +0 -0
  63. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/step/event_steps.py +0 -0
  64. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/step/provider_steps.py +0 -0
  65. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/e2e/testfilter.py +0 -0
  66. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-broken-default.json +0 -0
  67. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-broken-state.json +0 -0
  68. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-broken-targeting.json +0 -0
  69. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-broken-variants.json +0 -0
  70. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-combined-metadata.json +0 -0
  71. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-disabled.json +0 -0
  72. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-invalid.not-json +0 -0
  73. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-metadata.json +0 -0
  74. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-no-state.json +0 -0
  75. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-set-metadata.json +0 -0
  76. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-wrong-structure.json +0 -0
  77. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag-wrong-variant.json +0 -0
  78. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag.json +0 -0
  79. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/basic-flag.yaml +0 -0
  80. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-flag-metadata-list.json +0 -0
  81. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-flag-metadata.json +0 -0
  82. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-flag-set-metadata-list.json +0 -0
  83. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-flag-set-metadata.json +0 -0
  84. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-fractional-args-wrong-content.json +0 -0
  85. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-fractional-args.json +0 -0
  86. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-fractional-weights-strings.json +0 -0
  87. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-fractional-weights.json +0 -0
  88. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-semver-args.json +0 -0
  89. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-semver-op.json +0 -0
  90. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/flags/invalid-stringcomp-args.json +0 -0
  91. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_config.py +0 -0
  92. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_flagd.py +0 -0
  93. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_metadata.py +0 -0
  94. {openfeature_provider_flagd-0.2.4 → openfeature_provider_flagd-0.2.6}/tests/test_targeting.py +0 -0
@@ -54,3 +54,6 @@ docs/_build/
54
54
 
55
55
  # vscode
56
56
  .vscode/
57
+
58
+ # test coverage
59
+ /providers/openfeature-provider-env-var/tests-results.xml
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.2.6](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.5...openfeature-provider-flagd/v0.2.6) (2025-07-22)
4
+
5
+
6
+ ### 🐛 Bug Fixes
7
+
8
+ * **flagd:** Add upperbound to protobuf version to reduce potential conflict with other version ([5ad2896](https://github.com/open-feature/python-sdk-contrib/commit/5ad289683a0d9128a53d142d2804f9fffb9dd36f))
9
+ * **flagd:** adjust flagd to updated error codes ([#285](https://github.com/open-feature/python-sdk-contrib/issues/285)) ([64d755b](https://github.com/open-feature/python-sdk-contrib/commit/64d755b869a076216a91b2d409b1ba5d627ebe08))
10
+ * **flagd:** fix protobuf version for file generation to lowest 5.x version ([#280](https://github.com/open-feature/python-sdk-contrib/issues/280)) ([5ad2896](https://github.com/open-feature/python-sdk-contrib/commit/5ad289683a0d9128a53d142d2804f9fffb9dd36f))
11
+
12
+ ## [0.2.5](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.4...openfeature-provider-flagd/v0.2.5) (2025-07-08)
13
+
14
+
15
+ ### 🐛 Bug Fixes
16
+
17
+ * **flagd:** reduce loglevel of expected logs ([#277](https://github.com/open-feature/python-sdk-contrib/issues/277)) ([cca865f](https://github.com/open-feature/python-sdk-contrib/commit/cca865f490cc699ecf038c934dfccacad9bda8d2))
18
+
3
19
  ## [0.2.4](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.2.3...openfeature-provider-flagd/v0.2.4) (2025-06-10)
4
20
 
5
21
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: openfeature-provider-flagd
3
- Version: 0.2.4
3
+ Version: 0.2.6
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>
@@ -215,7 +215,7 @@ Requires-Dist: grpcio>=1.68.1
215
215
  Requires-Dist: mmh3>=4.1.0
216
216
  Requires-Dist: openfeature-sdk>=0.6.0
217
217
  Requires-Dist: panzi-json-logic>=1.0.1
218
- Requires-Dist: protobuf>=4.29.2
218
+ Requires-Dist: protobuf>=5.26.1
219
219
  Requires-Dist: pyyaml>=6.0.1
220
220
  Requires-Dist: semver<4,>=3
221
221
  Description-Content-Type: text/markdown
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "openfeature-provider-flagd"
8
- version = "0.2.4"
8
+ version = "0.2.6"
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" }]
@@ -19,7 +19,7 @@ keywords = []
19
19
  dependencies = [
20
20
  "openfeature-sdk>=0.6.0",
21
21
  "grpcio>=1.68.1",
22
- "protobuf>=4.29.2",
22
+ "protobuf>=5.26.1",
23
23
  "mmh3>=4.1.0",
24
24
  "panzi-json-logic>=1.0.1",
25
25
  "semver>=3,<4",
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "pytest-bdd",
41
41
  "testcontainers",
42
42
  "asserts",
43
- "grpcio-health-checking==1.73.0",
43
+ "grpcio-health-checking",
44
44
  ]
45
45
  pre-install-commands = [
46
46
  "hatch build",
@@ -77,6 +77,7 @@ run = "mypy"
77
77
  [tool.hatch.build.hooks.protobuf]
78
78
  generate_pyi = false
79
79
  dependencies = [
80
+ "protobuf==5.26.1",
80
81
  "hatch-protobuf",
81
82
  "mypy-protobuf~=3.0",
82
83
  ]
@@ -5,13 +5,13 @@ from openfeature.contrib.provider.flagd.resolvers.process.connector.file_watcher
5
5
  )
6
6
  from openfeature.evaluation_context import EvaluationContext
7
7
  from openfeature.event import ProviderEventDetails
8
- from openfeature.exception import FlagNotFoundError, ParseError
8
+ from openfeature.exception import ErrorCode, FlagNotFoundError, GeneralError, ParseError
9
9
  from openfeature.flag_evaluation import FlagResolutionDetails, Reason
10
10
 
11
11
  from ..config import Config
12
12
  from .process.connector import FlagStateConnector
13
13
  from .process.connector.grpc_watcher import GrpcWatcher
14
- from .process.flags import FlagStore
14
+ from .process.flags import Flag, FlagStore
15
15
  from .process.targeting import targeting
16
16
 
17
17
  T = typing.TypeVar("T")
@@ -128,26 +128,30 @@ class InProcessResolver:
128
128
  )
129
129
 
130
130
  if not flag.targeting:
131
- variant, value = flag.default
132
- return FlagResolutionDetails(
133
- value, variant=variant, flag_metadata=metadata, reason=Reason.STATIC
134
- )
131
+ return _default_resolve(flag, metadata, Reason.STATIC)
135
132
 
136
- variant = targeting(flag.key, flag.targeting, evaluation_context)
133
+ try:
134
+ variant = targeting(flag.key, flag.targeting, evaluation_context)
135
+ if variant is None:
136
+ return _default_resolve(flag, metadata, Reason.DEFAULT)
137
137
 
138
- if variant is None:
139
- variant, value = flag.default
140
- return FlagResolutionDetails(
141
- value, variant=variant, flag_metadata=metadata, reason=Reason.DEFAULT
142
- )
143
- if not isinstance(variant, (str, bool)):
144
- raise ParseError(
145
- "Parsed JSONLogic targeting did not return a string or bool"
146
- )
138
+ # convert to string to support shorthand (boolean in python is with capital T hence the special case)
139
+ if isinstance(variant, bool):
140
+ variant = str(variant).lower()
141
+ elif not isinstance(variant, str):
142
+ variant = str(variant)
143
+
144
+ if variant not in flag.variants:
145
+ raise GeneralError(
146
+ f"Resolved variant {variant} not in variants config."
147
+ )
148
+
149
+ except ReferenceError:
150
+ raise ParseError(f"Invalid targeting {targeting}") from ReferenceError
147
151
 
148
152
  variant, value = flag.get_variant(variant)
149
153
  if value is None:
150
- raise ParseError(f"Resolved variant {variant} not in variants config.")
154
+ raise GeneralError(f"Resolved variant {variant} not in variants config.")
151
155
 
152
156
  return FlagResolutionDetails(
153
157
  value,
@@ -155,3 +159,24 @@ class InProcessResolver:
155
159
  reason=Reason.TARGETING_MATCH,
156
160
  flag_metadata=metadata,
157
161
  )
162
+
163
+
164
+ def _default_resolve(
165
+ flag: Flag,
166
+ metadata: typing.Mapping[str, typing.Union[float, int, str, bool]],
167
+ reason: Reason,
168
+ ) -> FlagResolutionDetails:
169
+ variant, value = flag.default
170
+ if variant is None:
171
+ return FlagResolutionDetails(
172
+ value,
173
+ variant=variant,
174
+ reason=Reason.ERROR,
175
+ error_code=ErrorCode.FLAG_NOT_FOUND,
176
+ flag_metadata=metadata,
177
+ )
178
+ if variant not in flag.variants:
179
+ raise GeneralError(f"Resolved variant {variant} not in variants config.")
180
+ return FlagResolutionDetails(
181
+ value, variant=variant, flag_metadata=metadata, reason=reason
182
+ )
@@ -258,7 +258,7 @@ class GrpcWatcher(FlagStateConnector):
258
258
  logger.debug("Terminating gRPC sync thread")
259
259
  return
260
260
  except grpc.RpcError as e: # noqa: PERF203
261
- logger.error(f"SyncFlags stream error, {e.code()=} {e.details()=}")
261
+ logger.debug(f"SyncFlags stream error, {e.code()=} {e.details()=}")
262
262
  except json.JSONDecodeError:
263
263
  logger.exception(
264
264
  f"Could not parse JSON flag data from SyncFlags endpoint: {flag_str=}"
@@ -72,30 +72,22 @@ class Flag:
72
72
  key: str
73
73
  state: str
74
74
  variants: typing.Mapping[str, typing.Any]
75
- default_variant: typing.Union[bool, str]
75
+ default_variant: typing.Optional[typing.Union[bool, str]] = None
76
76
  targeting: typing.Optional[dict] = None
77
77
  metadata: typing.Optional[
78
78
  typing.Mapping[str, typing.Union[float, int, str, bool]]
79
79
  ] = None
80
80
 
81
81
  def __post_init__(self) -> None:
82
- if not self.state or not isinstance(self.state, str):
82
+ if not self.state or not (self.state == "ENABLED" or self.state == "DISABLED"):
83
83
  raise ParseError("Incorrect 'state' value provided in flag config")
84
84
 
85
85
  if not self.variants or not isinstance(self.variants, dict):
86
86
  raise ParseError("Incorrect 'variants' value provided in flag config")
87
87
 
88
- if not self.default_variant or not isinstance(
89
- self.default_variant, (str, bool)
90
- ):
88
+ if self.default_variant and not isinstance(self.default_variant, (str, bool)):
91
89
  raise ParseError("Incorrect 'defaultVariant' value provided in flag config")
92
90
 
93
- if self.targeting and not isinstance(self.targeting, dict):
94
- raise ParseError("Incorrect 'targeting' value provided in flag config")
95
-
96
- if self.default_variant not in self.variants:
97
- raise ParseError("Default variant does not match set of variants")
98
-
99
91
  if self.metadata:
100
92
  if not isinstance(self.metadata, dict):
101
93
  raise ParseError("Flag metadata is not a valid json object")
@@ -106,6 +98,8 @@ class Flag:
106
98
  def from_dict(cls, key: str, data: dict) -> "Flag":
107
99
  if "defaultVariant" in data:
108
100
  data["default_variant"] = data["defaultVariant"]
101
+ if data["default_variant"] == "":
102
+ data["default_variant"] = None
109
103
  del data["defaultVariant"]
110
104
 
111
105
  data.pop("source", None)
@@ -119,13 +113,16 @@ class Flag:
119
113
  raise ParseError from err
120
114
 
121
115
  @property
122
- def default(self) -> tuple[str, typing.Any]:
116
+ def default(self) -> tuple[typing.Optional[str], typing.Any]:
123
117
  return self.get_variant(self.default_variant)
124
118
 
125
119
  def get_variant(
126
- self, variant_key: typing.Union[str, bool]
127
- ) -> tuple[str, typing.Any]:
120
+ self, variant_key: typing.Union[str, bool, None]
121
+ ) -> tuple[typing.Optional[str], typing.Any]:
128
122
  if isinstance(variant_key, bool):
129
123
  variant_key = str(variant_key).lower()
130
124
 
125
+ if not variant_key:
126
+ return None, None
127
+
131
128
  return variant_key, self.variants.get(variant_key)
@@ -5,6 +5,7 @@ from json_logic import builtins, jsonLogic
5
5
  from json_logic.types import JsonValue
6
6
 
7
7
  from openfeature.evaluation_context import EvaluationContext
8
+ from openfeature.exception import ParseError
8
9
 
9
10
  from .custom_ops import (
10
11
  ends_with,
@@ -27,6 +28,9 @@ def targeting(
27
28
  targeting: dict,
28
29
  evaluation_context: typing.Optional[EvaluationContext] = None,
29
30
  ) -> JsonValue:
31
+ if not isinstance(targeting, dict):
32
+ raise ParseError(f"Invalid 'targeting' value in flag: {targeting}")
33
+
30
34
  json_logic_context = evaluation_context.attributes if evaluation_context else {}
31
35
  json_logic_context["$flagd"] = {"flagKey": key, "timestamp": int(time.time())}
32
36
  json_logic_context["targetingKey"] = (
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation.proto
5
- # Protobuf Python Version: 6.31.0
5
+ # Protobuf Python Version: 5.29.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,8 +11,8 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 31,
14
+ 5,
15
+ 29,
16
16
  0,
17
17
  '',
18
18
  'openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation.proto'
@@ -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.73.0'
8
+ GRPC_GENERATED_VERSION = '1.71.2'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: openfeature/schemas/protobuf/flagd/sync/v1/sync.proto
5
- # Protobuf Python Version: 6.31.0
5
+ # Protobuf Python Version: 5.29.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,8 +11,8 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 31,
14
+ 5,
15
+ 29,
16
16
  0,
17
17
  '',
18
18
  'openfeature/schemas/protobuf/flagd/sync/v1/sync.proto'
@@ -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.73.0'
8
+ GRPC_GENERATED_VERSION = '1.71.2'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: openfeature/schemas/protobuf/schema/v1/schema.proto
5
- # Protobuf Python Version: 6.31.0
5
+ # Protobuf Python Version: 5.29.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,8 +11,8 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 31,
14
+ 5,
15
+ 29,
16
16
  0,
17
17
  '',
18
18
  'openfeature/schemas/protobuf/schema/v1/schema.proto'
@@ -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.73.0'
8
+ GRPC_GENERATED_VERSION = '1.71.2'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -2,7 +2,7 @@
2
2
  # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # NO CHECKED-IN PROTOBUF GENCODE
4
4
  # source: openfeature/schemas/protobuf/sync/v1/sync_service.proto
5
- # Protobuf Python Version: 6.31.0
5
+ # Protobuf Python Version: 5.29.0
6
6
  """Generated protocol buffer code."""
7
7
  from google.protobuf import descriptor as _descriptor
8
8
  from google.protobuf import descriptor_pool as _descriptor_pool
@@ -11,8 +11,8 @@ from google.protobuf import symbol_database as _symbol_database
11
11
  from google.protobuf.internal import builder as _builder
12
12
  _runtime_version.ValidateProtobufRuntimeVersion(
13
13
  _runtime_version.Domain.PUBLIC,
14
- 6,
15
- 31,
14
+ 5,
15
+ 29,
16
16
  0,
17
17
  '',
18
18
  'openfeature/schemas/protobuf/sync/v1/sync_service.proto'
@@ -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.73.0'
8
+ GRPC_GENERATED_VERSION = '1.71.2'
9
9
  GRPC_VERSION = grpc.__version__
10
10
  _version_not_supported = False
11
11
 
@@ -48,7 +48,7 @@ class FlagdContainer(DockerContainer):
48
48
  return self
49
49
 
50
50
  @wait_container_is_ready(ConnectionError)
51
- def _checker(self, host: str, port: str) -> None:
51
+ def _checker(self, host: str, port: int) -> None:
52
52
  # First we wait for Flagd to say it's listening
53
53
  wait_for_logs(
54
54
  self,
@@ -58,7 +58,7 @@ class FlagdContainer(DockerContainer):
58
58
 
59
59
  time.sleep(1)
60
60
  # Second we use the GRPC health check endpoint
61
- with grpc.insecure_channel(host + ":" + port) as channel:
61
+ with grpc.insecure_channel(host + ":" + str(port)) as channel:
62
62
  health_stub = health_pb2_grpc.HealthStub(channel)
63
63
 
64
64
  def health_check_call(stub: health_pb2_grpc.HealthStub):
@@ -36,6 +36,18 @@ def update_context(
36
36
  evaluation_context.attributes[key] = type_cast[type_info](value)
37
37
 
38
38
 
39
+ @given(
40
+ parsers.cfparse(
41
+ 'a context containing a key "{key}", with type "{type_info}" and with value ""'
42
+ ),
43
+ )
44
+ def update_context_without_value(
45
+ evaluation_context: EvaluationContext, key: str, type_info: str
46
+ ):
47
+ """a context containing a key and value."""
48
+ update_context(evaluation_context, key, type_info, "")
49
+
50
+
39
51
  @when(
40
52
  parsers.cfparse(
41
53
  'context contains keys {fields:s} with values "{svalue}", "{svalue2}", {ivalue:d}, "{bvalue:bool}"',
@@ -1,5 +1,5 @@
1
1
  import requests
2
- from asserts import assert_equal
2
+ from asserts import assert_equal, assert_is_none
3
3
  from pytest_bdd import given, parsers, then, when
4
4
 
5
5
  from openfeature.client import OpenFeatureClient
@@ -96,6 +96,25 @@ def resolve_details_reason(
96
96
  assert_equal(details.reason, Reason(reason))
97
97
 
98
98
 
99
+ @then(
100
+ parsers.cfparse('the error-code should be "{error_code}"'),
101
+ )
102
+ def resolve_details_error_code(
103
+ details: FlagEvaluationDetails[JsonPrimitive],
104
+ error_code: str,
105
+ ):
106
+ assert_equal(details.error_code, error_code)
107
+
108
+
109
+ @then(
110
+ parsers.cfparse('the error-code should be ""'),
111
+ )
112
+ def resolve_details_empty_error_code(
113
+ details: FlagEvaluationDetails[JsonPrimitive],
114
+ ):
115
+ assert_is_none(details.error_code)
116
+
117
+
99
118
  @then(parsers.cfparse("the resolved metadata should contain"))
100
119
  def metadata_contains(details: FlagEvaluationDetails[JsonPrimitive], datatable):
101
120
  assert_equal(len(details.flag_metadata), len(datatable) - 1) # skip table header
@@ -0,0 +1,12 @@
1
+ {
2
+ "flags": {
3
+ "basic-flag": {
4
+ "state": "ENABLED",
5
+ "variants": {
6
+ "true": true,
7
+ "false": false
8
+ },
9
+ "targeting": {}
10
+ }
11
+ }
12
+ }
@@ -23,11 +23,9 @@ def create_client(provider: FlagdProvider):
23
23
  "not-a-flag.json",
24
24
  "basic-flag-wrong-structure.json",
25
25
  "basic-flag-invalid.not-json",
26
- "basic-flag-wrong-variant.json",
27
26
  "basic-flag-broken-state.json",
28
27
  "basic-flag-broken-variants.json",
29
28
  "basic-flag-broken-default.json",
30
- "basic-flag-broken-targeting.json",
31
29
  ],
32
30
  )
33
31
  def test_file_load_errors(file_name: str):
@@ -46,6 +44,38 @@ def test_file_load_errors(file_name: str):
46
44
  assert res.error_code == ErrorCode.FLAG_NOT_FOUND
47
45
 
48
46
 
47
+ def test_non_existent_variant():
48
+ path = os.path.abspath(os.path.join(os.path.dirname(__file__), "./flags/"))
49
+ client = create_client(
50
+ FlagdProvider(
51
+ resolver_type=ResolverType.IN_PROCESS,
52
+ offline_flag_source_path=f"{path}/basic-flag-wrong-variant.json",
53
+ )
54
+ )
55
+
56
+ res = client.get_boolean_details("basic-flag", False)
57
+
58
+ assert res.value is False
59
+ assert res.reason == Reason.ERROR
60
+ assert res.error_code == ErrorCode.GENERAL
61
+
62
+
63
+ def test_broken_targeting():
64
+ path = os.path.abspath(os.path.join(os.path.dirname(__file__), "./flags/"))
65
+ client = create_client(
66
+ FlagdProvider(
67
+ resolver_type=ResolverType.IN_PROCESS,
68
+ offline_flag_source_path=f"{path}/basic-flag-broken-targeting.json",
69
+ )
70
+ )
71
+
72
+ res = client.get_boolean_details("basic-flag", False)
73
+
74
+ assert res.value is False
75
+ assert res.reason == Reason.ERROR
76
+ assert res.error_code == ErrorCode.PARSE_ERROR
77
+
78
+
49
79
  @pytest.mark.parametrize(
50
80
  "file_name",
51
81
  [
@@ -21,6 +21,7 @@ def create_client(provider: FlagdProvider):
21
21
  "file_name",
22
22
  [
23
23
  "basic-flag.json",
24
+ "basic-flag-without-default.json",
24
25
  "basic-flag.yaml",
25
26
  ],
26
27
  )
@@ -6,7 +6,7 @@ from openfeature.contrib.provider.flagd.config import Config
6
6
  from openfeature.contrib.provider.flagd.resolvers.in_process import InProcessResolver
7
7
  from openfeature.contrib.provider.flagd.resolvers.process.flags import Flag, FlagStore
8
8
  from openfeature.evaluation_context import EvaluationContext
9
- from openfeature.exception import FlagNotFoundError, ParseError
9
+ from openfeature.exception import FlagNotFoundError, GeneralError
10
10
 
11
11
 
12
12
  def targeting():
@@ -79,7 +79,7 @@ def test_resolve_boolean_details_invalid_variant(resolver, flag):
79
79
 
80
80
  resolver.flag_store.get_flag = Mock(return_value=flag)
81
81
 
82
- with pytest.raises(ParseError):
82
+ with pytest.raises(GeneralError):
83
83
  resolver.resolve_boolean_details("flag", False)
84
84
 
85
85