-
Notifications
You must be signed in to change notification settings - Fork 1
Get typing support on client and do better exception handling #88
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
Changes from all commits
ffaebdf
3a397db
ad4e02b
9e2b3a5
9735654
2b6ae4e
d520103
0bbfc72
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,22 +1,33 @@ | ||
| import operator | ||
| from enum import StrEnum | ||
| from collections import defaultdict | ||
| from logging import Logger, getLogger | ||
| from pathlib import Path | ||
| from typing import Any | ||
| from typing import Any, TypeVar | ||
|
|
||
| import requests | ||
| from cachetools import TTLCache, cachedmethod | ||
| from pydantic import TypeAdapter | ||
| from requests import Response | ||
| from requests.exceptions import HTTPError | ||
|
|
||
| from daq_config_server.app import ValidAcceptHeaders | ||
|
|
||
| from .constants import ENDPOINTS | ||
|
|
||
| T = TypeVar("T", str, bytes, dict[Any, Any]) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Potential future change: make |
||
|
|
||
| class RequestedResponseFormats(StrEnum): | ||
| DICT = ValidAcceptHeaders.JSON # Convert to dict using Response.json() | ||
| DECODED_STRING = ValidAcceptHeaders.PLAIN_TEXT # Use utf-8 decoding in response | ||
| RAW_BYTE_STRING = ValidAcceptHeaders.RAW_BYTES # Use raw bytes in response | ||
|
|
||
| return_type_to_mime_type: dict[type, ValidAcceptHeaders] = defaultdict( | ||
| lambda: ValidAcceptHeaders.PLAIN_TEXT, | ||
| { | ||
| dict[Any, Any]: ValidAcceptHeaders.JSON, | ||
| str: ValidAcceptHeaders.PLAIN_TEXT, | ||
| bytes: ValidAcceptHeaders.RAW_BYTES, | ||
| }, | ||
| ) | ||
|
|
||
|
|
||
| class TypeConversionException(Exception): ... | ||
|
|
||
|
|
||
| class ConfigServer: | ||
|
|
@@ -38,42 +49,51 @@ def __init__( | |
| """ | ||
| self._url = url.rstrip("/") | ||
| self._log = log if log else getLogger("daq_config_server.client") | ||
| self._cache: TTLCache[tuple[str, str, Path], str] = TTLCache( | ||
| self._cache: TTLCache[tuple[str, str, Path], Response] = TTLCache( | ||
| maxsize=cache_size, ttl=cache_lifetime_s | ||
| ) | ||
|
|
||
| @cachedmethod(cache=operator.attrgetter("_cache")) | ||
| def _cached_get( | ||
| self, | ||
| endpoint: str, | ||
| accept_header: str, | ||
| accept_header: ValidAcceptHeaders, | ||
| file_path: Path, | ||
| ) -> Response: | ||
| """ | ||
| Get data from the config server and cache it. | ||
|
|
||
| Args: | ||
| endpoint: API endpoint. | ||
| accept_header: Accept header MIME type | ||
| file_path: absolute path to the file which will be read | ||
|
|
||
| Returns: | ||
| The response data. | ||
| """ | ||
|
|
||
| request_url = self._url + endpoint + (f"/{file_path}") | ||
| r = requests.get(request_url, headers={"Accept": accept_header}) | ||
| # Intercept http exceptions from server so that the client | ||
| # can include the response `detail` sent by the server | ||
| try: | ||
| request_url = self._url + endpoint + (f"/{file_path}") | ||
| r = requests.get(request_url, headers={"Accept": accept_header}) | ||
| r.raise_for_status() | ||
| self._log.debug(f"Cache set for {request_url}.") | ||
| return r | ||
| except requests.exceptions.HTTPError as e: | ||
| self._log.error(f"HTTP error: {e}") | ||
| raise | ||
| except requests.exceptions.HTTPError as err: | ||
| try: | ||
| error_detail = r.json().get("detail") | ||
| self._log.error(error_detail) | ||
| raise HTTPError(error_detail) from err | ||
| except ValueError: | ||
| self._log.error("Response raised HTTP error but no details provided") | ||
| raise HTTPError from err | ||
|
|
||
| self._log.debug(f"Cache set for {request_url}.") | ||
| return r | ||
|
|
||
| def _get( | ||
| self, | ||
| endpoint: str, | ||
| accept_header: str, | ||
| accept_header: ValidAcceptHeaders, | ||
| file_path: Path, | ||
| reset_cached_result: bool = False, | ||
| ): | ||
|
|
@@ -82,7 +102,11 @@ def _get( | |
| the content-type response header to format the return value. | ||
| If data parsing fails, return the response contents in bytes | ||
| """ | ||
| if (endpoint, accept_header, file_path) in self._cache and reset_cached_result: | ||
| if ( | ||
| endpoint, | ||
| accept_header, | ||
| file_path, | ||
| ) in self._cache and reset_cached_result: | ||
| del self._cache[(endpoint, accept_header, file_path)] | ||
| r = self._cached_get(endpoint, accept_header, file_path) | ||
|
|
||
|
|
@@ -103,40 +127,42 @@ def _get( | |
| case _: | ||
| content = r.content | ||
| except Exception as e: | ||
| self._log.warning( | ||
| f"Failed trying to convert to content-type {content_type} due to\ | ||
| exception {e} \nReturning as bytes instead" | ||
| ) | ||
| content = r.content | ||
| raise TypeConversionException( | ||
| f"Failed trying to convert to content-type {content_type}." | ||
| ) from e | ||
|
|
||
| return content | ||
|
|
||
| def get_file_contents( | ||
| self, | ||
| file_path: Path, | ||
| requested_response_format: RequestedResponseFormats = ( | ||
| RequestedResponseFormats.DECODED_STRING | ||
| ), | ||
| file_path: Path | str, | ||
| desired_return_type: type[T] = str, | ||
| reset_cached_result: bool = False, | ||
| ) -> Any: | ||
| ) -> T: | ||
| """ | ||
| Get contents of a file from the config server in the format specified. | ||
| If data parsing fails, contents will return as raw bytes. Optionally look | ||
| for cached result before making request. | ||
| Optionally look for cached result before making request. | ||
|
|
||
| Current supported return types are: str, bytes, dict[str, str]. This option will | ||
| determine how the server attempts to decode the file | ||
|
|
||
| Args: | ||
| file_path: Path to the file. | ||
olliesilvester marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| requested_response_format: Specify how to parse the response. | ||
| reset_cached_result: If true, make a request and store response in cache, | ||
| desired_return_type: If true, make a request and store response in cache, | ||
| otherwise look for cached response before making | ||
| new request | ||
| Returns: | ||
| The file contents, in the format specified. | ||
| """ | ||
|
|
||
| return self._get( | ||
| ENDPOINTS.CONFIG, | ||
| requested_response_format, | ||
| file_path, | ||
| reset_cached_result=reset_cached_result, | ||
| file_path = Path(file_path) | ||
| accept_header = return_type_to_mime_type[desired_return_type] | ||
|
|
||
| return TypeAdapter(desired_return_type).validate_python( # type: ignore - to allow any dict | ||
| self._get( | ||
| ENDPOINTS.CONFIG, | ||
| accept_header, | ||
| file_path, | ||
| reset_cached_result=reset_cached_result, | ||
| ) | ||
| ) | ||
This file was deleted.
Uh oh!
There was an error while loading. Please reload this page.