openfeature-provider-flagd 0.1.3__tar.gz → 0.1.5__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 (96) hide show
  1. openfeature_provider_flagd-0.1.5/CHANGELOG.md +60 -0
  2. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/PKG-INFO +19 -2
  3. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/README.md +13 -0
  4. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/pyproject.toml +14 -1
  5. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/config.py +59 -0
  6. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/provider.py +136 -0
  7. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/__init__.py +51 -0
  8. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +145 -0
  9. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +122 -0
  10. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +126 -0
  11. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/process/file_watcher.py +89 -0
  12. openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +51 -0
  13. openfeature_provider_flagd-0.1.5/test-harness/.gherkin-lintrc +57 -0
  14. openfeature_provider_flagd-0.1.5/test-harness/.git +1 -0
  15. openfeature_provider_flagd-0.1.5/test-harness/.github/workflows/ci.yml +59 -0
  16. openfeature_provider_flagd-0.1.5/test-harness/.github/workflows/lint-pr.yml +42 -0
  17. openfeature_provider_flagd-0.1.5/test-harness/.github/workflows/release-please.yml +93 -0
  18. openfeature_provider_flagd-0.1.5/test-harness/.release-please-manifest.json +3 -0
  19. openfeature_provider_flagd-0.1.5/test-harness/CHANGELOG.md +210 -0
  20. openfeature_provider_flagd-0.1.5/test-harness/Makefile +6 -0
  21. openfeature_provider_flagd-0.1.5/test-harness/README.md +41 -0
  22. openfeature_provider_flagd-0.1.5/test-harness/flagd/Dockerfile +17 -0
  23. openfeature_provider_flagd-0.1.5/test-harness/flagd/Dockerfile.unstable +12 -0
  24. openfeature_provider_flagd-0.1.5/test-harness/flags/changing-flag-bar.json +12 -0
  25. openfeature_provider_flagd-0.1.5/test-harness/flags/changing-flag-foo.json +12 -0
  26. openfeature_provider_flagd-0.1.5/test-harness/flags/custom-ops.json +167 -0
  27. openfeature_provider_flagd-0.1.5/test-harness/flags/edge-case-flags.json +57 -0
  28. openfeature_provider_flagd-0.1.5/test-harness/flags/evaluator-refs.json +52 -0
  29. openfeature_provider_flagd-0.1.5/test-harness/flags/testing-flags.json +156 -0
  30. openfeature_provider_flagd-0.1.5/test-harness/flags/zero-flags.json +36 -0
  31. openfeature_provider_flagd-0.1.5/test-harness/gherkin/flagd-json-evaluator.feature +117 -0
  32. openfeature_provider_flagd-0.1.5/test-harness/gherkin/flagd-reconnect.feature +15 -0
  33. openfeature_provider_flagd-0.1.5/test-harness/gherkin/flagd.feature +36 -0
  34. openfeature_provider_flagd-0.1.5/test-harness/package-lock.json +682 -0
  35. openfeature_provider_flagd-0.1.5/test-harness/package.json +8 -0
  36. openfeature_provider_flagd-0.1.5/test-harness/release-please-config.json +11 -0
  37. openfeature_provider_flagd-0.1.5/test-harness/renovate.json +15 -0
  38. openfeature_provider_flagd-0.1.5/test-harness/scripts/change-flag-wrapper.sh +22 -0
  39. openfeature_provider_flagd-0.1.5/test-harness/scripts/change-flag.sh +15 -0
  40. openfeature_provider_flagd-0.1.5/test-harness/scripts/restart-wrapper.sh +31 -0
  41. openfeature_provider_flagd-0.1.5/test-harness/sync/Dockerfile +22 -0
  42. openfeature_provider_flagd-0.1.5/test-harness/sync/Dockerfile.unstable +17 -0
  43. openfeature_provider_flagd-0.1.5/test-harness/sync/README.md +91 -0
  44. openfeature_provider_flagd-0.1.5/test-harness/sync/go.mod +20 -0
  45. openfeature_provider_flagd-0.1.5/test-harness/sync/go.sum +1138 -0
  46. openfeature_provider_flagd-0.1.5/test-harness/sync/main.go +13 -0
  47. openfeature_provider_flagd-0.1.5/test-harness/sync/pkg/config.go +50 -0
  48. openfeature_provider_flagd-0.1.5/test-harness/sync/pkg/file_watcher.go +150 -0
  49. openfeature_provider_flagd-0.1.5/test-harness/sync/pkg/server.go +136 -0
  50. openfeature_provider_flagd-0.1.5/tests/conftest.py +21 -0
  51. openfeature_provider_flagd-0.1.5/tests/e2e/conftest.py +208 -0
  52. openfeature_provider_flagd-0.1.5/tests/e2e/parsers.py +2 -0
  53. openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_custom_ops.py +38 -0
  54. openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_edge_cases.py +15 -0
  55. openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_evaluator_reuse.py +13 -0
  56. openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_events.py +91 -0
  57. openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_testing_flags.py +24 -0
  58. openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_zero_evals.py +28 -0
  59. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-default.json +13 -0
  60. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-state.json +13 -0
  61. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-targeting.json +15 -0
  62. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-variants.json +15 -0
  63. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-disabled.json +13 -0
  64. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-invalid.not-json +13 -0
  65. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-no-state.json +12 -0
  66. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-wrong-structure.json +11 -0
  67. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-wrong-variant.json +12 -0
  68. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag.json +13 -0
  69. openfeature_provider_flagd-0.1.5/tests/flags/basic-flag.yaml +8 -0
  70. openfeature_provider_flagd-0.1.5/tests/flags/invalid-fractional-args.json +16 -0
  71. openfeature_provider_flagd-0.1.5/tests/flags/invalid-fractional-weights.json +19 -0
  72. openfeature_provider_flagd-0.1.5/tests/flags/invalid-semver-args.json +16 -0
  73. openfeature_provider_flagd-0.1.5/tests/flags/invalid-semver-op.json +16 -0
  74. openfeature_provider_flagd-0.1.5/tests/flags/invalid-stringcomp-args.json +16 -0
  75. openfeature_provider_flagd-0.1.5/tests/test_config.py +29 -0
  76. openfeature_provider_flagd-0.1.5/tests/test_errors.py +79 -0
  77. openfeature_provider_flagd-0.1.5/tests/test_file_store.py +33 -0
  78. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/tests/test_flagd.py +8 -0
  79. openfeature_provider_flagd-0.1.3/CHANGELOG.md +0 -28
  80. openfeature_provider_flagd-0.1.3/src/openfeature/contrib/provider/flagd/defaults.py +0 -5
  81. openfeature_provider_flagd-0.1.3/src/openfeature/contrib/provider/flagd/provider.py +0 -202
  82. openfeature_provider_flagd-0.1.3/tests/conftest.py +0 -10
  83. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/.gitignore +0 -0
  84. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/LICENSE +0 -0
  85. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/scripts/gen_protos.sh +0 -0
  86. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
  87. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
  88. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2.py +0 -0
  89. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2_grpc.py +0 -0
  90. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2.py +0 -0
  91. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2_grpc.py +0 -0
  92. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2.py +0 -0
  93. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2_grpc.py +0 -0
  94. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2.py +0 -0
  95. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2_grpc.py +0 -0
  96. {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/tests/__init__.py +0 -0
@@ -0,0 +1,60 @@
1
+ # Changelog
2
+
3
+ ## [0.1.5](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.1.4...openfeature-provider-flagd/v0.1.5) (2024-04-11)
4
+
5
+
6
+ ### ✨ New Features
7
+
8
+ * in-process offline flagd resolver ([#74](https://github.com/open-feature/python-sdk-contrib/issues/74)) ([8cea506](https://github.com/open-feature/python-sdk-contrib/commit/8cea5066ee96f637f3108a9dc3a7539c450a14be))
9
+
10
+ ## [0.1.4](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.1.3...openfeature-provider-flagd/v0.1.4) (2024-03-26)
11
+
12
+
13
+ ### 🐛 Bug Fixes
14
+
15
+ * include targetingKey in flagd serialized evaluation context ([#58](https://github.com/open-feature/python-sdk-contrib/issues/58)) ([ddd79a4](https://github.com/open-feature/python-sdk-contrib/commit/ddd79a49b765aa0679a2c1938447c61b37b6d0fe))
16
+ * respect timeout setting in grpc method calls ([#60](https://github.com/open-feature/python-sdk-contrib/issues/60)) ([0149cf7](https://github.com/open-feature/python-sdk-contrib/commit/0149cf7ced8116f54a9b220549834a1970460bd9))
17
+ * return proper metadata object in FlagdProvider ([#59](https://github.com/open-feature/python-sdk-contrib/issues/59)) ([6508234](https://github.com/open-feature/python-sdk-contrib/commit/6508234486ba0b650e849cbee22505988233131a))
18
+
19
+
20
+ ### ✨ New Features
21
+
22
+ * implement environment-variable based config ([#62](https://github.com/open-feature/python-sdk-contrib/issues/62)) ([a8b78b2](https://github.com/open-feature/python-sdk-contrib/commit/a8b78b28fe44ca712b00db04ac1a23a9c9bc6d9b))
23
+ * replace schema with tls argument in FlagdProvider constructor ([#61](https://github.com/open-feature/python-sdk-contrib/issues/61)) ([7a7210f](https://github.com/open-feature/python-sdk-contrib/commit/7a7210f6f63a9cba886f4d512c01ebac39d910a9))
24
+
25
+
26
+ ### 🧹 Chore
27
+
28
+ * exclude generated protobuf files from coverage report ([#51](https://github.com/open-feature/python-sdk-contrib/issues/51)) ([660a0cb](https://github.com/open-feature/python-sdk-contrib/commit/660a0cbc9bb932ac0dd9cb09f1d75177b161601b))
29
+
30
+
31
+ ### 🔄 Refactoring
32
+
33
+ * add mypy and fix typing issues ([#72](https://github.com/open-feature/python-sdk-contrib/issues/72)) ([b405925](https://github.com/open-feature/python-sdk-contrib/commit/b4059255045cdb7054a35bc338207e23c42ce068))
34
+
35
+ ## [0.1.3](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd/v0.1.2...openfeature-provider-flagd/v0.1.3) (2024-02-23)
36
+
37
+
38
+ ### 🐛 Bug Fixes
39
+
40
+ * include proto file in build for openfeature-provider-flagd ([#45](https://github.com/open-feature/python-sdk-contrib/issues/45)) ([7783cc8](https://github.com/open-feature/python-sdk-contrib/commit/7783cc8e7fb8fe0f9b812938efcd1f4c07e3ff68))
41
+
42
+ ## [0.1.2](https://github.com/open-feature/python-sdk-contrib/compare/openfeature-provider-flagd-v0.1.1...openfeature-provider-flagd/v0.1.2) (2024-02-22)
43
+
44
+
45
+ ### 🐛 Bug Fixes
46
+
47
+ * remove mention of local eval in readme ([41df80e](https://github.com/open-feature/python-sdk-contrib/commit/41df80e1b3044356e3b228a484f3a13c92068d91))
48
+ * remove setup from flagd tests ([#39](https://github.com/open-feature/python-sdk-contrib/issues/39)) ([85661ff](https://github.com/open-feature/python-sdk-contrib/commit/85661ff170b378d37b0a3d5d0a955dad3417f538))
49
+
50
+
51
+ ### 🧹 Chore
52
+
53
+ * **main:** release providers/flagd 0.1.1 ([#40](https://github.com/open-feature/python-sdk-contrib/issues/40)) ([d42ee1e](https://github.com/open-feature/python-sdk-contrib/commit/d42ee1e531249e0023456dbe46db2f4f0c52a5c5))
54
+
55
+ ## [0.1.1](https://github.com/open-feature/python-sdk-contrib/compare/providers/flagd-v0.1.0...providers/flagd/v0.1.1) (2024-02-22)
56
+
57
+
58
+ ### 🐛 Bug Fixes
59
+
60
+ * remove setup from flagd tests ([#39](https://github.com/open-feature/python-sdk-contrib/issues/39)) ([85661ff](https://github.com/open-feature/python-sdk-contrib/commit/85661ff170b378d37b0a3d5d0a955dad3417f538))
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: openfeature-provider-flagd
3
- Version: 0.1.3
3
+ Version: 0.1.5
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>
@@ -211,8 +211,12 @@ Classifier: Programming Language :: Python
211
211
  Classifier: Programming Language :: Python :: 3
212
212
  Requires-Python: >=3.8
213
213
  Requires-Dist: grpcio>=1.60.0
214
+ Requires-Dist: mmh3>=4.1.0
214
215
  Requires-Dist: openfeature-sdk>=0.4.0
216
+ Requires-Dist: panzi-json-logic>=1.0.1
215
217
  Requires-Dist: protobuf>=4.25.2
218
+ Requires-Dist: pyyaml>=6.0.1
219
+ Requires-Dist: semver<4,>=3
216
220
  Description-Content-Type: text/markdown
217
221
 
218
222
  # flagd Provider for OpenFeature
@@ -236,6 +240,19 @@ from openfeature.contrib.provider.flagd import FlagdProvider
236
240
  api.set_provider(FlagdProvider())
237
241
  ```
238
242
 
243
+ To use in-process evaluation in offline mode with a file as source:
244
+
245
+ ```python
246
+ from openfeature import api
247
+ from openfeature.contrib.provider.flagd import FlagdProvider
248
+ from openfeature.contrib.provider.flagd.config import ResolverType
249
+
250
+ api.set_provider(FlagdProvider(
251
+ resolver_type=ResolverType.IN_PROCESS,
252
+ offline_flag_source_path="my-flag.json",
253
+ ))
254
+ ```
255
+
239
256
  ### Configuration options
240
257
 
241
258
  The default options can be defined in the FlagdProvider constructor.
@@ -19,6 +19,19 @@ from openfeature.contrib.provider.flagd import FlagdProvider
19
19
  api.set_provider(FlagdProvider())
20
20
  ```
21
21
 
22
+ To use in-process evaluation in offline mode with a file as source:
23
+
24
+ ```python
25
+ from openfeature import api
26
+ from openfeature.contrib.provider.flagd import FlagdProvider
27
+ from openfeature.contrib.provider.flagd.config import ResolverType
28
+
29
+ api.set_provider(FlagdProvider(
30
+ resolver_type=ResolverType.IN_PROCESS,
31
+ offline_flag_source_path="my-flag.json",
32
+ ))
33
+ ```
34
+
22
35
  ### Configuration options
23
36
 
24
37
  The default options can be defined in the FlagdProvider constructor.
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "openfeature-provider-flagd"
8
- version = "0.1.3"
8
+ version = "0.1.5"
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" }]
@@ -20,6 +20,10 @@ dependencies = [
20
20
  "openfeature-sdk>=0.4.0",
21
21
  "grpcio>=1.60.0",
22
22
  "protobuf>=4.25.2",
23
+ "mmh3>=4.1.0",
24
+ "panzi-json-logic>=1.0.1",
25
+ "semver>=3,<4",
26
+ "pyyaml>=6.0.1",
23
27
  ]
24
28
  requires-python = ">=3.8"
25
29
 
@@ -32,6 +36,7 @@ Homepage = "https://github.com/open-feature/python-sdk-contrib"
32
36
  dependencies = [
33
37
  "coverage[toml]>=6.5",
34
38
  "pytest",
39
+ "pytest-bdd",
35
40
  ]
36
41
  post-install-commands = [
37
42
  "./scripts/gen_protos.sh"
@@ -42,6 +47,7 @@ test = "pytest {args:tests}"
42
47
  test-cov = "coverage run -m pytest {args:tests}"
43
48
  cov-report = [
44
49
  "coverage xml",
50
+ "coverage html",
45
51
  ]
46
52
  cov = [
47
53
  "test-cov",
@@ -56,3 +62,10 @@ exclude = [
56
62
 
57
63
  [tool.hatch.build.targets.wheel]
58
64
  packages = ["src/openfeature"]
65
+
66
+ [tool.coverage.run]
67
+ omit = [
68
+ # exclude generated files
69
+ "src/openfeature/contrib/provider/flagd/proto/*",
70
+ "tests/**",
71
+ ]
@@ -0,0 +1,59 @@
1
+ import os
2
+ import typing
3
+ from enum import Enum
4
+
5
+ T = typing.TypeVar("T")
6
+
7
+
8
+ def str_to_bool(val: str) -> bool:
9
+ return val.lower() == "true"
10
+
11
+
12
+ def env_or_default(
13
+ env_var: str, default: T, cast: typing.Optional[typing.Callable[[str], T]] = None
14
+ ) -> typing.Union[str, T]:
15
+ val = os.environ.get(env_var)
16
+ if val is None:
17
+ return default
18
+ return val if cast is None else cast(val)
19
+
20
+
21
+ class ResolverType(Enum):
22
+ GRPC = "grpc"
23
+ IN_PROCESS = "in-process"
24
+
25
+
26
+ class Config:
27
+ def __init__( # noqa: PLR0913
28
+ self,
29
+ host: typing.Optional[str] = None,
30
+ port: typing.Optional[int] = None,
31
+ tls: typing.Optional[bool] = None,
32
+ timeout: typing.Optional[int] = None,
33
+ resolver_type: typing.Optional[ResolverType] = None,
34
+ offline_flag_source_path: typing.Optional[str] = None,
35
+ offline_poll_interval_seconds: typing.Optional[float] = None,
36
+ ):
37
+ self.host = env_or_default("FLAGD_HOST", "localhost") if host is None else host
38
+ self.port = (
39
+ env_or_default("FLAGD_PORT", 8013, cast=int) if port is None else port
40
+ )
41
+ self.tls = (
42
+ env_or_default("FLAGD_TLS", False, cast=str_to_bool) if tls is None else tls
43
+ )
44
+ self.timeout = 5 if timeout is None else timeout
45
+ self.resolver_type = (
46
+ ResolverType(env_or_default("FLAGD_RESOLVER_TYPE", "grpc"))
47
+ if resolver_type is None
48
+ else resolver_type
49
+ )
50
+ self.offline_flag_source_path = (
51
+ env_or_default("FLAGD_OFFLINE_FLAG_SOURCE_PATH", None)
52
+ if offline_flag_source_path is None
53
+ else offline_flag_source_path
54
+ )
55
+ self.offline_poll_interval_seconds = (
56
+ float(env_or_default("FLAGD_OFFLINE_POLL_INTERVAL_SECONDS", 1.0))
57
+ if offline_poll_interval_seconds is None
58
+ else offline_poll_interval_seconds
59
+ )
@@ -0,0 +1,136 @@
1
+ """
2
+ # This is a Python Provider to interact with flagd
3
+ #
4
+ # -- Usage --
5
+ # open_feature_api.set_provider(flagd_provider.FlagdProvider())
6
+ # flag_value = open_feature_client.get_string_value(
7
+ # key="foo",
8
+ # default_value="missingflag"
9
+ # )
10
+ # print(f"Flag Value is: {flag_value}")
11
+ # OR the more verbose option
12
+ # flag = open_feature_client.get_string_details(key="foo", default_value="missingflag")
13
+ # print(f"Flag is: {flag.value}")
14
+ # OR
15
+ # print(f"Flag Details: {vars(flag)}"")
16
+ #
17
+ # -- Customisation --
18
+ # Follows flagd defaults: 'http' protocol on 'localhost' on port '8013'
19
+ # But can be overridden:
20
+ # provider = open_feature_api.get_provider()
21
+ # provider.initialise(schema="https",endpoint="example.com",port=1234,timeout=10)
22
+ """
23
+
24
+ import typing
25
+
26
+ from openfeature.evaluation_context import EvaluationContext
27
+ from openfeature.flag_evaluation import FlagResolutionDetails
28
+ from openfeature.provider.metadata import Metadata
29
+ from openfeature.provider.provider import AbstractProvider
30
+
31
+ from .config import Config, ResolverType
32
+ from .resolvers import AbstractResolver, GrpcResolver, InProcessResolver
33
+
34
+ T = typing.TypeVar("T")
35
+
36
+
37
+ class FlagdProvider(AbstractProvider):
38
+ """Flagd OpenFeature Provider"""
39
+
40
+ def __init__( # noqa: PLR0913
41
+ self,
42
+ host: typing.Optional[str] = None,
43
+ port: typing.Optional[int] = None,
44
+ tls: typing.Optional[bool] = None,
45
+ timeout: typing.Optional[int] = None,
46
+ resolver_type: typing.Optional[ResolverType] = None,
47
+ offline_flag_source_path: typing.Optional[str] = None,
48
+ offline_poll_interval_seconds: typing.Optional[float] = None,
49
+ ):
50
+ """
51
+ Create an instance of the FlagdProvider
52
+
53
+ :param host: the host to make requests to
54
+ :param port: the port the flagd service is available on
55
+ :param tls: enable/disable secure TLS connectivity
56
+ :param timeout: the maximum to wait before a request times out
57
+ """
58
+ self.config = Config(
59
+ host=host,
60
+ port=port,
61
+ tls=tls,
62
+ timeout=timeout,
63
+ resolver_type=resolver_type,
64
+ offline_flag_source_path=offline_flag_source_path,
65
+ offline_poll_interval_seconds=offline_poll_interval_seconds,
66
+ )
67
+
68
+ self.resolver = self.setup_resolver()
69
+
70
+ def setup_resolver(self) -> AbstractResolver:
71
+ if self.config.resolver_type == ResolverType.GRPC:
72
+ return GrpcResolver(self.config)
73
+ elif self.config.resolver_type == ResolverType.IN_PROCESS:
74
+ return InProcessResolver(self.config, self)
75
+ else:
76
+ raise ValueError(
77
+ f"`resolver_type` parameter invalid: {self.config.resolver_type}"
78
+ )
79
+
80
+ def shutdown(self) -> None:
81
+ if self.resolver:
82
+ self.resolver.shutdown()
83
+
84
+ def get_metadata(self) -> Metadata:
85
+ """Returns provider metadata"""
86
+ return Metadata(name="FlagdProvider")
87
+
88
+ def resolve_boolean_details(
89
+ self,
90
+ key: str,
91
+ default_value: bool,
92
+ evaluation_context: typing.Optional[EvaluationContext] = None,
93
+ ) -> FlagResolutionDetails[bool]:
94
+ return self.resolver.resolve_boolean_details(
95
+ key, default_value, evaluation_context
96
+ )
97
+
98
+ def resolve_string_details(
99
+ self,
100
+ key: str,
101
+ default_value: str,
102
+ evaluation_context: typing.Optional[EvaluationContext] = None,
103
+ ) -> FlagResolutionDetails[str]:
104
+ return self.resolver.resolve_string_details(
105
+ key, default_value, evaluation_context
106
+ )
107
+
108
+ def resolve_float_details(
109
+ self,
110
+ key: str,
111
+ default_value: float,
112
+ evaluation_context: typing.Optional[EvaluationContext] = None,
113
+ ) -> FlagResolutionDetails[float]:
114
+ return self.resolver.resolve_float_details(
115
+ key, default_value, evaluation_context
116
+ )
117
+
118
+ def resolve_integer_details(
119
+ self,
120
+ key: str,
121
+ default_value: int,
122
+ evaluation_context: typing.Optional[EvaluationContext] = None,
123
+ ) -> FlagResolutionDetails[int]:
124
+ return self.resolver.resolve_integer_details(
125
+ key, default_value, evaluation_context
126
+ )
127
+
128
+ def resolve_object_details(
129
+ self,
130
+ key: str,
131
+ default_value: typing.Union[dict, list],
132
+ evaluation_context: typing.Optional[EvaluationContext] = None,
133
+ ) -> FlagResolutionDetails[typing.Union[dict, list]]:
134
+ return self.resolver.resolve_object_details(
135
+ key, default_value, evaluation_context
136
+ )
@@ -0,0 +1,51 @@
1
+ import typing
2
+
3
+ from typing_extensions import Protocol
4
+
5
+ from openfeature.evaluation_context import EvaluationContext
6
+ from openfeature.flag_evaluation import FlagResolutionDetails
7
+
8
+ from .grpc import GrpcResolver
9
+ from .in_process import InProcessResolver
10
+
11
+
12
+ class AbstractResolver(Protocol):
13
+ def shutdown(self) -> None: ...
14
+
15
+ def resolve_boolean_details(
16
+ self,
17
+ key: str,
18
+ default_value: bool,
19
+ evaluation_context: typing.Optional[EvaluationContext] = None,
20
+ ) -> FlagResolutionDetails[bool]: ...
21
+
22
+ def resolve_string_details(
23
+ self,
24
+ key: str,
25
+ default_value: str,
26
+ evaluation_context: typing.Optional[EvaluationContext] = None,
27
+ ) -> FlagResolutionDetails[str]: ...
28
+
29
+ def resolve_float_details(
30
+ self,
31
+ key: str,
32
+ default_value: float,
33
+ evaluation_context: typing.Optional[EvaluationContext] = None,
34
+ ) -> FlagResolutionDetails[float]: ...
35
+
36
+ def resolve_integer_details(
37
+ self,
38
+ key: str,
39
+ default_value: int,
40
+ evaluation_context: typing.Optional[EvaluationContext] = None,
41
+ ) -> FlagResolutionDetails[int]: ...
42
+
43
+ def resolve_object_details(
44
+ self,
45
+ key: str,
46
+ default_value: typing.Union[dict, list],
47
+ evaluation_context: typing.Optional[EvaluationContext] = None,
48
+ ) -> FlagResolutionDetails[typing.Union[dict, list]]: ...
49
+
50
+
51
+ __all__ = ["AbstractResolver", "GrpcResolver", "InProcessResolver"]
@@ -0,0 +1,145 @@
1
+ import typing
2
+
3
+ import grpc
4
+ from google.protobuf.struct_pb2 import Struct
5
+
6
+ from openfeature.evaluation_context import EvaluationContext
7
+ from openfeature.exception import (
8
+ FlagNotFoundError,
9
+ GeneralError,
10
+ InvalidContextError,
11
+ ParseError,
12
+ TypeMismatchError,
13
+ )
14
+ from openfeature.flag_evaluation import FlagResolutionDetails
15
+
16
+ from ..config import Config
17
+ from ..flag_type import FlagType
18
+ from ..proto.schema.v1 import schema_pb2, schema_pb2_grpc
19
+
20
+ T = typing.TypeVar("T")
21
+
22
+
23
+ class GrpcResolver:
24
+ def __init__(self, config: Config):
25
+ self.config = config
26
+ channel_factory = (
27
+ grpc.secure_channel if self.config.tls else grpc.insecure_channel
28
+ )
29
+ self.channel = channel_factory(f"{self.config.host}:{self.config.port}")
30
+ self.stub = schema_pb2_grpc.ServiceStub(self.channel)
31
+
32
+ def shutdown(self) -> None:
33
+ self.channel.close()
34
+
35
+ def resolve_boolean_details(
36
+ self,
37
+ key: str,
38
+ default_value: bool,
39
+ evaluation_context: typing.Optional[EvaluationContext] = None,
40
+ ) -> FlagResolutionDetails[bool]:
41
+ return self._resolve(key, FlagType.BOOLEAN, default_value, evaluation_context)
42
+
43
+ def resolve_string_details(
44
+ self,
45
+ key: str,
46
+ default_value: str,
47
+ evaluation_context: typing.Optional[EvaluationContext] = None,
48
+ ) -> FlagResolutionDetails[str]:
49
+ return self._resolve(key, FlagType.STRING, default_value, evaluation_context)
50
+
51
+ def resolve_float_details(
52
+ self,
53
+ key: str,
54
+ default_value: float,
55
+ evaluation_context: typing.Optional[EvaluationContext] = None,
56
+ ) -> FlagResolutionDetails[float]:
57
+ return self._resolve(key, FlagType.FLOAT, default_value, evaluation_context)
58
+
59
+ def resolve_integer_details(
60
+ self,
61
+ key: str,
62
+ default_value: int,
63
+ evaluation_context: typing.Optional[EvaluationContext] = None,
64
+ ) -> FlagResolutionDetails[int]:
65
+ return self._resolve(key, FlagType.INTEGER, default_value, evaluation_context)
66
+
67
+ def resolve_object_details(
68
+ self,
69
+ key: str,
70
+ default_value: typing.Union[dict, list],
71
+ evaluation_context: typing.Optional[EvaluationContext] = None,
72
+ ) -> FlagResolutionDetails[typing.Union[dict, list]]:
73
+ return self._resolve(key, FlagType.OBJECT, default_value, evaluation_context)
74
+
75
+ def _resolve(
76
+ self,
77
+ flag_key: str,
78
+ flag_type: FlagType,
79
+ default_value: T,
80
+ evaluation_context: typing.Optional[EvaluationContext],
81
+ ) -> FlagResolutionDetails[T]:
82
+ context = self._convert_context(evaluation_context)
83
+ call_args = {"timeout": self.config.timeout}
84
+ try:
85
+ if flag_type == FlagType.BOOLEAN:
86
+ request = schema_pb2.ResolveBooleanRequest( # type:ignore[attr-defined]
87
+ flag_key=flag_key, context=context
88
+ )
89
+ response = self.stub.ResolveBoolean(request, **call_args)
90
+ elif flag_type == FlagType.STRING:
91
+ request = schema_pb2.ResolveStringRequest( # type:ignore[attr-defined]
92
+ flag_key=flag_key, context=context
93
+ )
94
+ response = self.stub.ResolveString(request, **call_args)
95
+ elif flag_type == FlagType.OBJECT:
96
+ request = schema_pb2.ResolveObjectRequest( # type:ignore[attr-defined]
97
+ flag_key=flag_key, context=context
98
+ )
99
+ response = self.stub.ResolveObject(request, **call_args)
100
+ elif flag_type == FlagType.FLOAT:
101
+ request = schema_pb2.ResolveFloatRequest( # type:ignore[attr-defined]
102
+ flag_key=flag_key, context=context
103
+ )
104
+ response = self.stub.ResolveFloat(request, **call_args)
105
+ elif flag_type == FlagType.INTEGER:
106
+ request = schema_pb2.ResolveIntRequest( # type:ignore[attr-defined]
107
+ flag_key=flag_key, context=context
108
+ )
109
+ response = self.stub.ResolveInt(request, **call_args)
110
+ else:
111
+ raise ValueError(f"Unknown flag type: {flag_type}")
112
+
113
+ except grpc.RpcError as e:
114
+ code = e.code()
115
+ message = f"received grpc status code {code}"
116
+
117
+ if code == grpc.StatusCode.NOT_FOUND:
118
+ raise FlagNotFoundError(message) from e
119
+ elif code == grpc.StatusCode.INVALID_ARGUMENT:
120
+ raise TypeMismatchError(message) from e
121
+ elif code == grpc.StatusCode.DATA_LOSS:
122
+ raise ParseError(message) from e
123
+ raise GeneralError(message) from e
124
+
125
+ # Got a valid flag and valid type. Return it.
126
+ return FlagResolutionDetails(
127
+ value=response.value,
128
+ reason=response.reason,
129
+ variant=response.variant,
130
+ )
131
+
132
+ def _convert_context(
133
+ self, evaluation_context: typing.Optional[EvaluationContext]
134
+ ) -> Struct:
135
+ s = Struct()
136
+ if evaluation_context:
137
+ try:
138
+ s["targetingKey"] = evaluation_context.targeting_key
139
+ s.update(evaluation_context.attributes)
140
+ except ValueError as exc:
141
+ message = (
142
+ "could not serialize evaluation context to google.protobuf.Struct"
143
+ )
144
+ raise InvalidContextError(message) from exc
145
+ return s
@@ -0,0 +1,122 @@
1
+ import time
2
+ import typing
3
+
4
+ from json_logic import builtins, jsonLogic # type: ignore[import-untyped]
5
+
6
+ from openfeature.evaluation_context import EvaluationContext
7
+ from openfeature.exception import FlagNotFoundError, ParseError
8
+ from openfeature.flag_evaluation import FlagResolutionDetails, Reason
9
+ from openfeature.provider.provider import AbstractProvider
10
+
11
+ from ..config import Config
12
+ from .process.custom_ops import ends_with, fractional, sem_ver, starts_with
13
+ from .process.file_watcher import FileWatcherFlagStore
14
+
15
+ T = typing.TypeVar("T")
16
+
17
+
18
+ class InProcessResolver:
19
+ OPERATORS: typing.ClassVar[dict] = {
20
+ **builtins.BUILTINS,
21
+ "fractional": fractional,
22
+ "starts_with": starts_with,
23
+ "ends_with": ends_with,
24
+ "sem_ver": sem_ver,
25
+ }
26
+
27
+ def __init__(self, config: Config, provider: AbstractProvider):
28
+ self.config = config
29
+ self.provider = provider
30
+ if not self.config.offline_flag_source_path:
31
+ raise ValueError(
32
+ "offline_flag_source_path must be provided when using in-process resolver"
33
+ )
34
+ self.flag_store = FileWatcherFlagStore(
35
+ self.config.offline_flag_source_path,
36
+ self.provider,
37
+ self.config.offline_poll_interval_seconds,
38
+ )
39
+
40
+ def shutdown(self) -> None:
41
+ self.flag_store.shutdown()
42
+
43
+ def resolve_boolean_details(
44
+ self,
45
+ key: str,
46
+ default_value: bool,
47
+ evaluation_context: typing.Optional[EvaluationContext] = None,
48
+ ) -> FlagResolutionDetails[bool]:
49
+ return self._resolve(key, default_value, evaluation_context)
50
+
51
+ def resolve_string_details(
52
+ self,
53
+ key: str,
54
+ default_value: str,
55
+ evaluation_context: typing.Optional[EvaluationContext] = None,
56
+ ) -> FlagResolutionDetails[str]:
57
+ return self._resolve(key, default_value, evaluation_context)
58
+
59
+ def resolve_float_details(
60
+ self,
61
+ key: str,
62
+ default_value: float,
63
+ evaluation_context: typing.Optional[EvaluationContext] = None,
64
+ ) -> FlagResolutionDetails[float]:
65
+ return self._resolve(key, default_value, evaluation_context)
66
+
67
+ def resolve_integer_details(
68
+ self,
69
+ key: str,
70
+ default_value: int,
71
+ evaluation_context: typing.Optional[EvaluationContext] = None,
72
+ ) -> FlagResolutionDetails[int]:
73
+ return self._resolve(key, default_value, evaluation_context)
74
+
75
+ def resolve_object_details(
76
+ self,
77
+ key: str,
78
+ default_value: typing.Union[dict, list],
79
+ evaluation_context: typing.Optional[EvaluationContext] = None,
80
+ ) -> FlagResolutionDetails[typing.Union[dict, list]]:
81
+ return self._resolve(key, default_value, evaluation_context)
82
+
83
+ def _resolve(
84
+ self,
85
+ key: str,
86
+ default_value: T,
87
+ evaluation_context: typing.Optional[EvaluationContext] = None,
88
+ ) -> FlagResolutionDetails[T]:
89
+ flag = self.flag_store.get_flag(key)
90
+ if not flag:
91
+ raise FlagNotFoundError(f"Flag with key {key} not present in flag store.")
92
+
93
+ if flag.state == "DISABLED":
94
+ return FlagResolutionDetails(default_value, reason=Reason.DISABLED)
95
+
96
+ if not flag.targeting:
97
+ variant, value = flag.default
98
+ return FlagResolutionDetails(value, variant=variant, reason=Reason.STATIC)
99
+
100
+ json_logic_context = evaluation_context.attributes if evaluation_context else {}
101
+ json_logic_context["$flagd"] = {"flagKey": key, "timestamp": int(time.time())}
102
+ json_logic_context["targetingKey"] = (
103
+ evaluation_context.targeting_key if evaluation_context else None
104
+ )
105
+ variant = jsonLogic(flag.targeting, json_logic_context, self.OPERATORS)
106
+ if variant is None:
107
+ variant, value = flag.default
108
+ return FlagResolutionDetails(value, variant=variant, reason=Reason.DEFAULT)
109
+ if not isinstance(variant, (str, bool)):
110
+ raise ParseError(
111
+ "Parsed JSONLogic targeting did not return a string or bool"
112
+ )
113
+
114
+ variant, value = flag.get_variant(variant)
115
+ if not value:
116
+ raise ParseError(f"Resolved variant {variant} not in variants config.")
117
+
118
+ return FlagResolutionDetails(
119
+ value,
120
+ variant=variant,
121
+ reason=Reason.TARGETING_MATCH,
122
+ )