Skip to content

Commit 7c4eb30

Browse files
committed
Merge branch 'develop'
2 parents e389d11 + 78957a4 commit 7c4eb30

5 files changed

Lines changed: 173 additions & 7 deletions

File tree

CHANGES.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@ Changelog
22
=========
33

44

5+
.. _changes-1_6_0:
6+
7+
1.6.0 (2024-12-06)
8+
~~~~~~~~~~~~~~~~~~
9+
10+
New features
11+
------------
12+
13+
+ `#166`_: Add new custom IDS method
14+
:meth:`icat.client.Client.restoreData`.
15+
16+
.. _#166: https://github.com/icatproject/python-icat/pull/166
17+
18+
519
.. _changes-1_5_1:
620

721
1.5.1 (2024-10-25)

doc/src/client.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,4 +154,6 @@ manages the interaction with an ICAT service as a client.
154154

155155
.. automethod:: deleteData
156156

157+
.. automethod:: restoreData
158+
157159
.. _ICAT SOAP Manual: https://repo.icatproject.org/site/icat/server/4.10.0/soap.html

src/icat/client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,5 +1000,40 @@ def deleteData(self, objs):
10001000
objs = DataSelection(objs)
10011001
self.ids.delete(objs)
10021002

1003+
def restoreData(self, objs):
1004+
"""Request IDS to restore data.
1005+
1006+
Check the status of the data, request a restore if needed and
1007+
wait for the restore to complete.
1008+
1009+
:param objs: either a dict having some of the keys
1010+
`investigationIds`, `datasetIds`, and `datafileIds`
1011+
with a list of object ids as value respectively, or a list
1012+
of entity objects, or a data selection.
1013+
:type objs: :class:`dict`, :class:`list` of
1014+
:class:`icat.entity.Entity`, or
1015+
:class:`icat.ids.DataSelection`
1016+
1017+
.. versionadded:: 1.6.0
1018+
"""
1019+
if not self.ids:
1020+
raise RuntimeError("no IDS.")
1021+
if not isinstance(objs, DataSelection):
1022+
objs = DataSelection(objs)
1023+
while True:
1024+
self.autoRefresh()
1025+
status = self.ids.getStatus(objs)
1026+
if status == "ONLINE":
1027+
break
1028+
elif status == "RESTORING":
1029+
pass
1030+
elif status == "ARCHIVED":
1031+
self.ids.restore(objs)
1032+
else:
1033+
# Should never happen
1034+
raise IDSResponseError("unexpected response from "
1035+
"IDS getStatus() call: %s" % status)
1036+
time.sleep(30)
1037+
10031038

10041039
atexit.register(Client.cleanupall)

tests/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,28 @@
4848
logging.getLogger('suds.client').setLevel(logging.CRITICAL)
4949
logging.getLogger('suds').setLevel(logging.ERROR)
5050

51+
_skip_slow = True
5152
testdir = Path(__file__).resolve().parent
5253
testdatadir = testdir / "data"
5354

55+
def pytest_addoption(parser):
56+
parser.addoption("--no-skip-slow", action="store_true", default=False,
57+
help="do not skip slow tests.")
58+
59+
def pytest_configure(config):
60+
global _skip_slow
61+
_skip_slow = not config.getoption("--no-skip-slow")
62+
config.addinivalue_line("markers", "slow: mark a test as slow, "
63+
"the test will be skipped unless --no-skip-slow "
64+
"is set on the command line")
65+
66+
def pytest_runtest_setup(item):
67+
"""Skip slow tests by default.
68+
"""
69+
marker = item.get_closest_marker("slow")
70+
if marker is not None and _skip_slow:
71+
pytest.skip("skip slow test")
72+
5473
def _skip(reason):
5574
if Version(pytest.__version__) >= '3.3.0':
5675
pytest.skip(reason, allow_module_level=True)

tests/test_06_ids.py

Lines changed: 103 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,31 @@
33

44
import datetime
55
import filecmp
6+
import logging
67
import time
78
import zipfile
89
import pytest
910
import icat
11+
import icat.client
1012
import icat.config
11-
from icat.ids import DataSelection
13+
from icat.ids import IDSClient, DataSelection
1214
from icat.query import Query
1315
from conftest import DummyDatafile, UtcTimezone
1416
from conftest import getConfig, tmpSessionId, tmpClient
1517

18+
logger = logging.getLogger(__name__)
1619

