Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: correct configuration params, stream reconnects, add tests #104

Draft
wants to merge 44 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
3477153
build(flagd): auto generate proto files from schema
aepfli Nov 22, 2024
db81de1
fix(flagd): adding events for rpc mode
aepfli Nov 21, 2024
8cafa56
feat: add grpc sync flag store
colebaileygit Apr 27, 2024
a0df24f
fix: float/int type casting
colebaileygit Apr 27, 2024
373b343
add: test cases and reconnect functionality
colebaileygit May 1, 2024
73dbce3
fix: pyproject settings
colebaileygit May 1, 2024
9f3e15a
add: thread shutdown and cleaner flag store / connector logic
colebaileygit May 1, 2024
6f9b093
add: selector and sync init timeout
colebaileygit May 1, 2024
1033fe6
fix: noisy test due to insufficient wait time
colebaileygit May 1, 2024
4243ec7
docs: configuration options in readme
colebaileygit May 1, 2024
8b02e10
tests: edge cases for grpc response parsing
colebaileygit May 1, 2024
dc3c8dd
Add env var configs to readme
colebaileygit May 3, 2024
93b0269
fix: improve naming of emit flag
colebaileygit May 4, 2024
08d8172
docs: update CONTRIBUTING details
colebaileygit May 4, 2024
794e35f
add: worker thread names
colebaileygit Jun 20, 2024
bff53a9
fix: remove hasattr usage
colebaileygit Jun 20, 2024
f775b99
fix: circular dependency
colebaileygit Jun 20, 2024
fb110bb
fix: AbstractProvider dependency everywhere
colebaileygit Jun 20, 2024
914d9e0
Merge branch 'main' into feat/autogenerate_proto_files
aepfli Nov 22, 2024
6bf2b3c
feat(flagd-rpc): add caching
aepfli Nov 22, 2024
e8bb3f2
Merge branch 'main' into feat/caching
aepfli Nov 23, 2024
f01d6e5
feat(flagd-rpc): add caching
aepfli Nov 22, 2024
af0df41
fixup: adding gherkin tests for evaluations, and fxing found issues
aepfli Nov 17, 2024
c7f81b6
Merge branch 'feat/caching' into feat/grpc-sync-addition
aepfli Nov 23, 2024
07a38e6
fixup: improve readme and use all the settings everywhere
aepfli Nov 23, 2024
4b74a53
fixup: changing to mypy-protobuf
aepfli Nov 24, 2024
87c50a4
Merge branch 'feat/autogenerate_proto_files' into feat/grpc-sync-addi…
aepfli Nov 25, 2024
6cff3d4
fixup: grpc -> rpc
toddbaert Nov 25, 2024
228008d
Merge branch 'main' into feat/grpc-sync-addition
toddbaert Nov 25, 2024
cd6b35b
fixup: comment
toddbaert Nov 25, 2024
9e10577
chore: comment lol
toddbaert Nov 25, 2024
4ba24f0
fixup: 3rd try...
toddbaert Nov 25, 2024
f91dd5c
fixup: recreate channel on any grpc err
toddbaert Nov 25, 2024
266d2f1
feat(flagd-rpc): add caching with tests
aepfli Nov 28, 2024
755f04c
fixup: using new test-harness
aepfli Dec 2, 2024
3c0e9cc
fixup(flagd): remove merge conflict error as stated by warber
aepfli Dec 4, 2024
c05b07d
Merge branch 'feat/caching' into feat/grpc-sync-addition
aepfli Dec 6, 2024
51d7651
feat(flagd-rpc): add caching with tests
aepfli Nov 28, 2024
07452ea
fixup: using new test-harness
aepfli Dec 2, 2024
d15f529
fixup(flagd): remove merge conflict error as stated by warber
aepfli Dec 4, 2024
c74d6ad
feat(flagd): add graceful attempts
aepfli Dec 6, 2024
518572a
feat(flagd): add graceful attempts
aepfli Dec 6, 2024
a37e5a3
Merge branch 'feat/grace_attempts' into feat/grpc-sync-addition
aepfli Dec 6, 2024
77b9eb8
Merge remote-tracking branch 'upstream/main' into feat/grpc-sync-addi…
aepfli Dec 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,13 @@ We use `pytest` for our unit testing, making use of `parametrized` to inject cas

### Integration tests

