openfeature-provider-flagd 0.1.5__py3-none-any.whl → 0.2.0__py3-none-any.whl

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 (43) hide show
  1. openfeature/.gitignore +2 -0
  2. openfeature/contrib/provider/flagd/config.py +193 -23
  3. openfeature/contrib/provider/flagd/provider.py +62 -12
  4. openfeature/contrib/provider/flagd/resolvers/__init__.py +1 -47
  5. openfeature/contrib/provider/flagd/resolvers/grpc.py +226 -17
  6. openfeature/contrib/provider/flagd/resolvers/in_process.py +40 -31
  7. openfeature/contrib/provider/flagd/resolvers/process/connector/__init__.py +11 -0
  8. openfeature/contrib/provider/flagd/resolvers/process/connector/file_watcher.py +106 -0
  9. openfeature/contrib/provider/flagd/resolvers/process/connector/grpc_watcher.py +192 -0
  10. openfeature/contrib/provider/flagd/resolvers/process/custom_ops.py +58 -19
  11. openfeature/contrib/provider/flagd/resolvers/process/flags.py +50 -6
  12. openfeature/contrib/provider/flagd/resolvers/process/targeting.py +35 -0
  13. openfeature/contrib/provider/flagd/resolvers/protocol.py +47 -0
  14. openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.py +72 -0
  15. openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2.pyi +450 -0
  16. openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.py +358 -0
  17. openfeature/schemas/protobuf/flagd/evaluation/v1/evaluation_pb2_grpc.pyi +155 -0
  18. openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.py +50 -0
  19. openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2.pyi +148 -0
  20. openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.py +186 -0
  21. openfeature/schemas/protobuf/flagd/sync/v1/sync_pb2_grpc.pyi +86 -0
  22. openfeature/schemas/protobuf/schema/v1/schema_pb2.py +72 -0
  23. openfeature/schemas/protobuf/schema/v1/schema_pb2.pyi +451 -0
  24. openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.py +358 -0
  25. openfeature/schemas/protobuf/schema/v1/schema_pb2_grpc.pyi +156 -0
  26. openfeature/schemas/protobuf/sync/v1/sync_service_pb2.py +47 -0
  27. openfeature/schemas/protobuf/sync/v1/sync_service_pb2.pyi +174 -0
  28. openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.py +143 -0
  29. openfeature/schemas/protobuf/sync/v1/sync_service_pb2_grpc.pyi +70 -0
  30. {openfeature_provider_flagd-0.1.5.dist-info → openfeature_provider_flagd-0.2.0.dist-info}/METADATA +116 -15
  31. openfeature_provider_flagd-0.2.0.dist-info/RECORD +35 -0
  32. {openfeature_provider_flagd-0.1.5.dist-info → openfeature_provider_flagd-0.2.0.dist-info}/WHEEL +1 -1
  33. {openfeature_provider_flagd-0.1.5.dist-info → openfeature_provider_flagd-0.2.0.dist-info}/licenses/LICENSE +1 -1
  34. openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2.py +0 -62
  35. openfeature/contrib/provider/flagd/proto/flagd/evaluation/v1/evaluation_pb2_grpc.py +0 -267
  36. openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2.py +0 -40
  37. openfeature/contrib/provider/flagd/proto/flagd/sync/v1/sync_pb2_grpc.py +0 -135
  38. openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2.py +0 -62
  39. openfeature/contrib/provider/flagd/proto/schema/v1/schema_pb2_grpc.py +0 -267
  40. openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2.py +0 -37
  41. openfeature/contrib/provider/flagd/proto/sync/v1/sync_service_pb2_grpc.py +0 -102
  42. openfeature/contrib/provider/flagd/resolvers/process/file_watcher.py +0 -89
  43. openfeature_provider_flagd-0.1.5.dist-info/RECORD +0 -22
openfeature/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+
2
+ schemas
@@ -1,7 +1,54 @@
1
+ import dataclasses
1
2
  import os
2
3
  import typing
3
4
  from enum import Enum
4
5
 
