Skip to content

Commit

Permalink
added feature values
Browse files Browse the repository at this point in the history
  • Loading branch information
d.maximchuk committed Aug 29, 2024
1 parent a8f4c4d commit f15bfce
Show file tree
Hide file tree
Showing 16 changed files with 523 additions and 30 deletions.
22 changes: 18 additions & 4 deletions featureflags_client/http/client.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
from contextlib import contextmanager
from typing import Any, Dict, Generator, Optional, cast
from typing import Any, Dict, Generator, Optional, Union, cast

from featureflags_client.http.flags import Flags
from featureflags_client.http.managers.base import (
AsyncBaseManager,
BaseManager,
)
from featureflags_client.http.values import Values


class FeatureFlagsClient:
"""
Feature flags http based client.
Feature flags and values http based client.
"""

def __init__(self, manager: BaseManager) -> None:
Expand All @@ -29,9 +30,22 @@ def flags(
"""
yield Flags(self._manager, ctx, overrides)

@contextmanager
def values(
self,
ctx: Optional[Dict[str, Any]] = None,
*,
overrides: Optional[Dict[str, Union[int, str]]] = None,
) -> Generator[Values, None, None]:
"""
Context manager to wrap your request handling code and get actual
feature values.
"""
yield Values(self._manager, ctx, overrides)

def preload(self) -> None:
"""Preload flags from featureflags server.
This method syncs all flags with server"""
"""Preload flags and values from featureflags server.
This method syncs all flags and values with server"""
self._manager.preload()

async def preload_async(self) -> None:
Expand Down
67 changes: 66 additions & 1 deletion featureflags_client/http/conditions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import re
from typing import Any, Callable, Dict, List, Optional, Set

from featureflags_client.http.types import Check, Flag, Operator
from featureflags_client.http.types import Check, Flag, Operator, Value
from featureflags_client.http.utils import hash_flag_value

log = logging.getLogger(__name__)
Expand Down Expand Up @@ -206,3 +206,68 @@ def update_flags_state(flags: List[Flag]) -> Dict[str, Callable[..., bool]]:
procs[flag.name] = proc

return procs


def str_to_int(value: int | str) -> int | str:
try:
return int(value)
except ValueError:
return value


def value_proc(value: Value) -> Callable | int | str:
if not value.overridden:
# Value was not overridden on server, use value from defaults.
log.debug(
f"Value[{value.name}] is not override yet, using default value"
)
return str_to_int(value.value_default)

conditions = []
for condition in value.conditions:
checks_procs = [check_proc(check) for check in condition.checks]

# in case of invalid condition it would be safe to replace it
# with a falsish condition
if not checks_procs:
log.debug("Condition has empty checks")
checks_procs = [false]

conditions.append(
(condition.value_override, checks_procs),
)

if value.enabled and conditions:

def proc(ctx: Dict[str, Any]) -> int | str:
for condition_value_override, checks in conditions:
if all(check(ctx) for check in checks):
return str_to_int(condition_value_override)
return str_to_int(value.value_override)

else:
log.debug(
f"Value[{value.name}] is disabled or do not have any conditions"
)

def proc(ctx: Dict[str, Any]) -> int | str:
return str_to_int(value.value_override)

return proc


def update_values_state(
values: List[Value],
) -> dict[str, Callable[..., int | str]]:
"""
Assign a proc to each values which has to be computed.
"""

procs = {}

for value in values:
proc = value_proc(value)
if proc is not None:
procs[value.name] = proc

return procs
2 changes: 2 additions & 0 deletions featureflags_client/http/managers/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__( # noqa: PLR0913
project: str,
variables: List[Variable],
defaults: Union[EnumMeta, Type, Dict[str, bool]],
values_defaults: EnumMeta | Type | dict[str, int | str] | None = None,
request_timeout: int = 5,
refresh_interval: int = 10,
) -> None:
Expand All @@ -38,6 +39,7 @@ def __init__( # noqa: PLR0913
project,
variables,
defaults,
values_defaults,
request_timeout,
refresh_interval,
)
Expand Down
65 changes: 54 additions & 11 deletions featureflags_client/http/managers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,29 @@
)
from featureflags_client.http.utils import (
coerce_defaults,
coerce_values_defaults,
custom_asdict_factory,
intervals_gen,
)

log = logging.getLogger(__name__)


def _values_defaults_to_tuple(
values: list[str], values_defaults: dict[str, int | str]
) -> list[tuple[str, str | int]]:
result = []
for value in values:
value_default = values_defaults.get(value, "")
result.append(
(
value,
value_default,
)
)
return result


class BaseManager(ABC):
"""
Base manager for using with sync http clients.
Expand All @@ -35,17 +51,24 @@ def __init__( # noqa: PLR0913
project: str,
variables: List[Variable],
defaults: Union[EnumMeta, Type, Dict[str, bool]],
values_defaults: EnumMeta | Type | dict[str, int | str] | None = None,
request_timeout: int = 5,
refresh_interval: int = 60, # 1 minute.
) -> None:
self.url = url
self.defaults = coerce_defaults(defaults)

if values_defaults is None:
values_defaults = {}

self.values_defaults = coerce_values_defaults(values_defaults)

self._request_timeout = request_timeout
self._state = HttpState(
project=project,
variables=variables,
flags=list(self.defaults.keys()),
values=list(self.values_defaults.keys()),
)

self._int_gen = intervals_gen(interval=refresh_interval)
Expand Down Expand Up @@ -84,7 +107,9 @@ def _check_sync(self) -> None:
self._next_sync,
)