17-
@pytest.fixture(scope="module")
18-
def client(setupicat):
19-
client, conf = getConfig(ids="mandatory")
20-
client.login(conf.auth, conf.credentials)
21-
yield client
22-
query = "SELECT df FROM Datafile df WHERE df.location IS NOT NULL"
20+
GiB = 1073741824
21+
22+
class LoggingIDSClient(IDSClient):
23+
"""Modified version of IDSClient that logs some calls.
24+
"""
25+
def getStatus(self, selection):
26+
status = super().getStatus(selection)
27+
logger.debug("getStatus(%s): %s", selection, status, stacklevel=2)
28+
return status
29+
30+
def _delete_datafiles(client, query):
2331
while True:
2432
try:
2533
client.deleteData(client.search(query))
@@ -28,6 +36,45 @@ def client(setupicat):
2836
else:
2937
break
3038

39+
@pytest.fixture(scope="module")
40+
def cleanup(setupicat):
41+
client, conf = getConfig(confSection="root", ids="mandatory")
42+
client.login(conf.auth, conf.credentials)
43+
yield
44+
query = "SELECT df FROM Datafile df WHERE df.location IS NOT NULL"
45+
_delete_datafiles(client, query)
46+
47+
@pytest.fixture(scope="function")
48+
def client(monkeypatch, cleanup):
49+
monkeypatch.setattr(icat.client, "IDSClient", LoggingIDSClient)
50+
client, conf = getConfig(ids="mandatory")
51+
client.login(conf.auth, conf.credentials)
52+
yield client
53+
54+
@pytest.fixture(scope="function")
55+
def dataset(client, cleanup_objs):
56+
"""A dataset to be used in the test.
57+
58+
The dataset will be eventually be deleted after the test.
59+
"""
60+
inv = client.assertedSearch(Query(client, "Investigation", conditions={
61+
"name": "= '10100601-ST'",
62+
}))[0]
63+
dstype = client.assertedSearch(Query(client, "DatasetType", conditions={
64+
"name": "= 'raw'",
65+
}))[0]
66+
dataset = client.new("Dataset",
67+
name="e208343", complete=False,
68+
investigation=inv, type=dstype)
69+
dataset.create()
70+
cleanup_objs.append(dataset)
71+
yield dataset
72+
query = Query(client, "Datafile", conditions={
73+
"dataset.id": "= %d" % dataset.id,
74+
"location": "IS NOT NULL",
75+
})
76+
_delete_datafiles(client, query)
77+
3178

3279
# ============================ testdata ============================
3380

@@ -392,6 +439,55 @@ def test_restore(client, case):
392439
# outcome of the restore() call.
393440
print("Status of dataset %s is now %s" % (case['dsname'], status))
394441

442+
@pytest.mark.parametrize(("case"), markeddatasets)
443+
def test_restoreDataCall(client, case):
444+
"""Test the high level call restoreData().
445+
446+
This is essentially a no-op as the dataset in question will
447+
already be ONLINE. It only tests that the call does not throw an
448+
error.
449+
"""
450+
dataset = getDataset(client, case)
451+
client.restoreData([dataset])
452+
status = client.ids.getStatus(DataSelection([dataset]))
453+
assert status == "ONLINE"
454+
455+
@pytest.mark.parametrize(("case"), markeddatasets)
456+
def test_restoreDataCallSelection(client, case):
457+
"""Test the high level call restoreData().
458+
459+
Same as last test, but now pass a DataSelection as argument.
460+
"""
461+
selection = DataSelection([getDataset(client, case)])
462+
client.restoreData(selection)
463+
status = client.ids.getStatus(selection)
464+
assert status == "ONLINE"
465+
466+
@pytest.mark.slow
467+
def test_restoreData(tmpdirsec, client, dataset):
468+
"""Test restoring data with the high level call restoreData().
469+
470+
This test archives a dataset and calls restoreData() to restore it
471+
again. The size of the dataset is large enough so that restoring
472+
takes some time, so that we actually can observe the call to wait
473+
until the restoring is finished. As a result, the test is rather
474+
slow. It is marked as such and thus disabled by default.
475+
"""
476+
if not client.ids.isTwoLevel():
477+
pytest.skip("This IDS does not use two levels of data storage")
478+
f = DummyDatafile(tmpdirsec, "e208343.nxs", GiB)
479+
query = Query(client, "DatafileFormat", conditions={
480+
"name": "= 'NeXus'",
481+
})
482+
datafileformat = client.assertedSearch(query)[0]
483+
datafile = client.new("Datafile", name=f.fname.name,
484+
dataset=dataset, datafileFormat=datafileformat)
485+
client.putData(f.fname, datafile)
486+
client.ids.archive(DataSelection([dataset]))
487+
client.restoreData([dataset])
488+
status = client.ids.getStatus(DataSelection([dataset]))
489+
assert status == "ONLINE"
490+
395491
@pytest.mark.parametrize(("case"), markeddatasets)
396492
def test_reset(client, case):
397493
"""Call reset() on a dataset.

0 commit comments

Comments
 (0)