Skip to content

Commit 9243d66

Browse files
committed
ENH: implementation and tests for take_snapshot_get API
1 parent c0bf122 commit 9243d66

File tree

9 files changed

+168
-51
lines changed

9 files changed

+168
-51
lines changed

container/create_env_file.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
import socket
1+
# import socket
22

33
if __name__ == "__main__":
44
# Get the local IP address
5-
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
6-
s.connect(("8.8.8.8", 80))
7-
ip = s.getsockname()[0]
5+
# s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
6+
# s.connect(("8.8.8.8", 80))
7+
# ip = s.getsockname()[0]
8+
9+
ip = "127.0.0.1"
810

911
with open(".env", "w") as f:
1012
f.write(f"HOST_EXTERNAL_IP_ADDRESS={ip}")

container/save-and-restore.yml

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
services:
22
saveandrestore:
3-
image: ghcr.io/controlsystemstudio/phoebus/service-save-and-restore:master
4-
ports:
5-
- "8080:8080"
3+
image: ghcr.io/controlsystemstudio/phoebus/service-save-and-restore:v5.0.2
4+
network_mode: "host"
5+
# ports:
6+
# - "8080:8080"
67
depends_on:
78
- elasticsearch
89
environment:
910
ELASTICSEARCH_NETWORK_HOST: ${HOST_EXTERNAL_IP_ADDRESS}
1011
EPICS_PVA_ADDR_LIST: ${HOST_EXTERNAL_IP_ADDRESS}
12+
EPICS_PVAS_INTF_ADDR_LIST: ${HOST_EXTERNAL_IP_ADDRESS}
1113
EPICS_PVA_AUTO_ADDR_LIST: "NO"
1214
EPICS_PVA_ENABLE_IPV6: "false"
1315
command: >
@@ -22,8 +24,9 @@ services:
2224

2325
elasticsearch:
2426
image: docker.elastic.co/elasticsearch/elasticsearch:8.11.2
25-
ports:
26-
- "9200:9200"
27+
network_mode: "host"
28+
# ports:
29+
# - "9200:9200"
2730
environment:
2831
discovery.type: single-node
2932
bootstrap.memory_lock: "true"

ioc/sim_ioc.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,16 @@
99

1010
# Create records
1111
AA = builder.aOut("A", initial_value=1.0)
12-
BB = builder.aOut("B", initial_value=1.0)
13-
CC = builder.aOut("C", initial_value=1.0)
14-
DD = builder.aOut("D", initial_value=1.0)
15-
EE = builder.aOut("E", initial_value=1.0)
16-
FF = builder.aOut("F", initial_value=1.0)
17-
GG = builder.aOut("G", initial_value=1.0)
18-
HH = builder.aOut("H", initial_value=1.0)
19-
II = builder.aOut("I", initial_value=1.0)
20-
JJ = builder.aOut("J", initial_value=1.0)
12+
BB = builder.aOut("B", initial_value=2.0)
13+
CC = builder.aOut("C", initial_value=3.0)
14+
DD = builder.aOut("D", initial_value=4.0)
15+
EE = builder.aOut("E", initial_value=5.0)
16+
FF = builder.aOut("F", initial_value=6.0)
17+
GG = builder.aOut("G", initial_value=7.0)
18+
HH = builder.aOut("H", initial_value=8.0)
19+
II = builder.aOut("I", initial_value=9.0)
20+
JJ = builder.aOut("J", initial_value=10.0)
21+
2122

2223
# Get the IOC started
2324
builder.LoadDatabase()
File renamed without changes.

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ dev = [
4141
"pytest-asyncio",
4242
"pre-commit",
4343
"ruff",
44-
# "caproto !=1.2.0",
4544
"softioc",
4645
"pyepics",
4746
]

src/save_and_restore_api/_api_async.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,10 @@ async def nodes_get(self, uniqueIds):
6060

