openfeature-provider-flagd 0.1.4__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.4 → openfeature_provider_flagd-0.1.5}/CHANGELOG.md +7 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/PKG-INFO +18 -1
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/README.md +13 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/pyproject.toml +8 -1
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/config.py +25 -1
- 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.4/src/openfeature/contrib/provider/flagd/provider.py → openfeature_provider_flagd-0.1.5/src/openfeature/contrib/provider/flagd/resolvers/grpc.py +8 -57
- 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_errors.py +79 -0
- openfeature_provider_flagd-0.1.5/tests/test_file_store.py +33 -0
- openfeature_provider_flagd-0.1.4/tests/conftest.py +0 -10
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/.gitignore +0 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/LICENSE +0 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/scripts/gen_protos.sh +0 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/__init__.py +0 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/flag_type.py +0 -0
- {openfeature_provider_flagd-0.1.4 → 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.4 → 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.4 → 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.4 → 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.4 → openfeature_provider_flagd-0.1.5}/src/openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2.py +0 -0
- {openfeature_provider_flagd-0.1.4 → 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.4 → 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.4 → 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.4 → openfeature_provider_flagd-0.1.5}/tests/__init__.py +0 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/tests/test_config.py +0 -0
- {openfeature_provider_flagd-0.1.4 → openfeature_provider_flagd-0.1.5}/tests/test_flagd.py +0 -0
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
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
|
+
|
|
3
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)
|
|
4
11
|
|
|
5
12
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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",
|
|
@@ -61,4 +67,5 @@ packages = ["src/openfeature"]
|
|
|
61
67
|
omit = [
|
|
62
68
|
# exclude generated files
|
|
63
69
|
"src/openfeature/contrib/provider/flagd/proto/*",
|
|
70
|
+
"tests/**",
|
|
64
71
|
]
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import typing
|
|
3
|
+
from enum import Enum
|
|
3
4
|
|
|
4
5
|
T = typing.TypeVar("T")
|
|
5
6
|
|
|
@@ -17,13 +18,21 @@ def env_or_default(
|
|
|
17
18
|
return val if cast is None else cast(val)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
class ResolverType(Enum):
|
|
22
|
+
GRPC = "grpc"
|
|
23
|
+
IN_PROCESS = "in-process"
|
|
24
|
+
|
|
25
|
+
|
|
20
26
|
class Config:
|
|
21
|
-
def __init__(
|
|
27
|
+
def __init__( # noqa: PLR0913
|
|
22
28
|
self,
|
|
23
29
|
host: typing.Optional[str] = None,
|
|
24
30
|
port: typing.Optional[int] = None,
|
|
25
31
|
tls: typing.Optional[bool] = None,
|
|
26
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,
|
|
27
36
|
):
|
|
28
37
|
self.host = env_or_default("FLAGD_HOST", "localhost") if host is None else host
|
|
29
38
|
self.port = (
|
|
@@ -33,3 +42,18 @@ class Config:
|
|
|
33
42
|
env_or_default("FLAGD_TLS", False, cast=str_to_bool) if tls is None else tls
|
|
34
43
|
)
|
|
35
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"]
|
|
@@ -1,26 +1,3 @@
|
|
|
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
1
|
import typing
|
|
25
2
|
|
|
26
3
|
import grpc
|
|
@@ -35,52 +12,26 @@ from openfeature.exception import (
|
|
|
35
12
|
TypeMismatchError,
|
|
36
13
|
)
|
|
37
14
|
from openfeature.flag_evaluation import FlagResolutionDetails
|
|
38
|
-
from openfeature.provider.metadata import Metadata
|
|
39
|
-
from openfeature.provider.provider import AbstractProvider
|
|
40
15
|
|
|
41
|
-
from
|
|
42
|
-
from
|
|
43
|
-
from
|
|
16
|
+
from ..config import Config
|
|
17
|
+
from ..flag_type import FlagType
|
|
18
|
+
from ..proto.schema.v1 import schema_pb2, schema_pb2_grpc
|
|
44
19
|
|
|
45
20
|
T = typing.TypeVar("T")
|
|
46
21
|
|
|
47
22
|
|
|
48
|
-
class
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
host: typing.Optional[str] = None,
|
|
54
|
-
port: typing.Optional[int] = None,
|
|
55
|
-
tls: typing.Optional[bool] = None,
|
|
56
|
-
timeout: typing.Optional[int] = None,
|
|
57
|
-
):
|
|
58
|
-
"""
|
|
59
|
-
Create an instance of the FlagdProvider
|
|
60
|
-
|
|
61
|
-
:param host: the host to make requests to
|
|
62
|
-
:param port: the port the flagd service is available on
|
|
63
|
-
:param tls: enable/disable secure TLS connectivity
|
|
64
|
-
:param timeout: the maximum to wait before a request times out
|
|
65
|
-
"""
|
|
66
|
-
self.config = Config(
|
|
67
|
-
host=host,
|
|
68
|
-
port=port,
|
|
69
|
-
tls=tls,
|
|
70
|
-
timeout=timeout,
|
|
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
|
|
71
28
|
)
|
|
72
|
-
|
|
73
|
-
channel_factory = grpc.secure_channel if tls else grpc.insecure_channel
|
|
74
29
|
self.channel = channel_factory(f"{self.config.host}:{self.config.port}")
|
|
75
30
|
self.stub = schema_pb2_grpc.ServiceStub(self.channel)
|
|
76
31
|
|
|
77
32
|
def shutdown(self) -> None:
|
|
78
33
|
self.channel.close()
|
|
79
34
|
|
|
80
|
-
def get_metadata(self) -> Metadata:
|
|
81
|
-
"""Returns provider metadata"""
|
|
82
|
-
return Metadata(name="FlagdProvider")
|
|
83
|
-
|
|
84
35
|
def resolve_boolean_details(
|
|
85
36
|
self,
|
|
86
37
|
key: str,
|
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
|
+
)
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import typing
|
|
3
|
+
|
|
4
|
+
import mmh3
|
|
5
|
+
import semver
|
|
6
|
+
|
|
7
|
+
JsonPrimitive = typing.Union[str, bool, float, int]
|
|
8
|
+
JsonLogicArg = typing.Union[JsonPrimitive, typing.Sequence[JsonPrimitive]]
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger("openfeature.contrib")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def fractional(data: dict, *args: JsonLogicArg) -> typing.Optional[str]:
|
|
14
|
+
if not args:
|
|
15
|
+
logger.error("No arguments provided to fractional operator.")
|
|
16
|
+
return None
|
|
17
|
+
|
|
18
|
+
bucket_by = None
|
|
19
|
+
if isinstance(args[0], str):
|
|
20
|
+
bucket_by = args[0]
|
|
21
|
+
args = args[1:]
|
|
22
|
+
else:
|
|
23
|
+
seed = data.get("$flagd", {}).get("flagKey", "")
|
|
24
|
+
targeting_key = data.get("targetingKey")
|
|
25
|
+
if not targeting_key:
|
|
26
|
+
logger.error("No targetingKey provided for fractional shorthand syntax.")
|
|
27
|
+
return None
|
|
28
|
+
bucket_by = seed + targeting_key
|
|
29
|
+
|
|
30
|
+
if not bucket_by:
|
|
31
|
+
logger.error("No hashKey value resolved")
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
hash_ratio = abs(mmh3.hash(bucket_by)) / (2**31 - 1)
|
|
35
|
+
bucket = int(hash_ratio * 100)
|
|
36
|
+
|
|
37
|
+
for arg in args:
|
|
38
|
+
if (
|
|
39
|
+
not isinstance(arg, (tuple, list))
|
|
40
|
+
or len(arg) != 2
|
|
41
|
+
or not isinstance(arg[0], str)
|
|
42
|
+
or not isinstance(arg[1], int)
|
|
43
|
+
):
|
|
44
|
+
logger.error("Fractional variant weights must be (str, int) tuple")
|
|
45
|
+
return None
|
|
46
|
+
variant_weights: typing.Tuple[typing.Tuple[str, int]] = args # type: ignore[assignment]
|
|
47
|
+
|
|
48
|
+
range_end = 0
|
|
49
|
+
for variant, weight in variant_weights:
|
|
50
|
+
range_end += weight
|
|
51
|
+
if bucket < range_end:
|
|
52
|
+
return variant
|
|
53
|
+
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def starts_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
|
|
58
|
+
def f(s1: str, s2: str) -> bool:
|
|
59
|
+
return s1.startswith(s2)
|
|
60
|
+
|
|
61
|
+
return string_comp(f, data, *args)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def ends_with(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]:
|
|
65
|
+
def f(s1: str, s2: str) -> bool:
|
|
66
|
+
return s1.endswith(s2)
|
|
67
|
+
|
|
68
|
+
return string_comp(f, data, *args)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def string_comp(
|
|
72
|
+
comparator: typing.Callable[[str, str], bool], data: dict, *args: JsonLogicArg
|
|
73
|
+
) -> typing.Optional[bool]:
|
|
74
|
+
if not args:
|
|
75
|
+
logger.error("No arguments provided to string_comp operator.")
|
|
76
|
+
return None
|
|
77
|
+
if len(args) != 2:
|
|
78
|
+
logger.error("Exactly 2 args expected for string_comp operator.")
|
|
79
|
+
return None
|
|
80
|
+
arg1, arg2 = args
|
|
81
|
+
if not isinstance(arg1, str):
|
|
82
|
+
logger.debug(f"incorrect argument for first argument, expected string: {arg1}")
|
|
83
|
+
return False
|
|
84
|
+
if not isinstance(arg2, str):
|
|
85
|
+
logger.debug(f"incorrect argument for second argument, expected string: {arg2}")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return comparator(arg1, arg2)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def sem_ver(data: dict, *args: JsonLogicArg) -> typing.Optional[bool]: # noqa: C901
|
|
92
|
+
if not args:
|
|
93
|
+
logger.error("No arguments provided to sem_ver operator.")
|
|
94
|
+
return None
|
|
95
|
+
if len(args) != 3:
|
|
96
|
+
logger.error("Exactly 3 args expected for sem_ver operator.")
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
arg1, op, arg2 = args
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
v1 = semver.Version.parse(str(arg1))
|
|
103
|
+
v2 = semver.Version.parse(str(arg2))
|
|
104
|
+
except ValueError as e:
|
|
105
|
+
logger.exception(e)
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
if op == "=":
|
|
109
|
+
return v1 == v2
|
|
110
|
+
elif op == "!=":
|
|
111
|
+
return v1 != v2
|
|
112
|
+
elif op == "<":
|
|
113
|
+
return v1 < v2
|
|
114
|
+
elif op == "<=":
|
|
115
|
+
return v1 <= v2
|
|
116
|
+
elif op == ">":
|
|
117
|
+
return v1 > v2
|
|
118
|
+
elif op == ">=":
|
|
119
|
+
return v1 >= v2
|
|
120
|
+
elif op == "^":
|
|
121
|
+
return v1.major == v2.major
|
|
122
|
+
elif op == "~":
|
|
123
|
+
return v1.major == v2.major and v1.minor == v2.minor
|
|
124
|
+
else:
|
|
125
|
+
logger.error(f"Op not supported by sem_ver: {op}")
|
|
126
|
+
return None
|