def get(self, name: str) -> Optional[Callable[[Dict], bool]]:
def get(
self, name: str
) -> Optional[Callable[[Dict], Union[bool, int, str]]]:
self._check_sync()
return self._state.get(name)

Expand All @@ -93,13 +118,18 @@ def preload(self) -> None:
project=self._state.project,
variables=self._state.variables,
flags=self._state.flags,
values=_values_defaults_to_tuple(
self._state.values,
self.values_defaults,
),
version=self._state.version,
)
log.debug(
"Exchange request, project: %s, version: %s, flags: %s",
"Exchange request, project: %s, version: %s, flags: %s, values: %s",
payload.project,
payload.version,
payload.flags,
payload.values,
)

response_raw = self._post(
Expand All @@ -110,19 +140,21 @@ def preload(self) -> None:
log.debug("Preload response: %s", response_raw)

response = PreloadFlagsResponse.from_dict(response_raw)
self._state.update(response.flags, response.version)
self._state.update(response.flags, response.values, response.version)

def sync(self) -> None:
payload = SyncFlagsRequest(
project=self._state.project,
flags=self._state.flags,
values=self._state.values,
version=self._state.version,
)
log.debug(
"Sync request, project: %s, version: %s, flags: %s",
"Sync request, project: %s, version: %s, flags: %s, values: %s",
payload.project,
payload.version,
payload.flags,
payload.values,
)

response_raw = self._post(
Expand All @@ -133,7 +165,7 @@ def sync(self) -> None:
log.debug("Sync reply: %s", response_raw)

response = SyncFlagsResponse.from_dict(response_raw)
self._state.update(response.flags, response.version)
self._state.update(response.flags, response.values, response.version)


class AsyncBaseManager(BaseManager):
Expand All @@ -147,6 +179,7 @@ def __init__( # noqa: PLR0913
project: str,
variables: List[Variable],
defaults: Union[EnumMeta, Type, Dict[str, bool]],
values_defaults: EnumMeta | Type | dict[str, int | str] | None = None,
request_timeout: int = 5,
refresh_interval: int = 10,
) -> None:
Expand All @@ -155,6 +188,7 @@ def __init__( # noqa: PLR0913
project,
variables,
defaults,
values_defaults,
request_timeout,
refresh_interval,
)
Expand All @@ -173,25 +207,32 @@ async def _post( # type: ignore
async def close(self) -> None:
pass

def get(self, name: str) -> Optional[Callable[[Dict], bool]]:
def get(
self, name: str
) -> Optional[Callable[[Dict], Union[bool, int, str]]]:
return self._state.get(name)

async def preload(self) -> None: # type: ignore
"""
Preload flags from the server.
Preload flags and values from the server.
"""

payload = PreloadFlagsRequest(
project=self._state.project,
variables=self._state.variables,
flags=self._state.flags,
values=_values_defaults_to_tuple(
self._state.values,
self.values_defaults,
),
version=self._state.version,
)
log.debug(
"Exchange request, project: %s, version: %s, flags: %s",
"Exchange request, project: %s, version: %s, flags: %s, values: %s",
payload.project,
payload.version,
payload.flags,
payload.values,
)

response_raw = await self._post(
Expand All @@ -202,19 +243,21 @@ async def preload(self) -> None: # type: ignore
log.debug("Preload response: %s", response_raw)

response = PreloadFlagsResponse.from_dict(response_raw)
self._state.update(response.flags, response.version)
self._state.update(response.flags, response.values, response.version)

async def sync(self) -> None: # type: ignore
payload = SyncFlagsRequest(
project=self._state.project,
flags=self._state.flags,
values=self._state.values,
version=self._state.version,
)
log.debug(
"Sync request, project: %s, version: %s, flags: %s",
"Sync request, project: %s, version: %s, flags: %s, values: %s",
payload.project,
payload.version,
payload.flags,
payload.values,
)

response_raw = await self._post(
Expand All @@ -225,7 +268,7 @@ async def sync(self) -> None: # type: ignore
log.debug("Sync reply: %s", response_raw)

response = SyncFlagsResponse.from_dict(response_raw)
self._state.update(response.flags, response.version)
self._state.update(response.flags, response.values, response.version)

def start(self) -> None:
if self._refresh_task is not None:
Expand Down
10 changes: 7 additions & 3 deletions featureflags_client/http/managers/dummy.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import logging
from typing import Any, Callable, Dict, Optional
from typing import Any, Callable, Dict, Optional, Union

from featureflags_client.http.constants import Endpoints
from featureflags_client.http.managers.base import (
Expand All @@ -16,7 +16,9 @@ class DummyManager(BaseManager):
It can be helpful when you want to use flags with their default values.
"""

def get(self, name: str) -> Optional[Callable[[Dict], bool]]:
def get(
self, name: str
) -> Optional[Callable[[Dict], Union[bool, int, str]]]:
"""
So that `featureflags.http.flags.Flags` will use default values.
"""
Expand All @@ -43,7 +45,9 @@ class AsyncDummyManager(AsyncBaseManager):
It can be helpful when you want to use flags with their default values.
"""

def get(self, name: str) -> Optional[Callable[[Dict], bool]]:
def get(
self, name: str
) -> Optional[Callable[[Dict], Union[bool, int, str]]]:
"""
So that `featureflags.http.flags.Flags` will use default values.
"""
Expand Down
2 changes: 2 additions & 0 deletions featureflags_client/http/managers/httpx.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ def __init__( # noqa: PLR0913
project: str,
variables: List[Variable],
defaults: Union[EnumMeta, Type, Dict[str, bool]],
values_defaults: EnumMeta | Type | dict[str, int | str] | None = None,
request_timeout: int = 5,
refresh_interval: int = 10,
) -> None:
Expand All @@ -38,6 +39,7 @@ def __init__( # noqa: PLR0913
project,
variables,
defaults,
values_defaults,
request_timeout,
refresh_interval,
)
Expand Down
Loading

0 comments on commit f15bfce

Please sign in to comment.