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.
- openfeature_provider_flagd-0.1.5/CHANGELOG.md +60 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/PKG-INFO +19 -2
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/README.md +13 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/pyproject.toml +14 -1
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/config.py +59 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/provider.py +136 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/__init__.py +51 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +145 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/in_process.py +122 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +126 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/process/file_watcher.py +89 -0
- openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/process/flags.py +51 -0
- openfeature_provider_flagd-0.1.5/test-harness/.gherkin-lintrc +57 -0
- openfeature_provider_flagd-0.1.5/test-harness/.git +1 -0
- openfeature_provider_flagd-0.1.5/test-harness/.github/workflows/ci.yml +59 -0
- openfeature_provider_flagd-0.1.5/test-harness/.github/workflows/lint-pr.yml +42 -0
- openfeature_provider_flagd-0.1.5/test-harness/.github/workflows/release-please.yml +93 -0
- openfeature_provider_flagd-0.1.5/test-harness/.release-please-manifest.json +3 -0
- openfeature_provider_flagd-0.1.5/test-harness/CHANGELOG.md +210 -0
- openfeature_provider_flagd-0.1.5/test-harness/Makefile +6 -0
- openfeature_provider_flagd-0.1.5/test-harness/README.md +41 -0
- openfeature_provider_flagd-0.1.5/test-harness/flagd/Dockerfile +17 -0
- openfeature_provider_flagd-0.1.5/test-harness/flagd/Dockerfile.unstable +12 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/changing-flag-bar.json +12 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/changing-flag-foo.json +12 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/custom-ops.json +167 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/edge-case-flags.json +57 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/evaluator-refs.json +52 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/testing-flags.json +156 -0
- openfeature_provider_flagd-0.1.5/test-harness/flags/zero-flags.json +36 -0
- openfeature_provider_flagd-0.1.5/test-harness/gherkin/flagd-json-evaluator.feature +117 -0
- openfeature_provider_flagd-0.1.5/test-harness/gherkin/flagd-reconnect.feature +15 -0
- openfeature_provider_flagd-0.1.5/test-harness/gherkin/flagd.feature +36 -0
- openfeature_provider_flagd-0.1.5/test-harness/package-lock.json +682 -0
- openfeature_provider_flagd-0.1.5/test-harness/package.json +8 -0
- openfeature_provider_flagd-0.1.5/test-harness/release-please-config.json +11 -0
- openfeature_provider_flagd-0.1.5/test-harness/renovate.json +15 -0
- openfeature_provider_flagd-0.1.5/test-harness/scripts/change-flag-wrapper.sh +22 -0
- openfeature_provider_flagd-0.1.5/test-harness/scripts/change-flag.sh +15 -0
- openfeature_provider_flagd-0.1.5/test-harness/scripts/restart-wrapper.sh +31 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/Dockerfile +22 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/Dockerfile.unstable +17 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/README.md +91 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/go.mod +20 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/go.sum +1138 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/main.go +13 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/pkg/config.go +50 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/pkg/file_watcher.go +150 -0
- openfeature_provider_flagd-0.1.5/test-harness/sync/pkg/server.go +136 -0
- openfeature_provider_flagd-0.1.5/tests/conftest.py +21 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/conftest.py +208 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/parsers.py +2 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_custom_ops.py +38 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_edge_cases.py +15 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_evaluator_reuse.py +13 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_events.py +91 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_testing_flags.py +24 -0
- openfeature_provider_flagd-0.1.5/tests/e2e/test_inprocess_zero_evals.py +28 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-default.json +13 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-state.json +13 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-targeting.json +15 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-broken-variants.json +15 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-disabled.json +13 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-invalid.not-json +13 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-no-state.json +12 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-wrong-structure.json +11 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag-wrong-variant.json +12 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag.json +13 -0
- openfeature_provider_flagd-0.1.5/tests/flags/basic-flag.yaml +8 -0
- openfeature_provider_flagd-0.1.5/tests/flags/invalid-fractional-args.json +16 -0
- openfeature_provider_flagd-0.1.5/tests/flags/invalid-fractional-weights.json +19 -0
- openfeature_provider_flagd-0.1.5/tests/flags/invalid-semver-args.json +16 -0
- openfeature_provider_flagd-0.1.5/tests/flags/invalid-semver-op.json +16 -0
- openfeature_provider_flagd-0.1.5/tests/flags/invalid-stringcomp-args.json +16 -0
- openfeature_provider_flagd-0.1.5/tests/test_config.py +29 -0
- openfeature_provider_flagd-0.1.5/tests/test_errors.py +79 -0
- openfeature_provider_flagd-0.1.5/tests/test_file_store.py +33 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/tests/test_flagd.py +8 -0
- openfeature_provider_flagd-0.1.3/CHANGELOG.md +0 -28
- openfeature_provider_flagd-0.1.3/src/openfeature/contrib/provider/flagd/defaults.py +0 -5
- openfeature_provider_flagd-0.1.3/src/openfeature/contrib/provider/flagd/provider.py +0 -202
- openfeature_provider_flagd-0.1.3/tests/conftest.py +0 -10
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/.gitignore +0 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/LICENSE +0 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/scripts/gen_protos.sh +0 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
- {openfeature_provider_flagd-0.1.3 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: openfeature-provider-flagd
|
|
3
|
-
Version: 0.1.
|
|
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.
|
|
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
|
openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/in_process.py
ADDED
|
@@ -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
|
+
)
|