Skip to content

Commit df952c3

Browse files
authored
Merge pull request #4 from dmgav/ci-config
Implementations of threaded and async versions of 'get_node' API with tests
2 parents a2debc8 + 8e39e04 commit df952c3

File tree

12 files changed

+411
-153
lines changed

12 files changed

+411
-153
lines changed

container/save-and-restore.yml

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
# Running elasticsearch in docker
2-
# sudo docker compose -f save-and-restore.yml up -d
3-
# Test:
4-
# curl -X GET "http://localhost:9200/"
5-
6-
# .env file:
7-
#
8-
# HOST_EXTERNAL_IP_ADDRESS=192.168.50.49
9-
101
services:
112
saveandrestore:
123
image: ghcr.io/controlsystemstudio/phoebus/service-save-and-restore:master

container/start-save-and-restore.sh

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,3 @@ set -x
33
python create_env_file.py
44
sudo docker compose -f save-and-restore.yml up -d
55
python wait_for_startup.py
6-
7-
# Wait until the service is started.
8-
#sleep 30

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ Homepage = "https://github.com/dmgav/save-and-restore-api"
3838
dev = [
3939
"pytest >=6",
4040
"pytest-cov >=3",
41+
"pytest-asyncio",
4142
"pre-commit",
4243
"ruff",
4344
]
Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
1-
"""
2-
Copyright (c) 2025 My Name. All rights reserved.
3-
4-
save-and-restore-api: Python package for communication with CS Studio save-and-restore service
5-
"""
6-
71
from __future__ import annotations
82

3+
from ._api_threads import _SaveRestoreAPI_Threads as SaveRestoreAPI
94
from ._version import version as __version__
105

