Skip to content

Commit da4fb1d

Browse files
create main get_configuration endpoint and add tests
1 parent 4069f66 commit da4fb1d

File tree

17 files changed

+120
-441
lines changed

17 files changed

+120
-441
lines changed

Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
2727
COPY --from=build /venv/ /venv/
2828
COPY tests/test_data/beamline_parameters.txt tests/test_data/beamline_parameters.txt
2929
ENV PATH=/venv/bin:$PATH
30-
ARG RUN_APP_IN_DEV_MODE=0
31-
ENV DEV_MODE=${RUN_APP_IN_DEV_MODE}
30+
ARG BEAMLINE="dev"
31+
ENV BEAMLINE=${RUN_APP_IN_DEV_MODE}
3232

3333
# change this entrypoint if it is not the same as the repo
3434
CMD daq-config-server

src/daq_config_server/__main__.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from . import __version__
44

55
try:
6-
import redis # noqa
76
import uvicorn # noqa
87
from fastapi import FastAPI # noqa
98

@@ -18,8 +17,7 @@
1817
def main():
1918
parser = ArgumentParser()
2019
parser.add_argument("-v", "--version", action="version", version=__version__)
21-
parser.add_argument("-d", "--dev", action="store_true")
22-
args = parser.parse_args()
20+
2321
if not server_dependencies_exist:
2422
print(
2523
"To do anything other than print the version and be available for "
@@ -29,7 +27,7 @@ def main():
2927
else:
3028
from .app import main
3129

32-
main(args)
30+
main()
3331

3432

3533
# test with: python -m daq_config_server

src/daq_config_server/app.py

Lines changed: 15 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,14 @@
1-
from os import environ
1+
from pathlib import Path
22

33
import uvicorn
4-
from fastapi import FastAPI, Request, Response, status
4+
from fastapi import FastAPI
55
from fastapi.middleware.cors import CORSMiddleware
6-
from pydantic import BaseModel
7-
from redis import Redis
86

9-
from .beamline_parameters import (
10-
BEAMLINE_PARAMETER_PATHS,
11-
GDABeamlineParameters,
12-
)
13-
from .constants import DATABASE_KEYS, ENDPOINTS
14-
15-
DEV_MODE = bool(int(environ.get("DEV_MODE") or 0))
16-
17-
ROOT_PATH = "/api"
18-
print(f"{DEV_MODE=}")
19-
print(f"{ROOT_PATH=}")
20-
if DEV_MODE:
21-
print("Running in dev mode! not setting root path!")
22-
ROOT_PATH = ""
7+
from daq_config_server.constants import ENDPOINTS
238

249
app = FastAPI(
2510
title="DAQ config server",
26-
description="""For storing and fetching beamline parameters, etc. which are needed
27-
by more than one applicatioon or service""",
28-
root_path=ROOT_PATH,
11+
description="""For reading files stored on /dls_sw from another container""",
2912
)
3013
origins = ["*"]
3114
app.add_middleware(
@@ -36,124 +19,21 @@
3619
allow_headers=["*"],
3720
)
3821

39-
valkey = Redis(host="localhost", port=6379, decode_responses=True)
40-
4122
__all__ = ["main"]
4223

43-
BEAMLINE_PARAM_PATH = ""
44-
BEAMLINE_PARAMS: GDABeamlineParameters | None = None
45-
46-
47-
@app.get(ENDPOINTS.BL_PARAM + "/{param}")
48-
def get_beamline_parameter(param: str):
49-
"""Get a single beamline parameter"""
50-
assert BEAMLINE_PARAMS is not None
51-
return {param: BEAMLINE_PARAMS.params.get(param)}
52-
53-
54-
class ParamList(BaseModel):
55-
param_list: list[str]
56-
57-
58-
@app.get(ENDPOINTS.BL_PARAM)
59-
def get_all_beamline_parameters(param_list_data: ParamList | None = None):
60-
"""Get a dict of all the current beamline parameters."""
61-
assert BEAMLINE_PARAMS is not None
62-
if param_list_data is None:
63-
return BEAMLINE_PARAMS.params
64-
return {k: BEAMLINE_PARAMS.params.get(k) for k in param_list_data.param_list}
65-
66-
67-
@app.get(ENDPOINTS.FEATURE)
68-
def get_feature_flag_list(get_values: bool = False):
69-
"""Get a list of all the current feature flags, or a dict of all the current values
70-
if get_values=true is passed"""
71-
flags = valkey.smembers(DATABASE_KEYS.FEATURE_SET)
72-
if not get_values:
73-
return flags
74-
else:
75-
return {flag: bool(int(valkey.get(flag))) for flag in flags} # type: ignore
76-
77-
78-
@app.get(ENDPOINTS.FEATURE + "/{flag_name}")
79-
def get_feature_flag(flag_name: str, response: Response):
80-
"""Get the value of a feature flag"""
81-
if not valkey.sismember(DATABASE_KEYS.FEATURE_SET, flag_name):
82-
response.status_code = status.HTTP_404_NOT_FOUND
83-
return {"message": f"Feature flag {flag_name} does not exist!"}
84-
else:
85-
ret = int(valkey.get(flag_name)) # type: ignore # We checked if it exists above
86-
return {flag_name: bool(ret)}
87-
88-
89-
@app.post(ENDPOINTS.FEATURE + "/{flag_name}", status_code=status.HTTP_201_CREATED)
90-
def create_feature_flag(flag_name: str, response: Response, value: bool = False):
91-
"""Sets a feature flag, creating it if it doesn't exist. Default to False."""
92-
if valkey.sismember(DATABASE_KEYS.FEATURE_SET, flag_name):
93-
response.status_code = status.HTTP_409_CONFLICT
94-
return {"message": f"Feature flag {flag_name} already exists!"}
95-
else:
96-
valkey.sadd(DATABASE_KEYS.FEATURE_SET, flag_name)
97-
return {"success": valkey.set(flag_name, int(value))}
98-
99-
100-
@app.put(ENDPOINTS.FEATURE + "/{flag_name}")
101-
def set_feature_flag(flag_name: str, value: bool, response: Response):
102-
"""Sets a feature flag, return an error if it doesn't exist."""
103-
if not valkey.sismember(DATABASE_KEYS.FEATURE_SET, flag_name):
104-
response.status_code = status.HTTP_404_NOT_FOUND
105-
return {"message": f"Feature flag {flag_name} does not exist!"}
106-
else:
107-
return {"success": valkey.set(flag_name, int(value))}
108-
109-
110-
@app.delete(ENDPOINTS.FEATURE + "/{flag_name}")
111-
def delete_feature_flag(flag_name: str, response: Response):
112-
"""Delete a feature flag."""
113-
if not valkey.sismember(DATABASE_KEYS.FEATURE_SET, flag_name):
114-
response.status_code = status.HTTP_404_NOT_FOUND
115-
return {"message": f"Feature flag {flag_name} does not exist!"}
116-
else:
117-
valkey.srem(DATABASE_KEYS.FEATURE_SET, flag_name)
118-
return {"success": not valkey.sismember(DATABASE_KEYS.FEATURE_SET, flag_name)}
119-
120-
121-
@app.get(ENDPOINTS.INFO)
122-
def get_info(request: Request):
123-
"""Get some generic information about the request, mostly for debugging"""
124-
return {
125-
"message": "Welcome to daq-config API.",
126-
"root_path": request.scope.get("root_path"),
127-
"request_headers": request.headers,
128-
}
129-
130-
131-
if DEV_MODE:
132-
133-
@app.api_route("/{full_path:path}")
134-
async def catch_all(request: Request, full_path: str):
135-
return {
136-
"message": "resource not found, supplying info for debug",
137-
"root_path": request.scope.get("root_path"),
138-
"path": full_path,
139-
"request_headers": repr(request.headers),
140-
}
141-
142-
143-
def _load_beamline_params():
144-
global BEAMLINE_PARAMS
145-
BEAMLINE_PARAMS = GDABeamlineParameters.from_file(BEAMLINE_PARAM_PATH)
14624

25+
@app.get(ENDPOINTS.CONFIG + "/{file_path:path}")
26+
def get_configuration(file_path: Path):
27+
"""Read a file and return its contents completely unformatted as a string. After
28+
https://github.com/DiamondLightSource/daq-config-server/issues/67, this endpoint
29+
will convert commonly read files to a dictionary format
30+
"""
31+
if not file_path.exists():
32+
raise FileNotFoundError(f"File {file_path} cannot be found")
14733

148-
def _set_beamline_param_path(dev: bool = True):
149-
global BEAMLINE_PARAM_PATH
150-
if dev:
151-
BEAMLINE_PARAM_PATH = "tests/test_data/beamline_parameters.txt"
152-
else:
153-
BEAMLINE_PARAM_PATH = BEAMLINE_PARAMETER_PATHS["i03"]
34+
with file_path.open("r", encoding="utf-8") as f:
35+
return f.read()
15436

15537

156-
def main(args):
157-
_set_beamline_param_path(args.dev)
158-
_load_beamline_params()
38+
def main():
15939
uvicorn.run(app="daq_config_server.app:app", host="0.0.0.0", port=8555)

src/daq_config_server/beamline_parameters.py

Lines changed: 0 additions & 86 deletions
This file was deleted.

src/daq_config_server/client.py

Lines changed: 7 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from logging import Logger, getLogger
2-
from typing import Any, TypeVar
2+
from typing import TypeVar
33

44
import requests
55

@@ -18,65 +18,12 @@ def _get(
1818
self,
1919
endpoint: str,
2020
item: str | None = None,
21-
options: dict[str, str] | None = None,
22-
data: dict[str, Any] | None = None,
2321
):
24-
r = requests.get(
25-
self._url + endpoint + (f"/{item}" if item else ""), options, json=data
26-
)
22+
r = requests.get(self._url + endpoint + (f"/{item}" if item else ""))
2723
return r.json()
2824

29-
def get_beamline_param(self, param: str) -> BlParamDType | None:
30-
return self._get(ENDPOINTS.BL_PARAM, param).get(param)
31-
32-
def get_some_beamline_params(self, params: list[str]) -> dict[str, BlParamDType]:
33-
return self._get(ENDPOINTS.BL_PARAM, data={"param_list": params})
34-
35-
def get_all_beamline_params(self) -> dict[str, BlParamDType]:
36-
return self._get(ENDPOINTS.BL_PARAM)
37-
38-
def get_feature_flag(self, flag_name: str) -> bool | None:
39-
"""Get the specified feature flag; returns None if it does not exist. Will check
40-
that the HTTP response is correct and raise an AssertionError if not."""
41-
return self._get(ENDPOINTS.FEATURE, flag_name).get(flag_name)
42-
43-
def get_all_feature_flags(self) -> dict | None:
44-
"""Get the values for all flags; returns None if it does not exist. Will check
45-
that the HTTP response is correct and raise an AssertionError if not."""
46-
return self._get(ENDPOINTS.FEATURE, options={"get_values": "true"})
47-
48-
def get_feature_flag_list(self) -> list[str]:
49-
"""Get the list of all feature flags. Will check that the HTTP response is
50-
correct and raise an AssertionError if not."""
51-
return self._get(ENDPOINTS.FEATURE)
52-
53-
def best_effort_get_feature_flag(
54-
self, flag_name: str, fallback: T = None
55-
) -> bool | T:
56-
"""Get the specified feature flag, returns fallback value (default None) if it
57-
doesn't exist or if there is a connection error - in the latter case logs to
58-
error."""
59-
try:
60-
assert (
61-
result := self._get(ENDPOINTS.FEATURE, flag_name).get(flag_name)
62-
) is not None
63-
return result
64-
except (AssertionError, OSError):
65-
self._log.error(
66-
"Encountered an error reading from the config service.", exc_info=True
67-
)
68-
return fallback
69-
70-
def best_effort_get_all_feature_flags(self) -> dict[str, bool]:
71-
"""Get all flags, returns an empty dict if there are no flags, or if there
72-
is a connection error - in the latter case logs to error."""
73-
try:
74-
assert (
75-
result := self._get(ENDPOINTS.FEATURE, options={"get_values": "true"})
76-
) is not None
77-
return result
78-
except (AssertionError, OSError):
79-
self._log.error(
80-
"Encountered an error reading from the config service.", exc_info=True
81-
)
82-
return {}
25+
def read_unformatted_file(self, file_path: str) -> BlParamDType | None:
26+
# After https://github.com/DiamondLightSource/daq-config-server/issues/67, we
27+
# can get specific formats, and then have better typing on
28+
# return values
29+
return self._get(ENDPOINTS.CONFIG, file_path)

src/daq_config_server/constants.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,5 @@
22

33

44
@dataclass(frozen=True)
5-
class Endpoints:
6-
FEATURE = "/featureflag"
7-
FEATURE_DISALLOWED_SPECIALS = ["all"]
8-
BL_PARAM = "/beamlineparameters"
9-
INFO = "/info"
10-
11-
12-
@dataclass(frozen=True)
13-
class DatabaseKeys:
14-
FEATURE_SET = "featureflags"
15-
16-
17-
DATABASE_KEYS = DatabaseKeys()
18-
ENDPOINTS = Endpoints()
5+
class ENDPOINTS:
6+
CONFIG = "/config"

0 commit comments

Comments
 (0)