6
+
7
+ class ResolverType(Enum):
8
+ RPC = "rpc"
9
+ IN_PROCESS = "in-process"
10
+ FILE = "file"
11
+
12
+
13
+ class CacheType(Enum):
14
+ LRU = "lru"
15
+ DISABLED = "disabled"
16
+
17
+
18
+ DEFAULT_CACHE = CacheType.LRU
19
+ DEFAULT_CACHE_SIZE = 1000
20
+ DEFAULT_DEADLINE = 500
21
+ DEFAULT_HOST = "localhost"
22
+ DEFAULT_KEEP_ALIVE = 0
23
+ DEFAULT_OFFLINE_SOURCE_PATH: typing.Optional[str] = None
24
+ DEFAULT_OFFLINE_POLL_MS = 5000
25
+ DEFAULT_PORT_IN_PROCESS = 8015
26
+ DEFAULT_PORT_RPC = 8013
27
+ DEFAULT_RESOLVER_TYPE = ResolverType.RPC
28
+ DEFAULT_RETRY_BACKOFF = 1000
29
+ DEFAULT_RETRY_BACKOFF_MAX = 120000
30
+ DEFAULT_RETRY_GRACE_PERIOD_SECONDS = 5
31
+ DEFAULT_STREAM_DEADLINE = 600000
32
+ DEFAULT_TLS = False
33
+ DEFAULT_TLS_CERT: typing.Optional[str] = None
34
+
35
+ ENV_VAR_CACHE_SIZE = "FLAGD_MAX_CACHE_SIZE"
36
+ ENV_VAR_CACHE_TYPE = "FLAGD_CACHE"
37
+ ENV_VAR_DEADLINE_MS = "FLAGD_DEADLINE_MS"
38
+ ENV_VAR_HOST = "FLAGD_HOST"
39
+ ENV_VAR_KEEP_ALIVE_TIME_MS = "FLAGD_KEEP_ALIVE_TIME_MS"
40
+ ENV_VAR_OFFLINE_FLAG_SOURCE_PATH = "FLAGD_OFFLINE_FLAG_SOURCE_PATH"
41
+ ENV_VAR_OFFLINE_POLL_MS = "FLAGD_OFFLINE_POLL_MS"
42
+ ENV_VAR_PORT = "FLAGD_PORT"
43
+ ENV_VAR_RESOLVER_TYPE = "FLAGD_RESOLVER"
44
+ ENV_VAR_RETRY_BACKOFF_MS = "FLAGD_RETRY_BACKOFF_MS"
45
+ ENV_VAR_RETRY_BACKOFF_MAX_MS = "FLAGD_RETRY_BACKOFF_MAX_MS"
46
+ ENV_VAR_RETRY_GRACE_PERIOD_SECONDS = "FLAGD_RETRY_GRACE_PERIOD"
47
+ ENV_VAR_SELECTOR = "FLAGD_SOURCE_SELECTOR"
48
+ ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS"
49
+ ENV_VAR_TLS = "FLAGD_TLS"
50
+ ENV_VAR_TLS_CERT = "FLAGD_SERVER_CERT_PATH"
51
+
5
52
  T = typing.TypeVar("T")
6
53
 
7
54
 
@@ -9,6 +56,14 @@ def str_to_bool(val: str) -> bool:
9
56
  return val.lower() == "true"
10
57
 
11
58
 
59
+ def convert_resolver_type(val: typing.Union[str, ResolverType]) -> ResolverType:
60
+ if isinstance(val, str):
61
+ v = val.lower()
62
+ return ResolverType(v)
63
+ else:
64
+ return ResolverType(val)
65
+
66
+
12
67
  def env_or_default(
13
68
  env_var: str, default: T, cast: typing.Optional[typing.Callable[[str], T]] = None
14
69
  ) -> typing.Union[str, T]:
