Skip to content

Commit

Permalink
Linting & formatting fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewelwell committed Jan 26, 2024
1 parent fee1b10 commit bbffe7c
Show file tree
Hide file tree
Showing 7 changed files with 89 additions and 36 deletions.
27 changes: 21 additions & 6 deletions src/cache.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import logging
import typing
from abc import ABC
from datetime import datetime

logger = logging.getLogger(__name__)

Expand All @@ -10,7 +9,11 @@ class BaseEnvironmentsCache(ABC):
def __init__(self, *args, **kwargs):
self.last_updated_at = None

def put_environment(self, environment_api_key: str, environment_document: typing.Dict[str, typing.Any]) -> bool:
def put_environment(
self,
environment_api_key: str,
environment_document: typing.Dict[str, typing.Any],
) -> bool:
"""
Update the environment cache for the given key with the given environment document.
Expand All @@ -22,10 +25,16 @@ def put_environment(self, environment_api_key: str, environment_document: typing
return True
return False

def _put_environment(self, environment_api_key: str, environment_document: typing.Dict[str, typing.Any]) -> None:
def _put_environment(
self,
environment_api_key: str,
environment_document: typing.Dict[str, typing.Any],
) -> None:
raise NotImplementedError()

def get_environment(self, environment_api_key: str) -> typing.Dict[str, typing.Any] | None:
def get_environment(
self, environment_api_key: str
) -> typing.Dict[str, typing.Any] | None:
raise NotImplementedError()


Expand All @@ -34,8 +43,14 @@ def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._cache = {}

def _put_environment(self, environment_api_key: str, environment_document: typing.Dict[str, typing.Any]) -> None:
def _put_environment(
self,
environment_api_key: str,
environment_document: typing.Dict[str, typing.Any],
) -> None:
self._cache[environment_api_key] = environment_document

def get_environment(self, environment_api_key) -> typing.Dict[str, typing.Any] | None:
def get_environment(
self, environment_api_key
) -> typing.Dict[str, typing.Any] | None:
return self._cache.get(environment_api_key)
25 changes: 18 additions & 7 deletions src/environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,36 @@

import httpx
from fastapi.responses import ORJSONResponse
from flag_engine.engine import get_environment_feature_state, get_environment_feature_states, \
get_identity_feature_states
from flag_engine.engine import (
get_environment_feature_state,
get_environment_feature_states,
get_identity_feature_states,
)
from flag_engine.environments.builders import build_environment_model
from flag_engine.identities.models import IdentityModel
from orjson import orjson

from src.cache import BaseEnvironmentsCache, LocalMemEnvironmentsCache
from src.exceptions import FlagsmithUnknownKeyError
from src.feature_utils import filter_out_server_key_only_feature_states
from src.mappers import map_feature_state_to_response_data, map_feature_states_to_response_data, \
map_traits_to_response_data
from src.mappers import (
map_feature_state_to_response_data,
map_feature_states_to_response_data,
map_traits_to_response_data,
)
from src.models import IdentityWithTraits
from src.settings import Settings

logger = logging.getLogger(__name__)


class EnvironmentService:
def __init__(self, cache: BaseEnvironmentsCache = None, client: httpx.AsyncClient = None, settings: Settings = None):
def __init__(
self,
cache: BaseEnvironmentsCache = None,
client: httpx.AsyncClient = None,
settings: Settings = None,
):
self.cache = cache or LocalMemEnvironmentsCache()
self.settings = settings or Settings()
self._client = client or httpx.AsyncClient(timeout=settings.api_poll_timeout)
Expand All @@ -49,10 +60,10 @@ async def refresh_environment_caches(self):
)
if self.cache.put_environment(
environment_api_key=key_pair.client_side_key,
environment_document=environment_document
environment_document=environment_document,
):
await self._clear_endpoint_caches()
except (httpx.HTTPError, orjson.JSONDecodeError) as e:
except (httpx.HTTPError, orjson.JSONDecodeError):
logger.exception(
f"Failed to fetch document for {key_pair.client_side_key}"
)
Expand Down
2 changes: 1 addition & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
environment_service = EnvironmentService(
LocalMemEnvironmentsCache(),
httpx.AsyncClient(timeout=settings.api_poll_timeout),
settings
settings,
)


Expand Down
2 changes: 1 addition & 1 deletion src/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ class IdentityWithTraits(BaseModel):
traits: list[TraitModel] = Field(default_factory=list)

def __str__(self):
return f"identifier:%s|traits:%s" % (
return "identifier:%s|traits:%s" % (
self.identifier,
",".join([f"{t.trait_key}={str(t.trait_value)}" for t in self.traits]),
)
Expand Down
1 change: 0 additions & 1 deletion src/settings.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import json
from functools import lru_cache
from pathlib import Path
from typing import Any, Dict, List, Tuple

Expand Down
56 changes: 39 additions & 17 deletions tests/test_environments.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
from src.environments import EnvironmentService
from src.exceptions import FlagsmithUnknownKeyError
from src.models import IdentityWithTraits
from src.settings import Settings, EndpointCachesSettings, EndpointCacheSettings
from tests.fixtures.response_data import environment_1_api_key, environment_1
from src.settings import (
EndpointCacheSettings,
EndpointCachesSettings,
Settings,
)
from tests.fixtures.response_data import environment_1, environment_1_api_key

client_key_2 = "test_env_key_2"

Expand Down Expand Up @@ -66,7 +70,9 @@ async def test_refresh_makes_correct_http_call(mocker: MockerFixture):


@pytest.mark.asyncio
async def test_refresh_does_not_update_last_updated_at_if_any_request_fails(mocker: MockerFixture):
async def test_refresh_does_not_update_last_updated_at_if_any_request_fails(
mocker: MockerFixture,
):
# Given
mock_client = mocker.AsyncMock()
mock_client.get.side_effect = [
Expand All @@ -91,7 +97,7 @@ async def test_get_environment_works_correctly(mocker: MockerFixture):

mock_client.get.side_effect = [
mocker.MagicMock(text=orjson.dumps(doc_1), raise_for_status=lambda: None),
mocker.MagicMock(text=orjson.dumps(doc_2), raise_for_status=lambda: None)
mocker.MagicMock(text=orjson.dumps(doc_2), raise_for_status=lambda: None),
]

environment_service = EnvironmentService(settings=settings, client=mock_client)
Expand All @@ -101,18 +107,26 @@ async def test_get_environment_works_correctly(mocker: MockerFixture):

# Next, test that get environment return correct document
assert (
environment_service.get_environment(settings.environment_key_pairs[0].client_side_key)
== doc_1
environment_service.get_environment(
settings.environment_key_pairs[0].client_side_key
)
== doc_1
)
assert (
environment_service.get_environment(settings.environment_key_pairs[1].client_side_key)
== doc_2
environment_service.get_environment(
settings.environment_key_pairs[1].client_side_key
)
== doc_2
)
assert mock_client.get.call_count == 2

# Next, let's verify that any additional call to get_environment does not call fetch document
environment_service.get_environment(settings.environment_key_pairs[0].client_side_key)
environment_service.get_environment(settings.environment_key_pairs[1].client_side_key)
environment_service.get_environment(
settings.environment_key_pairs[0].client_side_key
)
environment_service.get_environment(
settings.environment_key_pairs[1].client_side_key
)
assert mock_client.get.call_count == 2


Expand All @@ -123,7 +137,9 @@ def test_get_environment_raises_for_unknown_keys():


@pytest.mark.asyncio
async def test_refresh_environment_caches_clears_endpoint_caches_if_environment_changes(mocker: MockerFixture) -> None:
async def test_refresh_environment_caches_clears_endpoint_caches_if_environment_changes(
mocker: MockerFixture,
) -> None:
# Given
# we create a new settings object which includes caching settings
_settings = Settings(
Expand All @@ -133,7 +149,7 @@ async def test_refresh_environment_caches_clears_endpoint_caches_if_environment_
endpoint_caches=EndpointCachesSettings(
flags=EndpointCacheSettings(use_cache=True),
identities=EndpointCacheSettings(use_cache=True),
)
),
)

# let's create a modified environment document
Expand All @@ -152,7 +168,7 @@ async def test_refresh_environment_caches_clears_endpoint_caches_if_environment_
),
mocker.MagicMock(
text=orjson.dumps(modified_document), raise_for_status=lambda: None
)
),
]

# Now let's create the environment service, refresh the environment caches and
Expand All @@ -178,7 +194,9 @@ async def test_refresh_environment_caches_clears_endpoint_caches_if_environment_


@pytest.mark.asyncio
async def test_get_identity_flags_response_skips_cache_for_different_identity(mocker: MockerFixture) -> None:
async def test_get_identity_flags_response_skips_cache_for_different_identity(
mocker: MockerFixture,
) -> None:
# Given
# we create a new settings object which includes caching settings
_settings = Settings(
Expand All @@ -187,7 +205,7 @@ async def test_get_identity_flags_response_skips_cache_for_different_identity(mo
],
endpoint_caches=EndpointCachesSettings(
identities=EndpointCacheSettings(use_cache=True),
)
),
)

mocked_client = mocker.AsyncMock()
Expand All @@ -200,8 +218,12 @@ async def test_get_identity_flags_response_skips_cache_for_different_identity(mo

# When
# We retrieve the flags for 2 separate identities
environment_service.get_identity_response_data(IdentityWithTraits(identifier="foo"), environment_1_api_key)
environment_service.get_identity_response_data(IdentityWithTraits(identifier="bar"), environment_1_api_key)
environment_service.get_identity_response_data(
IdentityWithTraits(identifier="foo"), environment_1_api_key
)
environment_service.get_identity_response_data(
IdentityWithTraits(identifier="bar"), environment_1_api_key
)

# Then
# we get 2 cache misses
Expand Down
12 changes: 9 additions & 3 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@


@pytest.mark.parametrize("endpoint", ["/proxy/health", "/health"])
def test_health_check_returns_200_if_cache_was_updated_recently(mocker: MockerFixture, endpoint: str) -> None:
def test_health_check_returns_200_if_cache_was_updated_recently(
mocker: MockerFixture, endpoint: str
) -> None:
mocked_environment_service = mocker.patch("src.main.environment_service")
mocked_environment_service.last_updated_at = datetime.now()

Expand All @@ -35,7 +37,9 @@ def test_health_check_returns_500_if_cache_is_stale(mocker) -> None:
assert response.json() == {"status": "error"}


def test_get_flags(mocker: MockerFixture, environment_1_feature_states_response_list: list[dict]) -> None:
def test_get_flags(
mocker: MockerFixture, environment_1_feature_states_response_list: list[dict]
) -> None:
environment_key = "test_environment_key"
mocked_environment_cache = mocker.patch("src.main.environment_service.cache")
mocked_environment_cache.get_environment.return_value = environment_1
Expand All @@ -46,7 +50,9 @@ def test_get_flags(mocker: MockerFixture, environment_1_feature_states_response_
mocked_environment_cache.get_environment.assert_called_with(environment_key)


def test_get_flags_single_feature(mocker: MockerFixture, environment_1_feature_states_response_list: list[dict]) -> None:
def test_get_flags_single_feature(
mocker: MockerFixture, environment_1_feature_states_response_list: list[dict]
) -> None:
environment_key = "test_environment_key"
mocked_environment_cache = mocker.patch("src.main.environment_service.cache")
mocked_environment_cache.get_environment.return_value = environment_1
Expand Down

0 comments on commit bbffe7c

Please sign in to comment.