-
Notifications
You must be signed in to change notification settings - Fork 1
Add caching for client get #79
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
Merged
Relm-Arrowny
merged 34 commits into
main
from
66-implement-caching-on-get_configuration
May 22, 2025
Merged
Changes from all commits
Commits
Show all changes
34 commits
Select commit
Hold shift + click to select a range
77179b1
Remove unused features
olliesilvester 886268e
Remove CI for GUI
olliesilvester 4f86289
Update helmchart/values.yaml
olliesilvester c3f4124
Changes to CI
olliesilvester dd94bdd
remove paths from ci
olliesilvester 2184ace
update readme
olliesilvester d392aae
Update helmchart
olliesilvester 4069f66
Merge remote-tracking branch 'origin/get_cI_working' into 64_remove_u…
olliesilvester da4fb1d
create main get_configuration endpoint and add tests
olliesilvester c21a08f
Improve CI
olliesilvester 6aaca84
Improve codecov
olliesilvester 576e4f2
Merge branch 'main' into 65_create_main_endpoint
olliesilvester 963cd9f
Response from review
olliesilvester 2f1fe49
Small tidy on build bash scri[t
olliesilvester b7ffc13
add cache for client get
Relm-Arrowny c8c1b4c
add cachetools
Relm-Arrowny dd7ea82
catch miss cache
Relm-Arrowny f32e464
add error handling on responds
Relm-Arrowny d9cd16b
remove unused variables
olliesilvester 52e9a3f
remvoe try and add lib to dev
Relm-Arrowny 7cb94ff
Merge remote-tracking branch 'remotes/origin/65_create_main_endpoint'…
Relm-Arrowny 7bce3af
correct test to raise the correct exception
Relm-Arrowny bc5a2ff
make cache size and lifetime customisables
Relm-Arrowny b6ad4de
correct docstring
Relm-Arrowny d605301
remove pop cache
Relm-Arrowny 0f02426
make use of cachedmethod.
Relm-Arrowny 0c18eac
rename response to data
Relm-Arrowny aed0cf4
Update src/daq_config_server/__main__.py
olliesilvester 8a2d57d
Remove unused beamline variable
olliesilvester a26a4e8
Merge branch '65_create_main_endpoint' into 66-implement-caching-on-g…
olliesilvester b04eebe
fix docString
Relm-Arrowny a237f9e
Merge remote-tracking branch 'origin/main' into 66-implement-caching-…
Relm-Arrowny 3320488
Merge branch '66-implement-caching-on-get_configuration' of github.co…
Relm-Arrowny 3560c9f
correct typo
Relm-Arrowny File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,26 +1,101 @@ | ||
| import operator | ||
| from logging import Logger, getLogger | ||
| from typing import Any | ||
|
|
||
| import requests | ||
| from cachetools import TTLCache, cachedmethod | ||
|
|
||
| from .constants import ENDPOINTS | ||
|
|
||
|
|
||
| class ConfigServer: | ||
| def __init__(self, url: str, log: Logger | None = None) -> None: | ||
| def __init__( | ||
| self, | ||
| url: str, | ||
| log: Logger | None = None, | ||
| cache_size: int = 10, | ||
| cache_lifetime_s: int = 3600, | ||
| ) -> None: | ||
| """ | ||
| Initialize the ConfigServer client. | ||
|
|
||
| Args: | ||
| url: Base URL of the config server. | ||
| log: Optional logger instance. | ||
| cache_size: Size of the cache (maximum number of items can be stored). | ||
| cache_lifetime_s: Lifetime of the cache (in seconds). | ||
| """ | ||
| self._url = url.rstrip("/") | ||
| self._log = log if log else getLogger("daq_config_server.client") | ||
| self._cache = TTLCache(maxsize=cache_size, ttl=cache_lifetime_s) | ||
|
|
||
| def _get( | ||
Relm-Arrowny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self, | ||
| endpoint: str, | ||
| item: str | None = None, | ||
| ): | ||
| r = requests.get(self._url + endpoint + (f"/{item}" if item else "")) | ||
| return r.json() | ||
|
|
||
| def read_unformatted_file(self, file_path: str) -> Any: | ||
| # After https://github.com/DiamondLightSource/daq-config-server/issues/67, we | ||
| # can get specific formats, and then have better typing on | ||
| # return values | ||
| return self._get(ENDPOINTS.CONFIG, file_path) | ||
| reset_cached_result: bool = False, | ||
| ) -> Any: | ||
| """ | ||
| Get data from the config server with cache management. | ||
| If a cached response doesn't already exist, makes a request to | ||
| the config server. | ||
| If reset_cached_result is true, remove the cache entry for that request and | ||
| make a new request | ||
|
|
||
| Args: | ||
| endpoint: API endpoint. | ||
| item: Optional item identifier. | ||
| reset_cached_result: Whether to reset cache. | ||
|
|
||
| Returns: | ||
| The response data. | ||
| """ | ||
|
|
||
| if (endpoint, item) in self._cache and reset_cached_result: | ||
| del self._cache[(endpoint, item)] | ||
| return self._cached_get(endpoint, item) | ||
|
|
||
| @cachedmethod(cache=operator.attrgetter("_cache")) | ||
olliesilvester marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| def _cached_get( | ||
Relm-Arrowny marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| self, | ||
| endpoint: str, | ||
| item: str | None = None, | ||
| ) -> Any: | ||
| """ | ||
| Get data from the config server and cache it. | ||
|
|
||
| Args: | ||
| endpoint: API endpoint. | ||
| item: Optional item identifier. | ||
|
|
||
| Returns: | ||
| The response data. | ||
| """ | ||
| url = self._url + endpoint + (f"/{item}" if item else "") | ||
|
|
||
| try: | ||
| r = requests.get(url) | ||
| r.raise_for_status() | ||
| data = r.json() | ||
| self._log.debug(f"Cache set for {endpoint}/{item}.") | ||
| return data | ||
| except requests.exceptions.HTTPError as e: | ||
| self._log.error(f"HTTP error: {e}") | ||
| raise | ||
|
|
||
| def read_unformatted_file( | ||
| self, file_path: str, reset_cached_result: bool = False | ||
| ) -> Any: | ||
| """ | ||
| Read an unformatted file from the config server. | ||
|
|
||
| Args: | ||
| file_path: Path to the file. | ||
| reset_cached_result: Whether to reset cache. | ||
|
|
||
| Returns: | ||
| The file content. | ||
| """ | ||
| return self._get( | ||
| ENDPOINTS.CONFIG, file_path, reset_cached_result=reset_cached_result | ||
| ) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,18 +1,115 @@ | ||
| from time import sleep | ||
| from unittest.mock import MagicMock, patch | ||
|
|
||
| import pytest | ||
| import requests | ||
| from fastapi import status | ||
| from httpx import Response | ||
|
|
||
| from daq_config_server.client import ConfigServer | ||
| from daq_config_server.constants import ENDPOINTS | ||
|
|
||
|
|
||
| # More useful tests for the client are in tests/system_tests | ||
| def make_mock_response(json_value, status_code=200, raise_exc=None): | ||
| mock_response = MagicMock() | ||
| mock_response.json.return_value = json_value | ||
| mock_response.status_code = status_code | ||
| if raise_exc: | ||
| mock_response.raise_for_status.side_effect = raise_exc | ||
| else: | ||
| mock_response.raise_for_status.return_value = None | ||
| return mock_response | ||
|
|
||
|
|
||
| @patch("daq_config_server.client.requests.get") | ||
| def test_read_unformatted_file(mock_request: MagicMock): | ||
| mock_request.return_value = Response(status_code=status.HTTP_200_OK, json="test") | ||
| """Test that read_unformatted_file calls the correct endpoint and | ||
| returns the expected result.""" | ||
| mock_request.return_value = make_mock_response({"key": "value"}, status.HTTP_200_OK) | ||
| file_path = "test" | ||
| url = "url" | ||
| server = ConfigServer(url) | ||
| server.read_unformatted_file(file_path) | ||
| result = server.read_unformatted_file(file_path) | ||
| assert result == {"key": "value"} | ||
| mock_request.assert_called_once_with(url + ENDPOINTS.CONFIG + "/" + file_path) | ||
|
|
||
|
|
||
| @patch("daq_config_server.client.requests.get") | ||
| def test_read_unformatted_file_reading_cache(mock_request: MagicMock): | ||
| """Test cache behavior for read_unformatted_file.""" | ||
| mock_request.side_effect = [ | ||
| make_mock_response("1st_read", status.HTTP_200_OK), | ||
| make_mock_response("2nd_read", status.HTTP_200_OK), | ||
| make_mock_response("3rd_read", status.HTTP_200_OK), | ||
| ] | ||
| file_path = "test" | ||
| url = "url" | ||
| server = ConfigServer(url) | ||
| assert server.read_unformatted_file(file_path) == "1st_read" | ||
| assert server.read_unformatted_file(file_path) == "1st_read" # Cached | ||
| assert ( | ||
| server.read_unformatted_file(file_path, reset_cached_result=True) == "2nd_read" | ||
| ) | ||
| assert server.read_unformatted_file(file_path) == "2nd_read" # Cached | ||
| assert ( | ||
| server.read_unformatted_file(file_path, reset_cached_result=True) == "3rd_read" | ||
| ) | ||
|
|
||
|
|
||
| @patch("daq_config_server.client.requests.get") | ||
| def test_read_unformatted_file_reading_reset_cached_result_true_without_cache( | ||
| mock_request: MagicMock, | ||
| ): | ||
| """Test repeated reset_cached_result=False disables cache for each call.""" | ||
| mock_request.side_effect = [ | ||
| make_mock_response("1st_read", status.HTTP_200_OK), | ||
| ] | ||
| file_path = "test" | ||
| url = "url" | ||
| server = ConfigServer(url) | ||
| assert ( | ||
| server.read_unformatted_file(file_path, reset_cached_result=True) == "1st_read" | ||
| ) | ||
|
|
||
|
|
||
| @patch("daq_config_server.client.requests.get") | ||
| def test_read_unformatted_file_reading_not_OK(mock_request: MagicMock): | ||
| """Test that a non-200 response raises a RequestException.""" | ||
| mock_request.return_value = make_mock_response( | ||
| "1st_read", status.HTTP_204_NO_CONTENT, raise_exc=requests.exceptions.HTTPError | ||
| ) | ||
| file_path = "test" | ||
| url = "url" | ||
| server = ConfigServer(url) | ||
| with pytest.raises(requests.exceptions.HTTPError): | ||
| server.read_unformatted_file(file_path) | ||
|
|
||
|
|
||
| @patch("daq_config_server.client.requests.get") | ||
| def test_read_unformatted_file_reading_cache_custom_size(mock_request: MagicMock): | ||
| mock_request.side_effect = [ | ||
| make_mock_response("1st_read", status.HTTP_200_OK), | ||
| make_mock_response("2nd_read", status.HTTP_200_OK), | ||
| make_mock_response("3rd_read", status.HTTP_200_OK), | ||
| ] | ||
| file_path = "test" | ||
| url = "url" | ||
| server = ConfigServer(url=url, cache_size=1) | ||
| assert server.read_unformatted_file(file_path) == "1st_read" | ||
| assert server.read_unformatted_file(file_path + "1") == "2nd_read" | ||
| assert server.read_unformatted_file(file_path) == "3rd_read" | ||
|
|
||
|
|
||
| @patch("daq_config_server.client.requests.get") | ||
| def test_read_unformatted_file_cache_custom_lifetime(mock_request: MagicMock): | ||
| mock_request.side_effect = [ | ||
| make_mock_response("1st_read", status.HTTP_200_OK), | ||
| make_mock_response("2nd_read", status.HTTP_200_OK), | ||
| make_mock_response("3rd_read", status.HTTP_200_OK), | ||
| ] | ||
| file_path = "test" | ||
| url = "url" | ||
| server = ConfigServer(url=url, cache_lifetime_s=0.1) # type: ignore | ||
| assert server.read_unformatted_file(file_path) == "1st_read" | ||
| assert server.read_unformatted_file(file_path) == "1st_read" | ||
| sleep(0.1) | ||
| assert server.read_unformatted_file(file_path) == "2nd_read" |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.