6161
async def node_add(self, parentNodeId, *, name, nodeType, auth=None, **kwargs):
6262
# Reusing docstrings from the threaded version
63-
method, url, params = self._prepare_node_add(
63+
method, url, url_params, params = self._prepare_node_add(
6464
parentNodeId=parentNodeId, name=name, nodeType=nodeType, **kwargs
6565
)
66-
return await self.send_request(method, url, params=params, auth=auth)
66+
return await self.send_request(method, url, url_params=url_params, params=params, auth=auth)
6767

6868
async def node_delete(self, nodeId, *, auth=None):
6969
# Reusing docstrings from the threaded version
@@ -113,34 +113,36 @@ async def config_update(self, *, configurationNode, configurationData=None, auth
113113
# =============================================================================================
114114

115115
async def tags_get(self):
116-
"""
117-
Returns all existing tags.
118-
119-
API: GET /tags
120-
"""
116+
# Reusing docstrings from the threaded version
121117
method, url = self._prepare_tags_get()
122118
return await self.send_request(method, url)
123119

124120
async def tags_add(self, *, uniqueNodeIds, tag, auth=None):
125-
"""
126-
Adds ``tag`` to nodes specified by a list of UIDs ``uniqueNodeIds``. The ``tag``
127-
dictionary must contain the ``name`` key and optionally ``comment`` key.
128-
129-
API: POST /tags
130-
"""
121+
# Reusing docstrings from the threaded version
131122
method, url, params = self._prepare_tags_add(uniqueNodeIds=uniqueNodeIds, tag=tag)
132123
return await self.send_request(method, url, params=params, auth=auth)
133124

134125
async def tags_delete(self, *, uniqueNodeIds, tag, auth=None):
135-
"""
136-
Deletes ``tag`` to nodes specified by a list of UIDs ``uniqueNodeIds``. The ``tag``
137-
dictionary must contain the ``name`` key and optionally ``comment`` key.
138-
139-
API: DELETE /tags
140-
"""
126+
# Reusing docstrings from the threaded version
141127
method, url, params = self._prepare_tags_delete(uniqueNodeIds=uniqueNodeIds, tag=tag)
142128
return await self.send_request(method, url, params=params, auth=auth)
143129

130+
# =============================================================================================
131+
# TAKE-SNAPSHOT-CONTROLLER API METHODS
132+
# =============================================================================================
133+
134+
def take_snapshot_get(self, uniqueNodeId):
135+
# Reusing docstrings from the threaded version
136+
method, url = self._prepare_take_snapshot_get(uniqueNodeId=uniqueNodeId)
137+
return self.send_request(method, url)
138+
139+
def take_snapshot_save(self, uniqueNodeId, *, name=None, comment=None, auth=None):
140+
# Reusing docstrings from the threaded version
141+
method, url, url_params = self._prepare_take_snapshot_save(
142+
uniqueNodeId=uniqueNodeId, name=name, comment=comment
143+
)
144+
return self.send_request(method, url, url_params=url_params, auth=auth)
145+
144146

145147
SaveRestoreAPI.node_get.__doc__ = _SaveRestoreAPI_Threads.node_get.__doc__
146148
SaveRestoreAPI.nodes_get.__doc__ = _SaveRestoreAPI_Threads.nodes_get.__doc__
@@ -155,3 +157,5 @@ async def tags_delete(self, *, uniqueNodeIds, tag, auth=None):
155157
SaveRestoreAPI.tags_get.__doc__ = _SaveRestoreAPI_Threads.tags_get.__doc__
156158
SaveRestoreAPI.tags_add.__doc__ = _SaveRestoreAPI_Threads.tags_add.__doc__
157159
SaveRestoreAPI.tags_delete.__doc__ = _SaveRestoreAPI_Threads.tags_delete.__doc__
160+
SaveRestoreAPI.take_snapshot_get.__doc__ = _SaveRestoreAPI_Threads.take_snapshot_get.__doc__
161+
SaveRestoreAPI.take_snapshot_save.__doc__ = _SaveRestoreAPI_Threads.take_snapshot_save.__doc__

src/save_and_restore_api/_api_base.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,10 @@ def _prepare_node_add(self, *, parentNodeId, name, nodeType, **kwargs):
181181
node_types = ("FOLDER", "CONFIGURATION")
182182
if nodeType not in node_types:
183183
raise self.RequestParameterError(f"Invalid nodeType: {nodeType!r}. Supported types: {node_types}.")
184-
method, url = "PUT", f"/node?parentNodeId={parentNodeId}"
184+
method, url, url_params = "PUT", "/node", {"parentNodeId": parentNodeId}
185185
params = kwargs
186186
params.update({"name": name, "nodeType": nodeType})
187-
return method, url, params
187+
return method, url, url_params, params
188188

189189
def _prepare_node_delete(self, *, nodeId):
190190
method, url = "DELETE", f"/node/{nodeId}"
@@ -241,6 +241,19 @@ def _prepare_tags_delete(self, *, uniqueNodeIds, tag):
241241
params = {"uniqueNodeIds": uniqueNodeIds, "tag": tag}
242242
return method, url, params
243243

244+
# =============================================================================================
245+
# TAKE-SNAPSHOT-CONTROLLER API METHODS
246+
# =============================================================================================
247+
248+
def _prepare_take_snapshot_get(self, *, uniqueNodeId):
249+
method, url = "GET", f"/take-snapshot/{uniqueNodeId}"
250+
return method, url
251+
252+
def _prepare_take_snapshot_save(self, *, uniqueNodeId, name, comment):
253+
method, url = "PUT", "/take-snapshot"
254+
url_params = {"name": name, "comment": comment}
255+
return method, url, url_params
256+
244257
# =============================================================================================
245258

246259
# def create_config(self, parent_node_uid, name, pv_list):

src/save_and_restore_api/_api_threads.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,10 +72,10 @@ def node_add(self, parentNodeId, *, name, nodeType, auth=None, **kwargs):
7272
7373
API: PUT /node?parentNodeId={parentNodeId}
7474
"""
75-
method, url, params = self._prepare_node_add(
75+
method, url, url_params, params = self._prepare_node_add(
7676
parentNodeId=parentNodeId, name=name, nodeType=nodeType, **kwargs
7777
)
78-
return self.send_request(method, url, params=params, auth=auth)
78+
return self.send_request(method, url, url_params=url_params, params=params, auth=auth)
7979

8080
def node_delete(self, nodeId, *, auth=None):
8181
"""
@@ -137,7 +137,7 @@ def config_create(self, parentNodeId, *, configurationNode, configurationData, a
137137
Minimum required fields:
138138
139139
configurationNode = {"name": "test_config"}
140-
configurationData = {"pvList": [{"name": "PV1"}, {"name": "PV2"}]}
140+
configurationData = {"pvList": [{"pvName": "PV1"}, {"pvName": "PV2"}]}
141141
142142
The fields ``uniqueId``, ``nodeType``, ``userName`` in ``configurationNode`` are ignored
143143
and overwritten by the server.
@@ -197,3 +197,30 @@ def tags_delete(self, *, uniqueNodeIds, tag, auth=None):
197197
"""
198198
method, url, params = self._prepare_tags_delete(uniqueNodeIds=uniqueNodeIds, tag=tag)
199199
return self.send_request(method, url, params=params, auth=auth)
200+
201+
# =============================================================================================
202+
# TAKE-SNAPSHOT-CONTROLLER API METHODS
203+
# =============================================================================================
204+
205+
def take_snapshot_get(self, uniqueNodeId):
206+
"""
207+
Reads and returns PV values based on configuration specified by ``uniqueNodeId``.
208+
The API does not create any nodes in the database.
209+
210+
API: GET /take-snapshot/{uniqueNodeId}
211+
"""
212+
method, url = self._prepare_take_snapshot_get(uniqueNodeId=uniqueNodeId)
213+
return self.send_request(method, url)
214+
215+
def take_snapshot_save(self, uniqueNodeId, *, name=None, comment=None, auth=None):
216+
"""
217+
Reads PV values based on configuration specified by ``uniqueNodeId`` and
218+
saves the values in a new snapshot node. The parameter ``name`` specifies
219+
the name of the snapshot node and ``comment`` specifies the node description.
220+
221+
API: PUT /take-snapshot/{uniqueNodeId}
222+
"""
223+
method, url, url_params = self._prepare_take_snapshot_save(
224+
uniqueNodeId=uniqueNodeId, name=name, comment=comment
225+
)
226+
return self.send_request(method, url, url_params=url_params, auth=auth)

tests/test_snapshot_control.py

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from __future__ import annotations
22

3-
# import asyncio
4-
# import pytest
3+
import asyncio
4+
5+
import pytest
56
from epics import caget
67

7-
# from save_and_restore_api import SaveRestoreAPI as SaveRestoreAPI_Threads
8-
# from save_and_restore_api.aio import SaveRestoreAPI as SaveRestoreAPI_Async
8+
from save_and_restore_api import SaveRestoreAPI as SaveRestoreAPI_Threads
9+
from save_and_restore_api.aio import SaveRestoreAPI as SaveRestoreAPI_Async
10+
911
from .common import (
10-
# _is_async,
11-
# _select_auth,
12-
# base_url,
12+
_is_async,
13+
_select_auth,
14+
base_url,
1315
clear_sar, # noqa: F401
14-
# create_root_folder,
16+
create_root_folder,
1517
ioc, # noqa: F401
1618
ioc_pvs,
1719
)
@@ -24,3 +26,69 @@
2426
def test_epics(ioc): # noqa: F811
2527
for pv, value in ioc_pvs.items():
2628
assert caget(pv) == value, f"PV {pv} has value {caget(pv)}, expected {value}"
29+
30+
31+
# fmt: off
32+
@pytest.mark.parametrize("usesetauth", [True, False])
33+
@pytest.mark.parametrize("library", ["THREADS", "ASYNC"])
34+
# fmt: on
35+
def test_take_snapshot_get_01(clear_sar, library, usesetauth): # noqa: F811
36+
"""
37+
Basic tests for the 'take_snapshot_get' API.
38+
"""
39+
root_folder_uid = create_root_folder()
40+
41+
if not _is_async(library):
42+
with SaveRestoreAPI_Threads(base_url=base_url, timeout=10) as SR:
43+
auth = _select_auth(SR=SR, usesetauth=usesetauth)
44+
45+
configurationNode = {"name": "Test Config"}
46+
configurationData = {"pvList": [{"pvName": _} for _ in ioc_pvs.keys()]}
47+
48+
response = SR.config_create(
49+
root_folder_uid,
50+
configurationNode=configurationNode,
51+
configurationData=configurationData,
52+
**auth
53+
)
54+
config_uid = response["configurationNode"]["uniqueId"]
55+
56+
response = SR.take_snapshot_get(config_uid)
57+
58+
snapshot_pv_values = {}
59+
for data in response:
60+
pv_name, val = data["configPv"]["pvName"], data["value"]["value"]
61+
snapshot_pv_values[pv_name] = val
62+
63+
for pv, value in ioc_pvs.items():
64+
assert snapshot_pv_values[pv] == value, \
65+
print(f"pv: {pv}, expected: {value}, got: {snapshot_pv_values[pv]}")
66+
67+
else:
68+
async def testing():
69+
async with SaveRestoreAPI_Async(base_url=base_url, timeout=2) as SR:
70+
auth = _select_auth(SR=SR, usesetauth=usesetauth)
71+
72+
configurationNode = {"name": "Test Config"}
73+
configurationData = {"pvList": [{"pvName": _} for _ in ioc_pvs.keys()]}
74+
75+
response = await SR.config_create(
76+
root_folder_uid,
77+
configurationNode=configurationNode,
78+
configurationData=configurationData,
79+
**auth
80+
)
81+
config_uid = response["configurationNode"]["uniqueId"]
82+
83+
response = await SR.take_snapshot_get(config_uid)
84+
85+
snapshot_pv_values = {}
86+
for data in response:
87+
pv_name, val = data["configPv"]["pvName"], data["value"]["value"]
88+
snapshot_pv_values[pv_name] = val
89+
90+
for pv, value in ioc_pvs.items():
91+
assert snapshot_pv_values[pv] == value, \
92+
print(f"pv: {pv}, expected: {value}, got: {snapshot_pv_values[pv]}")
93+
94+
asyncio.run(testing())

0 commit comments

Comments
 (0)