@@ -18,42 +73,157 @@ def env_or_default(
18
73
  return val if cast is None else cast(val)
19
74
 
20
75
 
21
- class ResolverType(Enum):
22
- GRPC = "grpc"
23
- IN_PROCESS = "in-process"
24
-
25
-
76
+ @dataclasses.dataclass
26
77
  class Config:
27
78
  def __init__( # noqa: PLR0913
28
79
  self,
29
80
  host: typing.Optional[str] = None,
30
81
  port: typing.Optional[int] = None,
31
82
  tls: typing.Optional[bool] = None,
32
- timeout: typing.Optional[int] = None,
33
- resolver_type: typing.Optional[ResolverType] = None,
83
+ selector: typing.Optional[str] = None,
84
+ resolver: typing.Optional[ResolverType] = None,
34
85
  offline_flag_source_path: typing.Optional[str] = None,
35
- offline_poll_interval_seconds: typing.Optional[float] = None,
86
+ offline_poll_interval_ms: typing.Optional[int] = None,
87
+ retry_backoff_ms: typing.Optional[int] = None,
88
+ retry_backoff_max_ms: typing.Optional[int] = None,
89
+ retry_grace_period: typing.Optional[int] = None,
90
+ deadline_ms: typing.Optional[int] = None,
91
+ stream_deadline_ms: typing.Optional[int] = None,
92
+ keep_alive_time: typing.Optional[int] = None,
93
+ cache: typing.Optional[CacheType] = None,
94
+ max_cache_size: typing.Optional[int] = None,
95
+ cert_path: typing.Optional[str] = None,
36
96
  ):
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
- )
97
+ self.host = env_or_default(ENV_VAR_HOST, DEFAULT_HOST) if host is None else host
98
+
41
99
  self.tls = (
42
- env_or_default("FLAGD_TLS", False, cast=str_to_bool) if tls is None else tls
100
+ env_or_default(ENV_VAR_TLS, DEFAULT_TLS, cast=str_to_bool)
101
+ if tls is None
102
+ else tls
43
103
  )
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
104
+
105
+ self.retry_backoff_ms: int = (
106
+ int(
107
+ env_or_default(
108
+ ENV_VAR_RETRY_BACKOFF_MS, DEFAULT_RETRY_BACKOFF, cast=int
109
+ )
110
+ )
111
+ if retry_backoff_ms is None
112
+ else retry_backoff_ms
113
+ )
114
+ self.retry_backoff_max_ms: int = (
115
+ int(
116
+ env_or_default(
117
+ ENV_VAR_RETRY_BACKOFF_MAX_MS, DEFAULT_RETRY_BACKOFF_MAX, cast=int
118
+ )
119
+ )
120
+ if retry_backoff_max_ms is None
121
+ else retry_backoff_max_ms
122
+ )
123
+
124
+ self.retry_grace_period: int = (
125
+ int(
126
+ env_or_default(
127
+ ENV_VAR_RETRY_GRACE_PERIOD_SECONDS,
128
+ DEFAULT_RETRY_GRACE_PERIOD_SECONDS,
129
+ cast=int,
130
+ )
131
+ )
132
+ if retry_grace_period is None
133
+ else retry_grace_period
134
+ )
135
+
136
+ self.resolver = (
137
+ env_or_default(
138
+ ENV_VAR_RESOLVER_TYPE, DEFAULT_RESOLVER_TYPE, cast=convert_resolver_type
139
+ )
140
+ if resolver is None
141
+ else resolver
49
142
  )