11-
__all__ = ["__version__"]
6+
__all__ = ["__version__", "SaveRestoreAPI"]
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import httpx
2+
3+
from ._api_base import _SaveRestoreAPI_Base
4+
5+
6+
class _SaveRestoreAPI_Async(_SaveRestoreAPI_Base):
7+
def open(self):
8+
self._client = httpx.AsyncClient(base_url=self._base_url, timeout=self._timeout)
9+
10+
async def close(self):
11+
await self._client.aclose()
12+
self._client = None
13+
14+
async def send_request(
15+
self, method, url, *, params=None, url_params=None, headers=None, data=None, timeout=None, auth=None
16+
):
17+
try:
18+
client_response = None
19+
kwargs = self._prepare_request(
20+
method=method,
21+
params=params,
22+
url_params=url_params,
23+
headers=headers,
24+
data=data,
25+
timeout=timeout,
26+
auth=auth,
27+
)
28+
client_response = await self._client.request(method, url, **kwargs)
29+
response = self._process_response(client_response=client_response)
30+
except Exception:
31+
response = self._process_comm_exception(method=method, params=params, client_response=client_response)
32+
33+
return response
34+
35+
async def login(self, *, username=None, password=None):
36+
method, url, params = self._prepare_login(username=username, password=password)
37+
await self.send_request(method, url, params=params)
38+
39+
async def get_node(self, node_uid):
40+
method, url = self._prepare_get_node(node_uid=node_uid)
41+
return await self.send_request(method, url)
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
# import getpass
2+
import pprint
3+
from collections.abc import Mapping
4+
5+
import httpx
6+
7+
8+
class RequestParameterError(Exception): ...
9+
10+
11+
class HTTPRequestError(httpx.RequestError): ...
12+
13+
14+
class HTTPClientError(httpx.HTTPStatusError): ...
15+
16+
17+
class HTTPServerError(httpx.HTTPStatusError): ...
18+
19+
20+
class RequestTimeoutError(TimeoutError):
21+
def __init__(self, msg, request):
22+
msg = f"Request timeout: {msg}"
23+
self.request = request
24+
super().__init__(msg)
25+
26+
27+
class RequestFailedError(Exception):
28+
def __init__(self, request, response):
29+
msg = response.get("msg", "") if isinstance(response, Mapping) else str(response)
30+
msg = msg or "(no error message)"
31+
msg = f"Request failed: {msg}"
32+
self.request = request
33+
self.response = response
34+
super().__init__(msg)
35+
36+
37+
class _SaveRestoreAPI_Base:
38+
RequestParameterError = RequestParameterError
39+
RequestTimeoutError = RequestTimeoutError
40+
RequestFailedError = RequestFailedError
41+
HTTPRequestError = HTTPRequestError
42+
HTTPClientError = HTTPClientError
43+
HTTPServerError = HTTPServerError
44+
45+
def __init__(self, *, base_url, timeout, request_fail_exceptions=True):
46+
self._base_url = base_url
47+
self._timeout = timeout
48+
self._client = None
49+
self._root_node_uid = "44bef5de-e8e6-4014-af37-b8f6c8a939a2"
50+
self._auth = None
51+
52+
@property
53+
def ROOT_NODE_UID(self):
54+
return self._root_node_uid
55+
56+
@staticmethod
57+
def gen_auth(username, password):
58+
return httpx.BasicAuth(username=username, password=password)
59+
60+
def set_auth(self, *, username, password):
61+
self._auth = self.gen_auth(username=username, password=password)
62+
63+
# def set_username_password(self, username=None, password=None):
64+
# if not isinstance(username, str):
65+
# print("Username: ", end="")
66+
# username = input()
67+
# if not isinstance(password, str):
68+
# password = getpass.getpass()
69+
70+
# self._username = username
71+
# self._password = password
72+
73+
# # TODO: rewrite the logic in this function
74+
# def _check_response(self, *, request, response):
75+
# """
76+
# Check if response is a dictionary and has ``"success": True``. Raise an exception
77+
# if the request is considered failed and exceptions are allowed. If response is
78+
# a dictionary and contains no ``"success"``, then it is considered successful.
79+
# """
80+
# if self._request_fail_exceptions:
81+
# # The response must be a list or a dictionary. If the response is a dictionary
82+
# # and the key 'success': False, then consider the request failed. If there
83+
# # is not 'success' key, then consider the request successful.
84+
# is_iterable = isinstance(response, Iterable) and not isinstance(response, str)
85+
# is_mapping = isinstance(response, Mapping)
86+
# if not any([is_iterable, is_mapping]) or (is_mapping and not response.get("success", True)):
87+
# raise self.RequestFailedError(request, response)
88+
89+
def _process_response(self, *, client_response):
90+
client_response.raise_for_status()
91+
response = client_response.json()
92+
return response
93+
94+
def _process_comm_exception(self, *, method, params, client_response):
95+
"""
96+
The function must be called from ``except`` block and returns response with an error message
97+
or raises an exception.
98+
"""
99+
try:
100+
raise
101+
102+
except httpx.TimeoutException as ex:
103+
raise self.RequestTimeoutError(ex, {"method": method, "params": params}) from ex
104+
105+
except httpx.RequestError as ex:
106+
raise self.HTTPRequestError(f"HTTP request error: {ex}") from ex
107+
108+
except httpx.HTTPStatusError as exc:
109+
common_params = {"request": exc.request, "response": exc.response}
110+
if client_response and (client_response.status_code < 500):
111+
# Include more detail that httpx does by default.
112+
message = (
113+
f"{exc.response.status_code}: "
114+
f"{exc.response.json()['detail'] if client_response.content else ''} "
115+
f"{exc.request.url}"
116+
)
117+
raise self.HTTPClientError(message, **common_params) from exc
118+
else:
119+
raise self.HTTPServerError(exc, **common_params) from exc
120+
121+
def _prepare_request(
122+
self, *, method, params=None, url_params=None, headers=None, data=None, timeout=None, auth=None
123+
):
124+
kwargs = {}
125+
if params:
126+
kwargs.update({"json": params})
127+
if url_params:
128+
kwargs.update({"params": url_params})
129+
if headers:
130+
kwargs.update({"headers": headers})
131+
if data:
132+
kwargs.update({"data": data})
133+
if timeout is not None:
134+
kwargs.update({"timeout": self._adjust_timeout(timeout)})
135+
if method != "GET":
136+
auth = auth or self._auth
137+
if auth is not None:
138+
kwargs.update({"auth": auth})
139+
return kwargs
140+
141+
def _prepare_login(self, *, username=None, password=None):
142+
method, url = "POST", "/login"
143+
params = {"username": username, "password": password}
144+
return method, url, params
145+
146+
def _prepare_get_node(self, *, node_uid):
147+
method, url = "GET", f"/node/{node_uid}"
148+
return method, url
149+
150+
def get_children(self, node_uid):
151+
return self.send_request("GET", f"/node/{node_uid}/children")
152+
153+
def create_config(self, parent_node_uid, name, pv_list):
154+
config_dict = {
155+
"configurationNode": {
156+
"name": name,
157+
"nodeType": "CONFIGURATION",
158+
"userName": self._username,
159+
},
160+
"configurationData": {
161+
"pvList": pv_list,
162+
},
163+
}
164+
print(f"config_dict=\n{pprint.pformat(config_dict)}")
165+
return self.send_request("PUT", f"/config?parentNodeId={parent_node_uid}", json=config_dict)
166+
167+
def update_config(self, node_uid, name, pv_list):
168+
config_dict = {
169+
"configurationNode": {
170+
"name": name,
171+
"nodeType": "CONFIGURATION",
172+
"userName": self._username,
173+
"uniqueId": node_uid,
174+
},
175+
"configurationData": {
176+
"pvList": pv_list,
177+
},
178+
}
179+
print(f"config_dict=\n{pprint.pformat(config_dict)}")
180+
# return self.send_request("POST", f"/config/{node_uid}", json=config_dict)
181+
return self.send_request("POST", "/config", json=config_dict)
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import httpx
2+
3+
from ._api_base import _SaveRestoreAPI_Base
4+
5+
6+
class _SaveRestoreAPI_Threads(_SaveRestoreAPI_Base):
7+
def open(self):
8+
self._client = httpx.Client(base_url=self._base_url, timeout=self._timeout)
9+
10+
def close(self):
11+
self._client.close()
12+
self._client = None
13+
14+
def send_request(
15+
self, method, url, *, params=None, url_params=None, headers=None, data=None, timeout=None, auth=None
16+
):
17+
try:
18+
client_response = None
19+
kwargs = self._prepare_request(
20+
method=method,
21+
params=params,
22+
url_params=url_params,
23+
headers=headers,
24+
data=data,
25+
timeout=timeout,
26+
auth=auth,
27+
)
28+
client_response = self._client.request(method, url, **kwargs)
29+
response = self._process_response(client_response=client_response)
30+
except Exception:
31+
response = self._process_comm_exception(method=method, params=params, client_response=client_response)
32+
33+
return response
34+
35+
def login(self, *, username=None, password=None):
36+
method, url, params = self._prepare_login(username=username, password=password)
37+
self.send_request(method, url, params=params)
38+
39+
def get_node(self, node_uid):
40+
method, url = self._prepare_get_node(node_uid=node_uid)
41+
return self.send_request(method, url)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from __future__ import annotations
2+
3+
from .._api_async import _SaveRestoreAPI_Async as SaveRestoreAPI
4+
from .._version import version as __version__
5+
6+
__all__ = ["__version__", "SaveRestoreAPI"]

0 commit comments

Comments
 (0)