These are planned once the SDK has been stabilized and a Flagd provider implemented. At that point, we will utilize the [gherkin integration tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) to validate against a live, seeded Flagd instance.
The Flagd provider utilizes the [gherkin integration tests](https://github.com/open-feature/test-harness/blob/main/features/evaluation.feature) to validate against a live, seeded Flagd instance.

To run the integration tests you need to have a container runtime, like docker, ranger, etc. installed.

```bash
hatch run test
```

### Type checking

Expand All @@ -52,6 +58,13 @@ Navigate to the repository folder
cd python-sdk-contrib
```

Checkout submodules

```bash
git submodule update --init --recursive
```


Add your fork as an origin

```bash
Expand All @@ -62,15 +75,16 @@ Ensure your development environment is all set up by building and testing

```bash
cd <package>
hatch run test
hatch build
hatch test
```

To start working on a new feature or bugfix, create a new branch and start working on it.

```bash
git checkout -b feat/NAME_OF_FEATURE
# Make your changes
git commit
git commit -s -m "feat: my feature"
git push fork feat/NAME_OF_FEATURE
```

Expand Down
36 changes: 34 additions & 2 deletions providers/openfeature-provider-flagd/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# flagd Provider for OpenFeature

This provider is designed to use flagd's [evaluation protocol](https://github.com/open-feature/schemas/blob/main/protobuf/schema/v1/schema.proto).
This provider is designed to use
flagd's [evaluation protocol](https://github.com/open-feature/schemas/blob/main/protobuf/schema/v1/schema.proto).

## Installation

Expand Down Expand Up @@ -29,7 +30,34 @@ api.set_provider(FlagdProvider())

### In-process resolver

This mode performs flag evaluations locally (in-process).
This mode performs flag evaluations locally (in-process). Flag configurations for evaluation are obtained via gRPC protocol using [sync protobuf schema](https://buf.build/open-feature/flagd/file/main:sync/v1/sync_service.proto) service definition.

Consider the following example to create a `FlagdProvider` with in-process evaluations,

```python
from openfeature import api
from openfeature.contrib.provider.flagd import FlagdProvider
from openfeature.contrib.provider.flagd.config import ResolverType

api.set_provider(FlagdProvider(
resolver_type=ResolverType.IN_PROCESS,
))
```

In the above example, in-process handlers attempt to connect to a sync service on address `localhost:8013` to obtain [flag definitions](https://github.com/open-feature/schemas/blob/main/json/flags.json).

<!--
#### Sync-metadata

To support the injection of contextual data configured in flagd for in-process evaluation, the provider exposes a `getSyncMetadata` accessor which provides the most recent value returned by the [GetMetadata RPC](https://buf.build/open-feature/flagd/docs/main:flagd.sync.v1#flagd.sync.v1.FlagSyncService.GetMetadata).
The value is updated with every (re)connection to the sync implementation.
This can be used to enrich evaluations with such data.
If the `in-process` mode is not used, and before the provider is ready, the `getSyncMetadata` returns an empty map.
-->
#### Offline mode

In-process resolvers can also work in an offline mode.
To enable this mode, you should provide a valid flag configuration file with the option `offlineFlagSourcePath`.

```python
from openfeature import api
Expand All @@ -42,6 +70,10 @@ api.set_provider(FlagdProvider(
))
```

Provider will attempt to detect file changes using polling.
Polling happens at 5 second intervals and this is currently unconfigurable.
This mode is useful for local development, tests and offline applications.

### Configuration options

The default options can be defined in the FlagdProvider constructor.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class CacheType(Enum):
ENV_VAR_RETRY_BACKOFF_MS = "FLAGD_RETRY_BACKOFF_MS"
ENV_VAR_RETRY_BACKOFF_MAX_MS = "FLAGD_RETRY_BACKOFF_MAX_MS"
ENV_VAR_RETRY_GRACE_PERIOD_SECONDS = "FLAGD_RETRY_GRACE_PERIOD"
ENV_VAR_SELECTOR = "FLAGD_SOURCE_SELECTOR"
ENV_VAR_STREAM_DEADLINE_MS = "FLAGD_STREAM_DEADLINE_MS"
ENV_VAR_TLS = "FLAGD_TLS"
ENV_VAR_TLS_CERT = "FLAGD_SERVER_CERT_PATH"
Expand Down Expand Up @@ -78,6 +79,7 @@ def __init__( # noqa: PLR0913
host: typing.Optional[str] = None,
port: typing.Optional[int] = None,
tls: typing.Optional[bool] = None,
selector: typing.Optional[str] = None,
resolver: typing.Optional[ResolverType] = None,
offline_flag_source_path: typing.Optional[str] = None,
offline_poll_interval_ms: typing.Optional[int] = None,
Expand Down Expand Up @@ -209,3 +211,7 @@ def __init__( # noqa: PLR0913
if cert_path is None
else cert_path
)

self.selector = (
env_or_default(ENV_VAR_SELECTOR, None) if selector is None else selector
)
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ def __init__( # noqa: PLR0913
deadline: typing.Optional[int] = None,
timeout: typing.Optional[int] = None,
retry_backoff_ms: typing.Optional[int] = None,
selector: typing.Optional[str] = None,
resolver_type: typing.Optional[ResolverType] = None,
offline_flag_source_path: typing.Optional[str] = None,
stream_deadline_ms: typing.Optional[int] = None,
Expand Down Expand Up @@ -86,6 +87,7 @@ def __init__( # noqa: PLR0913
retry_backoff_ms=retry_backoff_ms,
retry_backoff_max_ms=retry_backoff_max_ms,
retry_grace_period=retry_grace_period,
selector=selector,
resolver=resolver_type,
offline_flag_source_path=offline_flag_source_path,
stream_deadline_ms=stream_deadline_ms,
Expand All @@ -107,7 +109,13 @@ def setup_resolver(self) -> AbstractResolver:
self.emit_provider_configuration_changed,
)
elif self.config.resolver == ResolverType.IN_PROCESS:
return InProcessResolver(self.config, self)
return InProcessResolver(
self.config,
self.emit_provider_ready,
self.emit_provider_error,
self.emit_provider_stale,
self.emit_provider_configuration_changed,
)
else:
raise ValueError(
f"`resolver_type` parameter invalid: {self.config.resolver}"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,51 +1,5 @@
import typing

from openfeature.evaluation_context import EvaluationContext
from openfeature.flag_evaluation import FlagResolutionDetails

from .grpc import GrpcResolver
from .in_process import InProcessResolver


class AbstractResolver(typing.Protocol):
def initialize(self, evaluation_context: EvaluationContext) -> None: ...

def shutdown(self) -> None: ...

def resolve_boolean_details(
self,
key: str,
default_value: bool,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[bool]: ...

def resolve_string_details(
self,
key: str,
default_value: str,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[str]: ...

def resolve_float_details(
self,
key: str,
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]: ...

def resolve_integer_details(
self,
key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]: ...

def resolve_object_details(
self,
key: str,
default_value: typing.Union[dict, list],
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[typing.Union[dict, list]]: ...

from .protocol import AbstractResolver

__all__ = ["AbstractResolver", "GrpcResolver", "InProcessResolver"]
Original file line number Diff line number Diff line change
@@ -1,36 +1,52 @@
import typing

from openfeature.evaluation_context import EvaluationContext
from openfeature.event import ProviderEventDetails
from openfeature.exception import FlagNotFoundError, ParseError
from openfeature.flag_evaluation import FlagResolutionDetails, Reason
from openfeature.provider import AbstractProvider

from ..config import Config
from .process.file_watcher import FileWatcherFlagStore
from .process.connector import FlagStateConnector
from .process.connector.file_watcher import FileWatcher
from .process.connector.grpc_watcher import GrpcWatcher
from .process.flags import FlagStore
from .process.targeting import targeting

T = typing.TypeVar("T")


class InProcessResolver:
def __init__(self, config: Config, provider: AbstractProvider):
def __init__(
self,
config: Config,
emit_provider_ready: typing.Callable[[ProviderEventDetails], None],
emit_provider_error: typing.Callable[[ProviderEventDetails], None],
emit_provider_stale: typing.Callable[[ProviderEventDetails], None],
emit_provider_configuration_changed: typing.Callable[
[ProviderEventDetails], None
],
):
self.config = config
self.provider = provider
if not self.config.offline_flag_source_path:
raise ValueError(
"offline_flag_source_path must be provided when using in-process resolver"
self.flag_store = FlagStore(emit_provider_configuration_changed)
self.connector: FlagStateConnector = (
FileWatcher(
self.config, self.flag_store, emit_provider_ready, emit_provider_error
)
if self.config.offline_flag_source_path
else GrpcWatcher(
self.config,
self.flag_store,
emit_provider_ready,
emit_provider_error,
emit_provider_stale,
)
self.flag_store = FileWatcherFlagStore(
self.config.offline_flag_source_path,
self.provider,
self.config.retry_backoff_ms * 0.001,
)

def initialize(self, evaluation_context: EvaluationContext) -> None:
pass
self.connector.initialize(evaluation_context)

def shutdown(self) -> None:
self.flag_store.shutdown()
self.connector.shutdown()

def resolve_boolean_details(
self,
Expand All @@ -54,15 +70,19 @@ def resolve_float_details(
default_value: float,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[float]:
return self._resolve(key, default_value, evaluation_context)
result = self._resolve(key, default_value, evaluation_context)
if isinstance(result.value, int):
result.value = float(result.value)
return result

def resolve_integer_details(
self,
key: str,
default_value: int,
evaluation_context: typing.Optional[EvaluationContext] = None,
) -> FlagResolutionDetails[int]:
return self._resolve(key, default_value, evaluation_context)
result = self._resolve(key, default_value, evaluation_context)
return result

def resolve_object_details(
self,
Expand Down Expand Up @@ -98,6 +118,7 @@ def _resolve(
raise ParseError(
"Parsed JSONLogic targeting did not return a string or bool"
)

variant, value = flag.get_variant(variant)
if not value:
raise ParseError(f"Resolved variant {variant} not in variants config.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import typing

from openfeature.evaluation_context import EvaluationContext


class FlagStateConnector(typing.Protocol):
def initialize(
self, evaluation_context: EvaluationContext
) -> None: ... # pragma: no cover

def shutdown(self) -> None: ... # pragma: no cover
Loading
Loading