From a381b3dce3e8f9092dce0814a7728faa7a2faeee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 14 Mar 2025 17:30:16 -0300 Subject: [PATCH 1/8] add environment-document endpoint --- src/edge_proxy/server.py | 16 ++++++++++++++++ tests/test_server.py | 39 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 54 insertions(+), 1 deletion(-) diff --git a/src/edge_proxy/server.py b/src/edge_proxy/server.py index 0d26b85..f3a2c14 100644 --- a/src/edge_proxy/server.py +++ b/src/edge_proxy/server.py @@ -103,6 +103,22 @@ async def get_identities( return ORJSONResponse(data) +@app.get("/api/v1/environment-document", response_class=ORJSONResponse) +async def environment_document( + x_environment_key: str = Header(None), +) -> ORJSONResponse: + for key_pair in settings.environment_key_pairs: + print(key_pair) + if key_pair.server_side_key == x_environment_key: + environment_doc = environment_service.get_environment( + key_pair.client_side_key + ) + print(environment_doc is None) + if environment_doc: + return ORJSONResponse(environment_doc) + return ORJSONResponse(status_code=401, content=None) + + @app.on_event("startup") @repeat_every( seconds=settings.api_poll_frequency_seconds, diff --git a/tests/test_server.py b/tests/test_server.py index 3f5d19e..5f35243 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from edge_proxy.settings import AppSettings, HealthCheckSettings +from edge_proxy.settings import AppSettings, HealthCheckSettings, EnvironmentKeyPair from tests.fixtures.response_data import environment_1 if typing.TYPE_CHECKING: @@ -287,3 +287,40 @@ def test_get_identities( assert response.status_code == 200 assert data["traits"] == [] assert data["flags"] + + +@pytest.mark.parametrize( + "environment_key,expected_status", + [ + ("ser.good", 200), + ("ser.bad", 401), + (None, 401), + ], +) +def test_get_environment_document( + mocker: MockerFixture, + client: TestClient, + environment_key: str, + expected_status: int, +) -> None: + # Given + environment_key_pairs = [ + EnvironmentKeyPair(server_side_key="ser.good", client_side_key="foo") + ] + mocker.patch( + "edge_proxy.server.settings.environment_key_pairs", environment_key_pairs + ) + mocker.patch( + "edge_proxy.server.environment_service.cache" + ).get_environment.return_value = environment_1 + + # When + response = client.get( + "/api/v1/environment-document", + headers={"X-Environment-Key": environment_key} if environment_key else None, + ) + + # Then + assert response.status_code == expected_status + if expected_status == 200: + assert response.json() == environment_1 From 8c6bb9c0ca5ae0aaefa412098cea0e6ac24d0adf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Fri, 14 Mar 2025 19:19:30 -0300 Subject: [PATCH 2/8] nit --- tests/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index 5f35243..c637973 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -300,7 +300,7 @@ def test_get_identities( def test_get_environment_document( mocker: MockerFixture, client: TestClient, - environment_key: str, + environment_key: str | None, expected_status: int, ) -> None: # Given From 2a91b598d7b988052f9022efd5e30951aad1d7f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Mon, 17 Mar 2025 09:15:46 -0300 Subject: [PATCH 3/8] remove prints --- src/edge_proxy/server.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/edge_proxy/server.py b/src/edge_proxy/server.py index f3a2c14..6fc8f75 100644 --- a/src/edge_proxy/server.py +++ b/src/edge_proxy/server.py @@ -108,12 +108,10 @@ async def environment_document( x_environment_key: str = Header(None), ) -> ORJSONResponse: for key_pair in settings.environment_key_pairs: - print(key_pair) if key_pair.server_side_key == x_environment_key: environment_doc = environment_service.get_environment( key_pair.client_side_key ) - print(environment_doc is None) if environment_doc: return ORJSONResponse(environment_doc) return ORJSONResponse(status_code=401, content=None) From c1fba53b41fb9f174c3603c74515f76069220a59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Mon, 17 Mar 2025 09:48:18 -0300 Subject: [PATCH 4/8] separate environment document test cases --- tests/test_server.py | 39 +++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index c637973..a89194f 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -289,19 +289,9 @@ def test_get_identities( assert data["flags"] -@pytest.mark.parametrize( - "environment_key,expected_status", - [ - ("ser.good", 200), - ("ser.bad", 401), - (None, 401), - ], -) def test_get_environment_document( mocker: MockerFixture, client: TestClient, - environment_key: str | None, - expected_status: int, ) -> None: # Given environment_key_pairs = [ @@ -317,10 +307,31 @@ def test_get_environment_document( # When response = client.get( "/api/v1/environment-document", - headers={"X-Environment-Key": environment_key} if environment_key else None, + headers={"X-Environment-Key": environment_key_pairs[0].server_side_key}, ) # Then - assert response.status_code == expected_status - if expected_status == 200: - assert response.json() == environment_1 + assert response.status_code == 200 + assert response.json() == environment_1 + + +def test_get_environment_document_missing_key( + client: TestClient, +) -> None: + # When + response = client.get( + "/api/v1/environment-document", + ) + # Then + assert response.status_code == 401 + + +def test_get_environment_document_wrong_key( + client: TestClient, +) -> None: + # When + response = client.get( + "/api/v1/environment-document", headers={"X-Environment-Key": "ser.bad"} + ) + # Then + assert response.status_code == 401 From f5bae61c772e22eb06703f6f1da5eeefad2b5327 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Mon, 17 Mar 2025 10:31:18 -0300 Subject: [PATCH 5/8] Abstract environment-document into service, make args explicit --- src/edge_proxy/environments.py | 36 +++++++++++++++++++++++----------- src/edge_proxy/server.py | 11 ++++------- tests/test_environments.py | 10 +++++----- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/src/edge_proxy/environments.py b/src/edge_proxy/environments.py index 8127a55..67ca13d 100644 --- a/src/edge_proxy/environments.py +++ b/src/edge_proxy/environments.py @@ -1,4 +1,4 @@ -import typing +from typing import Any, Optional from datetime import datetime from functools import lru_cache @@ -76,8 +76,8 @@ async def refresh_environment_caches(self): def get_flags_response_data( self, environment_key: str, feature: str = None - ) -> dict[str, typing.Any]: - environment_document = self.get_environment(environment_key) + ) -> dict[str, Any]: + environment_document = self.get_environment(client_side_key=environment_key) environment = EnvironmentModel.model_validate(environment_document) is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX) @@ -105,8 +105,8 @@ def get_flags_response_data( def get_identity_response_data( self, input_data: IdentityWithTraits, environment_key: str - ) -> dict[str, typing.Any]: - environment_document = self.get_environment(environment_key) + ) -> dict[str, Any]: + environment_document = self.get_environment(client_side_key=environment_key) environment = EnvironmentModel.model_validate(environment_document) is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX) @@ -137,12 +137,26 @@ def get_identity_response_data( } return data - def get_environment(self, client_side_key: str) -> dict[str, typing.Any]: - if environment_document := self.cache.get_environment(client_side_key): - return environment_document - raise FlagsmithUnknownKeyError(client_side_key) - - async def _fetch_document(self, server_side_key: str) -> dict[str, typing.Any]: + def get_environment( + self, + *, + client_side_key: Optional[str] = None, + server_side_key: Optional[str] = None, + ) -> dict[str, Any]: + if client_side_key: + if environment_document := self.cache.get_environment(client_side_key): + return environment_document + raise FlagsmithUnknownKeyError(client_side_key) + if server_side_key: + for key_pair in self.settings.environment_key_pairs: + if key_pair.server_side_key == server_side_key: + if environment_document := self.cache.get_environment( + key_pair.client_side_key + ): + return environment_document + raise FlagsmithUnknownKeyError(server_side_key) + + async def _fetch_document(self, server_side_key: str) -> dict[str, Any]: response = await self._client.get( url=f"{self.settings.api_url}/environment-document/", headers={"X-Environment-Key": server_side_key}, diff --git a/src/edge_proxy/server.py b/src/edge_proxy/server.py index 6fc8f75..f8c5c6a 100644 --- a/src/edge_proxy/server.py +++ b/src/edge_proxy/server.py @@ -107,13 +107,10 @@ async def get_identities( async def environment_document( x_environment_key: str = Header(None), ) -> ORJSONResponse: - for key_pair in settings.environment_key_pairs: - if key_pair.server_side_key == x_environment_key: - environment_doc = environment_service.get_environment( - key_pair.client_side_key - ) - if environment_doc: - return ORJSONResponse(environment_doc) + if environment_doc := environment_service.get_environment( + server_side_key=x_environment_key, + ): + return ORJSONResponse(environment_doc) return ORJSONResponse(status_code=401, content=None) diff --git a/tests/test_environments.py b/tests/test_environments.py index e9bbc88..f6655d6 100644 --- a/tests/test_environments.py +++ b/tests/test_environments.py @@ -111,13 +111,13 @@ 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 + client_side_key=settings.environment_key_pairs[0].client_side_key ) == doc_1 ) assert ( environment_service.get_environment( - settings.environment_key_pairs[1].client_side_key + client_side_key=settings.environment_key_pairs[1].client_side_key ) == doc_2 ) @@ -125,10 +125,10 @@ async def test_get_environment_works_correctly(mocker: MockerFixture): # 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 + client_side_key=settings.environment_key_pairs[0].client_side_key ) environment_service.get_environment( - settings.environment_key_pairs[1].client_side_key + client_side_key=settings.environment_key_pairs[1].client_side_key ) assert mock_client.get.call_count == 2 @@ -136,7 +136,7 @@ async def test_get_environment_works_correctly(mocker: MockerFixture): def test_get_environment_raises_for_unknown_keys(): environment_service = EnvironmentService(settings=settings) with pytest.raises(FlagsmithUnknownKeyError): - environment_service.get_environment("test_env_key_unknown") + environment_service.get_environment(client_side_key="test_env_key_unknown") @pytest.mark.asyncio From 6033eb3c3c820ba6ee79088c675a642e27c82be5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rodrigo=20L=C3=B3pez=20Dato?= Date: Mon, 17 Mar 2025 10:36:36 -0300 Subject: [PATCH 6/8] lint --- tests/test_server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_server.py b/tests/test_server.py index 4c6153e..0b832a2 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -6,7 +6,7 @@ from fastapi.testclient import TestClient from pytest_mock import MockerFixture -from edge_proxy.settings import AppSettings, HealthCheckSettings, EnvironmentKeyPair +from edge_proxy.settings import HealthCheckSettings, EnvironmentKeyPair from tests.fixtures.response_data import environment_1 if typing.TYPE_CHECKING: From f4ea8d6274369dde13a531fecf0b020034d5ca8d Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Thu, 17 Jul 2025 21:28:27 +0100 Subject: [PATCH 7/8] Refactor --- src/edge_proxy/environments.py | 34 ++++++++++++++++++---------------- src/edge_proxy/server.py | 2 +- tests/test_environments.py | 10 +++++----- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/src/edge_proxy/environments.py b/src/edge_proxy/environments.py index 8c069f5..29c313c 100644 --- a/src/edge_proxy/environments.py +++ b/src/edge_proxy/environments.py @@ -77,7 +77,7 @@ async def refresh_environment_caches(self): def get_flags_response_data( self, environment_key: str, feature: str = None ) -> dict[str, Any]: - environment_document = self.get_environment(client_side_key=environment_key) + environment_document = self.get_environment(environment_key=environment_key) environment = EnvironmentModel.model_validate(environment_document) is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX) @@ -106,7 +106,7 @@ def get_flags_response_data( def get_identity_response_data( self, input_data: IdentityWithTraits, environment_key: str ) -> dict[str, Any]: - environment_document = self.get_environment(client_side_key=environment_key) + environment_document = self.get_environment(environment_key=environment_key) environment = EnvironmentModel.model_validate(environment_document) is_server_key = environment_key.startswith(SERVER_API_KEY_PREFIX) @@ -140,21 +140,17 @@ def get_identity_response_data( def get_environment( self, *, - client_side_key: Optional[str] = None, - server_side_key: Optional[str] = None, + environment_key: Optional[str] = None, ) -> dict[str, Any]: - if client_side_key: - if environment_document := self.cache.get_environment(client_side_key): - return environment_document - raise FlagsmithUnknownKeyError(client_side_key) - if server_side_key: - for key_pair in self.settings.environment_key_pairs: - if key_pair.server_side_key == server_side_key: - if environment_document := self.cache.get_environment( - key_pair.client_side_key - ): - return environment_document - raise FlagsmithUnknownKeyError(server_side_key) + if environment_key and environment_key.startswith(SERVER_API_KEY_PREFIX): + client_side_key = self._get_client_key_from_server_key(environment_key) + else: + client_side_key = environment_key + + if environment_document := self.cache.get_environment(client_side_key): + return environment_document + + raise FlagsmithUnknownKeyError(environment_key) async def _fetch_document(self, key_pair: EnvironmentKeyPair) -> dict[str, Any]: headers = { @@ -198,3 +194,9 @@ async def _clear_endpoint_caches(self): func.cache_clear() except AttributeError: pass + + def _get_client_key_from_server_key(self, server_key: str) -> str: + for key_pair in self.settings.environment_key_pairs: + if key_pair.server_side_key == server_key: + return key_pair.client_side_key + raise FlagsmithUnknownKeyError(server_key) diff --git a/src/edge_proxy/server.py b/src/edge_proxy/server.py index 8c707e3..372109d 100644 --- a/src/edge_proxy/server.py +++ b/src/edge_proxy/server.py @@ -130,7 +130,7 @@ async def environment_document( x_environment_key: str = Header(None), ) -> ORJSONResponse: if environment_doc := environment_service.get_environment( - server_side_key=x_environment_key, + environment_key=x_environment_key, ): return ORJSONResponse(environment_doc) return ORJSONResponse(status_code=401, content=None) diff --git a/tests/test_environments.py b/tests/test_environments.py index 00647c1..d308b72 100644 --- a/tests/test_environments.py +++ b/tests/test_environments.py @@ -111,13 +111,13 @@ async def test_get_environment_works_correctly(mocker: MockerFixture): # Next, test that get environment return correct document assert ( environment_service.get_environment( - client_side_key=settings.environment_key_pairs[0].client_side_key + environment_key=settings.environment_key_pairs[0].client_side_key ) == doc_1 ) assert ( environment_service.get_environment( - client_side_key=settings.environment_key_pairs[1].client_side_key + environment_key=settings.environment_key_pairs[1].client_side_key ) == doc_2 ) @@ -125,10 +125,10 @@ async def test_get_environment_works_correctly(mocker: MockerFixture): # Next, let's verify that any additional call to get_environment does not call fetch document environment_service.get_environment( - client_side_key=settings.environment_key_pairs[0].client_side_key + environment_key=settings.environment_key_pairs[0].client_side_key ) environment_service.get_environment( - client_side_key=settings.environment_key_pairs[1].client_side_key + environment_key=settings.environment_key_pairs[1].client_side_key ) assert mock_client.get.call_count == 2 @@ -136,7 +136,7 @@ async def test_get_environment_works_correctly(mocker: MockerFixture): def test_get_environment_raises_for_unknown_keys(): environment_service = EnvironmentService(settings=settings) with pytest.raises(FlagsmithUnknownKeyError): - environment_service.get_environment(client_side_key="test_env_key_unknown") + environment_service.get_environment(environment_key="test_env_key_unknown") @pytest.mark.asyncio From 79b01e1e6a34831ba89c08ee4994d2c35afbda77 Mon Sep 17 00:00:00 2001 From: Matthew Elwell Date: Thu, 17 Jul 2025 21:33:03 +0100 Subject: [PATCH 8/8] Refactor --- tests/test_environments.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/test_environments.py b/tests/test_environments.py index d308b72..3609f4d 100644 --- a/tests/test_environments.py +++ b/tests/test_environments.py @@ -278,9 +278,7 @@ async def test_get_flags_response_data_skips_filter_for_server_key( # We create a new settings object that contains a server key as a client_side_key api_key = "ser." + environment_1_api_key _settings = AppSettings( - environment_key_pairs=[ - {"client_side_key": api_key, "server_side_key": "ser.key"} - ] + environment_key_pairs=[{"client_side_key": api_key, "server_side_key": api_key}] ) mocked_client = mocker.AsyncMock() @@ -342,9 +340,7 @@ async def test_get_identity_flags_response_skips_filter_for_server_key( # We create a new settings object that contains a server key as a client_side_key api_key = "ser." + environment_1_api_key _settings = AppSettings( - environment_key_pairs=[ - {"client_side_key": api_key, "server_side_key": "ser.key"} - ] + environment_key_pairs=[{"client_side_key": api_key, "server_side_key": api_key}] ) mocked_client = mocker.AsyncMock()