143
+
144
+ default_port = (
145
+ DEFAULT_PORT_RPC
146
+ if self.resolver is ResolverType.RPC
147
+ else DEFAULT_PORT_IN_PROCESS
148
+ )
149
+
150
+ self.port: int = (
151
+ int(env_or_default(ENV_VAR_PORT, default_port, cast=int))
152
+ if port is None
153
+ else port
154
+ )
155
+
50
156
  self.offline_flag_source_path = (
51
- env_or_default("FLAGD_OFFLINE_FLAG_SOURCE_PATH", None)
157
+ env_or_default(
158
+ ENV_VAR_OFFLINE_FLAG_SOURCE_PATH, DEFAULT_OFFLINE_SOURCE_PATH
159
+ )
52
160
  if offline_flag_source_path is None
53
161
  else offline_flag_source_path
54
162
  )
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
163
+
164
+ if (
165
+ self.offline_flag_source_path is not None
166
+ and self.resolver is ResolverType.IN_PROCESS
167
+ ):
168
+ self.resolver = ResolverType.FILE
169
+
170
+ if self.resolver is ResolverType.FILE and self.offline_flag_source_path is None:
171
+ raise AttributeError(
172
+ "Resolver Type 'FILE' requires a offlineFlagSourcePath"
173
+ )
174
+
175
+ self.offline_poll_interval_ms: int = (
176
+ int(
177
+ env_or_default(
178
+ ENV_VAR_OFFLINE_POLL_MS, DEFAULT_OFFLINE_POLL_MS, cast=int
179
+ )
180
+ )
181
+ if offline_poll_interval_ms is None
182
+ else offline_poll_interval_ms
183
+ )
184
+
185
+ self.deadline_ms: int = (
186
+ int(env_or_default(ENV_VAR_DEADLINE_MS, DEFAULT_DEADLINE, cast=int))
187
+ if deadline_ms is None
188
+ else deadline_ms
189
+ )
190
+
191
+ self.stream_deadline_ms: int = (
192
+ int(
193
+ env_or_default(
194
+ ENV_VAR_STREAM_DEADLINE_MS, DEFAULT_STREAM_DEADLINE, cast=int
195
+ )
196
+ )
197
+ if stream_deadline_ms is None
198
+ else stream_deadline_ms
199
+ )
200
+
201
+ self.keep_alive_time: int = (
202
+ int(
203
+ env_or_default(ENV_VAR_KEEP_ALIVE_TIME_MS, DEFAULT_KEEP_ALIVE, cast=int)
204
+ )
205
+ if keep_alive_time is None
206
+ else keep_alive_time
207
+ )
208
+
209
+ self.cache = (
210
+ CacheType(env_or_default(ENV_VAR_CACHE_TYPE, DEFAULT_CACHE))
211
+ if cache is None
212
+ else cache
213
+ )
214
+
215
+ self.max_cache_size: int = (
216
+ int(env_or_default(ENV_VAR_CACHE_SIZE, DEFAULT_CACHE_SIZE, cast=int))
217
+ if max_cache_size is None
218
+ else max_cache_size
219
+ )
220
+
221
+ self.cert_path = (
222
+ env_or_default(ENV_VAR_TLS_CERT, DEFAULT_TLS_CERT)
223
+ if cert_path is None
224
+ else cert_path
225
+ )
226
+
227
+ self.selector = (
228
+ env_or_default(ENV_VAR_SELECTOR, None) if selector is None else selector
59
229
  )
@@ -22,13 +22,14 @@
22
22
  """
23
23
 
24
24
  import typing
25
+ import warnings
25
26
 
26
27
  from openfeature.evaluation_context import EvaluationContext
27
28
  from openfeature.flag_evaluation import FlagResolutionDetails
29
+ from openfeature.provider import AbstractProvider
28
30
  from openfeature.provider.metadata import Metadata
29
- from openfeature.provider.provider import AbstractProvider
30
31
 
31
- from .config import Config, ResolverType
32
+ from .config import CacheType, Config, ResolverType
32
33
  from .resolvers import AbstractResolver, GrpcResolver, InProcessResolver
33
34
 
34
35
  T = typing.TypeVar("T")
@@ -42,10 +43,19 @@ class FlagdProvider(AbstractProvider):
42
43
  host: typing.Optional[str] = None,
43
44
  port: typing.Optional[int] = None,
44
45
  tls: typing.Optional[bool] = None,
46
+ deadline_ms: typing.Optional[int] = None,
45
47
  timeout: typing.Optional[int] = None,
48
+ retry_backoff_ms: typing.Optional[int] = None,
49
+ selector: typing.Optional[str] = None,
46
50
  resolver_type: typing.Optional[ResolverType] = None,
47
51
  offline_flag_source_path: typing.Optional[str] = None,
48
- offline_poll_interval_seconds: typing.Optional[float] = None,
52
+ stream_deadline_ms: typing.Optional[int] = None,
53
+ keep_alive_time: typing.Optional[int] = None,
54
+ cache: typing.Optional[CacheType] = None,
55
+ max_cache_size: typing.Optional[int] = None,
56
+ retry_backoff_max_ms: typing.Optional[int] = None,
57
+ retry_grace_period: typing.Optional[int] = None,
58
+ cert_path: typing.Optional[str] = None,
49
59
  ):
50
60
  """
