Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
77179b1
Remove unused features
olliesilvester Apr 28, 2025
886268e
Remove CI for GUI
olliesilvester Apr 28, 2025
4f86289
Update helmchart/values.yaml
olliesilvester Apr 28, 2025
c3f4124
Changes to CI
olliesilvester Apr 28, 2025
dd94bdd
remove paths from ci
olliesilvester Apr 28, 2025
2184ace
update readme
olliesilvester May 2, 2025
d392aae
Update helmchart
olliesilvester May 2, 2025
4069f66
Merge remote-tracking branch 'origin/get_cI_working' into 64_remove_u…
olliesilvester May 2, 2025
da4fb1d
create main get_configuration endpoint and add tests
olliesilvester May 8, 2025
c21a08f
Improve CI
olliesilvester May 8, 2025
6aaca84
Improve codecov
olliesilvester May 8, 2025
576e4f2
Merge branch 'main' into 65_create_main_endpoint
olliesilvester May 8, 2025
963cd9f
Response from review
olliesilvester May 19, 2025
2f1fe49
Small tidy on build bash scri[t
olliesilvester May 20, 2025
b7ffc13
add cache for client get
Relm-Arrowny May 20, 2025
c8c1b4c
add cachetools
Relm-Arrowny May 20, 2025
dd7ea82
catch miss cache
Relm-Arrowny May 20, 2025
f32e464
add error handling on responds
Relm-Arrowny May 21, 2025
d9cd16b
remove unused variables
olliesilvester May 21, 2025
52e9a3f
remvoe try and add lib to dev
Relm-Arrowny May 21, 2025
7cb94ff
Merge remote-tracking branch 'remotes/origin/65_create_main_endpoint'…
Relm-Arrowny May 21, 2025
7bce3af
correct test to raise the correct exception
Relm-Arrowny May 21, 2025
bc5a2ff
make cache size and lifetime customisables
Relm-Arrowny May 21, 2025
b6ad4de
correct docstring
Relm-Arrowny May 21, 2025
d605301
remove pop cache
Relm-Arrowny May 21, 2025
0f02426
make use of cachedmethod.
Relm-Arrowny May 22, 2025
0c18eac
rename response to data
Relm-Arrowny May 22, 2025
aed0cf4
Update src/daq_config_server/__main__.py
olliesilvester May 22, 2025
8a2d57d
Remove unused beamline variable
olliesilvester May 22, 2025
a26a4e8
Merge branch '65_create_main_endpoint' into 66-implement-caching-on-g…
olliesilvester May 22, 2025
b04eebe
fix docString
Relm-Arrowny May 22, 2025
a237f9e
Merge remote-tracking branch 'origin/main' into 66-implement-caching-…
Relm-Arrowny May 22, 2025
3320488
Merge branch '66-implement-caching-on-get_configuration' of github.co…
Relm-Arrowny May 22, 2025
3560c9f
correct typo
Relm-Arrowny May 22, 2025
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
59 changes: 39 additions & 20 deletions src/daq_config_server/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@
from typing import Any

import requests
from cachetools import TTLCache, cached
from cachetools import TTLCache

from .constants import ENDPOINTS

CACHE_SIZE = 10
CACHE_TTL = 3600 # seconds

# Cache for the config server
CACHE = TTLCache(maxsize=CACHE_SIZE, ttl=CACHE_TTL)


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: int = 3600,
) -> None:
"""
Initialize the ConfigServer client.

Expand All @@ -24,38 +24,50 @@ def __init__(self, url: str, log: Logger | None = None) -> None:
"""
self._url = url.rstrip("/")
self._log = log if log else getLogger("daq_config_server.client")
self._cache = TTLCache(maxsize=cache_size, ttl=cache_lifetime)

def _get(
self,
endpoint: str,
item: str | None = None,
use_cache: bool = True,
reset_cached_result: bool = False,
) -> Any:
"""
Internal method to get data from the config server, optionally using cache.
Internal method to get data from the cache config server, with cache
management.
This method checks if the data is already cached. If it is, it returns
the cached data. If not, it fetches the data from the config server,
caches it, and returns the data.
If reset_cached_result is True, it will remove the cached data and
fetch it again from the config server.

Args:
endpoint: API endpoint.
item: Optional item identifier.
use_cache: Whether to use the cache.
reset_cached_result: Whether to reset cache.

Returns:
The response data.
"""
if not use_cache:
if (self, endpoint, item) in CACHE:
CACHE.pop((self, endpoint, item))
input_hash = hash(endpoint + (f"/{item}" if item else ""))
if (input_hash) in self._cache:
self._log.debug(f"Cache hit for {endpoint}/{item}.")

if reset_cached_result:
self._cache.pop(input_hash)
self._log.debug(f"Cache entry for {endpoint}/{item} removed.")
return self._cached_get(endpoint, item)
return self._cache[input_hash]
self._log.debug(f"Cache miss for {endpoint}/{item}.")
return self._cached_get(endpoint, item)

@cached(cache=CACHE)
def _cached_get(
self,
endpoint: str,
item: str | None = None,
) -> Any:
"""
Get a cached value from the config server.
Get data from the config server and cache it.

Args:
endpoint: API endpoint.
Expand All @@ -72,17 +84,24 @@ def _cached_get(
except requests.exceptions.HTTPError as e:
self._log.error(f"HTTP error: {e}")
raise
return r.json()
response = r.json()
self._cache[hash(endpoint + (f"/{item}" if item else ""))] = r.json()
self._log.debug(f"Cache set for {endpoint}/{item}.")
return response

def read_unformatted_file(self, file_path: str, use_cache: bool = True) -> Any:
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.
use_cache: Whether to use the cache.
reset_cached_result: Whether to reset cache.

Returns:
The file content.
"""
return self._get(ENDPOINTS.CONFIG, file_path, use_cache=use_cache)
return self._get(
ENDPOINTS.CONFIG, file_path, reset_cached_result=reset_cached_result
)
52 changes: 43 additions & 9 deletions tests/unit_tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from time import sleep
from unittest.mock import MagicMock, patch

import pytest
Expand Down Expand Up @@ -45,27 +46,29 @@ def test_read_unformatted_file_reading_cache(mock_request: MagicMock):
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, use_cache=False) == "2nd_read"
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, use_cache=False) == "3rd_read"
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_use_cache_false_without_cache(
def test_read_unformatted_file_reading_reset_cached_result_true_without_cache(
mock_request: MagicMock,
):
"""Test repeated use_cache=False disables cache for each call."""
"""Test repeated reset_cached_result=False disables cache for each call."""
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, use_cache=False) == "1st_read"
assert server.read_unformatted_file(file_path, use_cache=False) == "2nd_read"
assert server.read_unformatted_file(file_path, use_cache=False) == "3rd_read"
assert (
server.read_unformatted_file(file_path, reset_cached_result=True) == "1st_read"
)


@patch("daq_config_server.client.requests.get")
Expand All @@ -79,3 +82,34 @@ def test_read_unformatted_file_reading_not_OK(mock_request: MagicMock):
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=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"
Loading