51
61
  Create an instance of the FlagdProvider
@@ -53,30 +63,70 @@ class FlagdProvider(AbstractProvider):
53
63
  :param host: the host to make requests to
54
64
  :param port: the port the flagd service is available on
55
65
  :param tls: enable/disable secure TLS connectivity
56
- :param timeout: the maximum to wait before a request times out
66
+ :param deadline_ms: the maximum to wait before a request times out
67
+ :param timeout: the maximum time to wait before a request times out
68
+ :param retry_backoff_ms: the number of milliseconds to backoff
69
+ :param offline_flag_source_path: the path to the flag source file
70
+ :param stream_deadline_ms: the maximum time to wait before a request times out
71
+ :param keep_alive_time: the number of milliseconds to keep alive
72
+ :param resolver_type: the type of resolver to use
57
73
  """
74
+ if deadline_ms is None and timeout is not None:
75
+ deadline_ms = timeout * 1000
76
+ warnings.warn(
77
+ "'timeout' property is deprecated, please use 'deadline' instead, be aware that 'deadline' is in milliseconds",
78
+ DeprecationWarning,
79
+ stacklevel=2,
80
+ )
81
+
58
82
  self.config = Config(
59
83
  host=host,
60
84
  port=port,
61
85
  tls=tls,
62
- timeout=timeout,
63
- resolver_type=resolver_type,
86
+ deadline_ms=deadline_ms,
87
+ retry_backoff_ms=retry_backoff_ms,
88
+ retry_backoff_max_ms=retry_backoff_max_ms,
89
+ retry_grace_period=retry_grace_period,
90
+ selector=selector,
91
+ resolver=resolver_type,
64
92
  offline_flag_source_path=offline_flag_source_path,
65
- offline_poll_interval_seconds=offline_poll_interval_seconds,
93
+ stream_deadline_ms=stream_deadline_ms,
94
+ keep_alive_time=keep_alive_time,
95
+ cache=cache,
96
+ max_cache_size=max_cache_size,
97
+ cert_path=cert_path,
66
98
  )
67
99
 
68
100
  self.resolver = self.setup_resolver()
69
101
 
70
102
  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)
103
+ if self.config.resolver == ResolverType.RPC:
104
+ return GrpcResolver(
105
+ self.config,
106
+ self.emit_provider_ready,
107
+ self.emit_provider_error,
108
+ self.emit_provider_stale,
109
+ self.emit_provider_configuration_changed,
110
+ )
111
+ elif (
112
+ self.config.resolver == ResolverType.IN_PROCESS
113
+ or self.config.resolver == ResolverType.FILE
114
+ ):
115
+ return InProcessResolver(
116
+ self.config,
117
+ self.emit_provider_ready,
118
+ self.emit_provider_error,
119
+ self.emit_provider_stale,
120
+ self.emit_provider_configuration_changed,
121
+ )
75
122
  else:
76
123
  raise ValueError(
77
- f"`resolver_type` parameter invalid: {self.config.resolver_type}"
124
+ f"`resolver_type` parameter invalid: {self.config.resolver}"
78
125
  )
79
126
 
127
+ def initialize(self, evaluation_context: EvaluationContext) -> None:
128
+ self.resolver.initialize(evaluation_context)
129
+
80
130
  def shutdown(self) -> None:
81
131
  if self.resolver:
82
132
  self.resolver.shutdown()
@@ -1,51 +1,5 @@
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
1
  from .grpc import GrpcResolver
9
2
  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
-
3
+ from .protocol import AbstractResolver
50
4
 
51
5
  __all__ = ["AbstractResolver", "GrpcResolver", "InProcessResolver"]