hpcflow-new2 0.2.0a158__py3-none-any.whl → 0.2.0a160__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. hpcflow/_version.py +1 -1
  2. hpcflow/app.py +0 -3
  3. hpcflow/sdk/__init__.py +2 -0
  4. hpcflow/sdk/app.py +91 -18
  5. hpcflow/sdk/cli.py +18 -0
  6. hpcflow/sdk/cli_common.py +16 -0
  7. hpcflow/sdk/config/config.py +0 -4
  8. hpcflow/sdk/core/actions.py +20 -7
  9. hpcflow/sdk/core/command_files.py +4 -4
  10. hpcflow/sdk/core/element.py +15 -16
  11. hpcflow/sdk/core/rule.py +2 -0
  12. hpcflow/sdk/core/run_dir_files.py +63 -0
  13. hpcflow/sdk/core/task.py +34 -35
  14. hpcflow/sdk/core/utils.py +37 -15
  15. hpcflow/sdk/core/workflow.py +147 -49
  16. hpcflow/sdk/data/config_schema.yaml +0 -6
  17. hpcflow/sdk/demo/cli.py +12 -0
  18. hpcflow/sdk/log.py +2 -2
  19. hpcflow/sdk/persistence/base.py +142 -12
  20. hpcflow/sdk/persistence/json.py +84 -63
  21. hpcflow/sdk/persistence/pending.py +21 -7
  22. hpcflow/sdk/persistence/utils.py +2 -1
  23. hpcflow/sdk/persistence/zarr.py +143 -108
  24. hpcflow/sdk/runtime.py +0 -12
  25. hpcflow/sdk/submission/jobscript.py +25 -4
  26. hpcflow/sdk/submission/schedulers/sge.py +3 -0
  27. hpcflow/sdk/submission/schedulers/slurm.py +3 -0
  28. hpcflow/sdk/submission/shells/bash.py +2 -2
  29. hpcflow/sdk/submission/shells/powershell.py +2 -2
  30. hpcflow/sdk/submission/submission.py +24 -7
  31. hpcflow/tests/scripts/test_main_scripts.py +40 -0
  32. hpcflow/tests/unit/test_utils.py +28 -0
  33. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/METADATA +1 -2
  34. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/RECORD +36 -35
  35. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/WHEEL +0 -0
  36. {hpcflow_new2-0.2.0a158.dist-info → hpcflow_new2-0.2.0a160.dist-info}/entry_points.txt +0 -0
@@ -9,10 +9,11 @@ from datetime import datetime, timezone
9
9
  import enum
10
10
  import os
11
11
  from pathlib import Path
12
+ import re
12
13
  import shutil
13
14
  import socket
14
15
  import time
15
- from typing import Any, Dict, Iterable, List, Optional, Tuple, TypeVar, Union
16
+ from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, TypeVar, Union
16
17
 
17
18
  from hpcflow.sdk.core.utils import (
18
19
  flatten,
@@ -107,6 +108,7 @@ class StoreTask:
107
108
  """
108
109
  return cls(is_pending=False, **task_dat)
109
110
 
111
+ @TimeIt.decorator
110
112
  def append_element_IDs(self: AnySTask, pend_IDs: List[int]) -> AnySTask:
111
113
  """Return a copy, with additional element IDs."""
112
114
  elem_IDs = self.element_IDs[:] + pend_IDs
@@ -164,6 +166,7 @@ class StoreElement:
164
166
  "iterations": iters,
165
167
  }
166
168
 
169
+ @TimeIt.decorator
167
170
  def append_iteration_IDs(self: AnySElement, pend_IDs: List[int]) -> AnySElement:
168
171
  """Return a copy, with additional iteration IDs."""
169
172
  iter_IDs = self.iteration_IDs[:] + pend_IDs
@@ -234,6 +237,7 @@ class StoreElementIter:
234
237
  "loop_idx": self.loop_idx,
235
238
  }
236
239
 
240
+ @TimeIt.decorator
237
241
  def append_EAR_IDs(
238
242
  self: AnySElementIter, pend_IDs: Dict[int, List[int]]
239
243
  ) -> AnySElementIter:
@@ -256,6 +260,7 @@ class StoreElementIter:
256
260
  EARs_initialised=self.EARs_initialised,
257
261
  )
258
262
 
263
+ @TimeIt.decorator
259
264
  def update_loop_idx(
260
265
  self: AnySElementIter, loop_idx: Dict[str, int]
261
266
  ) -> AnySElementIter:
@@ -273,6 +278,7 @@ class StoreElementIter:
273
278
  loop_idx=loop_idx_new,
274
279
  )
275
280
 
281
+ @TimeIt.decorator
276
282
  def set_EARs_initialised(self: AnySElementIter) -> AnySElementIter:
277
283
  """Return a copy with `EARs_initialised` set to `True`."""
278
284
  return self.__class__(
@@ -378,6 +384,7 @@ class StoreEAR:
378
384
  "run_hostname": self.run_hostname,
379
385
  }
380
386
 
387
+ @TimeIt.decorator
381
388
  def update(
382
389
  self,
383
390
  submission_idx: Optional[int] = None,
@@ -655,6 +662,10 @@ class PersistentStore(ABC):
655
662
  self._resources_in_use = set()
656
663
  self._in_batch_mode = False
657
664
 
665
+ self._use_cache = False
666
+ self._cache = None
667
+ self._reset_cache()
668
+
658
669
  @property
659
670
  def logger(self):
660
671
  return self.app.persistence_logger
@@ -672,6 +683,70 @@ class PersistentStore(ABC):
672
683
  """Does this store support workflow submission?"""
673
684
  return self.fs.__class__.__name__ == "LocalFileSystem"
674
685
 
686
+ @property
687
+ def use_cache(self):
688
+ return self._use_cache
689
+
690
+ @property
691
+ def task_cache(self):
692
+ """Cache for persistent tasks."""
693
+ return self._cache["tasks"]
694
+
695
+ @property
696
+ def element_cache(self):
697
+ """Cache for persistent elements."""
698
+ return self._cache["elements"]
699
+
700
+ @property
701
+ def element_iter_cache(self):
702
+ """Cache for persistent element iterations."""
703
+ return self._cache["element_iters"]
704
+
705
+ @property
706
+ def EAR_cache(self):
707
+ """Cache for persistent EARs."""
708
+ return self._cache["EARs"]
709
+
710
+ @property
711
+ def num_tasks_cache(self):
712
+ """Cache for number of persistent tasks."""
713
+ return self._cache["num_tasks"]
714
+
715
+ @property
716
+ def param_sources_cache(self):
717
+ """Cache for persistent parameter sources."""
718
+ return self._cache["param_sources"]
719
+
720
+ @property
721
+ def parameter_cache(self):
722
+ """Cache for persistent parameters."""
723
+ return self._cache["parameters"]
724
+
725
+ @num_tasks_cache.setter
726
+ def num_tasks_cache(self, value):
727
+ self._cache["num_tasks"] = value
728
+
729
+ def _reset_cache(self):
730
+ self._cache = {
731
+ "tasks": {},
732
+ "elements": {},
733
+ "element_iters": {},
734
+ "EARs": {},
735
+ "param_sources": {},
736
+ "num_tasks": None,
737
+ "parameters": {},
738
+ }
739
+
740
+ @contextlib.contextmanager
741
+ def cache_ctx(self):
742
+ """Context manager for using the persistent element/iteration/run cache."""
743
+ self._use_cache = True
744
+ try:
745
+ yield
746
+ finally:
747
+ self._use_cache = False
748
+ self._reset_cache()
749
+
675
750
  @staticmethod
676
751
  def prepare_test_store_from_spec(task_spec):
677
752
  """Generate a valid store from a specification in terms of nested
@@ -872,6 +947,7 @@ class PersistentStore(ABC):
872
947
  if save:
873
948
  self.save()
874
949
 
950
+ @TimeIt.decorator
875
951
  def add_submission(self, sub_idx: int, sub_js: Dict, save: bool = True):
876
952
  """Add a new submission."""
877
953
  self.logger.debug(f"Adding store submission.")
@@ -964,6 +1040,7 @@ class PersistentStore(ABC):
964
1040
  if save:
965
1041
  self.save()
966
1042
 
1043
+ @TimeIt.decorator
967
1044
  def set_EAR_submission_index(
968
1045
  self, EAR_ID: int, sub_idx: int, save: bool = True
969
1046
  ) -> None:
@@ -973,9 +1050,7 @@ class PersistentStore(ABC):
973
1050
 
974
1051
  def set_EAR_start(self, EAR_ID: int, save: bool = True) -> datetime:
975
1052
  dt = datetime.utcnow()
976
- snapshot = JSONLikeDirSnapShot()
977
- snapshot.take(".")
978
- ss_js = snapshot.to_json_like()
1053
+ ss_js = self.app.RunDirAppFiles.take_snapshot()
979
1054
  run_hostname = socket.gethostname()
980
1055
  self._pending.set_EAR_starts[EAR_ID] = (dt, ss_js, run_hostname)
981
1056
  if save:
@@ -987,9 +1062,7 @@ class PersistentStore(ABC):
987
1062
  ) -> datetime:
988
1063
  # TODO: save output files
989
1064
  dt = datetime.utcnow()
990
- snapshot = JSONLikeDirSnapShot()
991
- snapshot.take(".")
992
- ss_js = snapshot.to_json_like()
1065
+ ss_js = self.app.RunDirAppFiles.take_snapshot()
993
1066
  self._pending.set_EAR_ends[EAR_ID] = (dt, ss_js, exit_code, success)
994
1067
  if save:
995
1068
  self.save()
@@ -1245,6 +1318,7 @@ class PersistentStore(ABC):
1245
1318
  def _get_task_id_to_idx_map(self) -> Dict[int, int]:
1246
1319
  return {i.id_: i.index for i in self.get_tasks()}
1247
1320
 
1321
+ @TimeIt.decorator
1248
1322
  def get_task(self, task_idx: int) -> AnySTask:
1249
1323
  return self.get_tasks()[task_idx]
1250
1324
 
@@ -1287,10 +1361,10 @@ class PersistentStore(ABC):
1287
1361
 
1288
1362
  return self._process_retrieved_tasks(tasks)
1289
1363
 
1364
+ @TimeIt.decorator
1290
1365
  def get_tasks(self) -> List[AnySTask]:
1291
1366
  """Retrieve all tasks, including pending."""
1292
-
1293
- tasks = self._get_persistent_tasks()
1367
+ tasks = self._get_persistent_tasks(range(self._get_num_persistent_tasks()))
1294
1368
  tasks.update({k: v for k, v in self._pending.add_tasks.items()})
1295
1369
 
1296
1370
  # order by index:
@@ -1326,6 +1400,7 @@ class PersistentStore(ABC):
1326
1400
 
1327
1401
  return self._process_retrieved_loops(loops)
1328
1402
 
1403
+ @TimeIt.decorator
1329
1404
  def get_submissions(self) -> Dict[int, Dict]:
1330
1405
  """Retrieve all submissions, including pending."""
1331
1406
 
@@ -1337,6 +1412,7 @@ class PersistentStore(ABC):
1337
1412
 
1338
1413
  return subs
1339
1414
 
1415
+ @TimeIt.decorator
1340
1416
  def get_submissions_by_ID(self, id_lst: Iterable[int]) -> Dict[int, Dict]:
1341
1417
  # separate pending and persistent IDs:
1342
1418
  id_set = set(id_lst)
@@ -1352,6 +1428,7 @@ class PersistentStore(ABC):
1352
1428
 
1353
1429
  return subs
1354
1430
 
1431
+ @TimeIt.decorator
1355
1432
  def get_elements(self, id_lst: Iterable[int]) -> List[AnySElement]:
1356
1433
  self.logger.debug(f"PersistentStore.get_elements: id_lst={id_lst!r}")
1357
1434
 
@@ -1378,6 +1455,7 @@ class PersistentStore(ABC):
1378
1455
 
1379
1456
  return elems_new
1380
1457
 
1458
+ @TimeIt.decorator
1381
1459
  def get_element_iterations(self, id_lst: Iterable[int]) -> List[AnySElementIter]:
1382
1460
  self.logger.debug(f"PersistentStore.get_element_iterations: id_lst={id_lst!r}")
1383
1461
 
@@ -1413,6 +1491,7 @@ class PersistentStore(ABC):
1413
1491
 
1414
1492
  return iters_new
1415
1493
 
1494
+ @TimeIt.decorator
1416
1495
  def get_EARs(self, id_lst: Iterable[int]) -> List[AnySEAR]:
1417
1496
  self.logger.debug(f"PersistentStore.get_EARs: id_lst={id_lst!r}")
1418
1497
 
@@ -1457,10 +1536,51 @@ class PersistentStore(ABC):
1457
1536
 
1458
1537
  return EARs_new
1459
1538
 
1539
+ @TimeIt.decorator
1540
+ def _get_cached_persistent_items(
1541
+ self, id_lst: Iterable[int], cache: Dict
1542
+ ) -> Tuple[Dict[int, Any], List[int]]:
1543
+ id_lst = list(id_lst)
1544
+ if self.use_cache:
1545
+ id_set = set(id_lst)
1546
+ all_cached = set(cache.keys())
1547
+ id_cached = id_set.intersection(all_cached)
1548
+ id_non_cached = list(id_set.difference(all_cached))
1549
+ items = {k: cache[k] for k in id_cached}
1550
+ else:
1551
+ items = {}
1552
+ id_non_cached = id_lst
1553
+ return items, id_non_cached
1554
+
1555
+ def _get_cached_persistent_EARs(
1556
+ self, id_lst: Iterable[int]
1557
+ ) -> Tuple[Dict[int, AnySEAR], List[int]]:
1558
+ return self._get_cached_persistent_items(id_lst, self.EAR_cache)
1559
+
1560
+ def _get_cached_persistent_element_iters(
1561
+ self, id_lst: Iterable[int]
1562
+ ) -> Tuple[Dict[int, AnySEAR], List[int]]:
1563
+ return self._get_cached_persistent_items(id_lst, self.element_iter_cache)
1564
+
1565
+ def _get_cached_persistent_elements(
1566
+ self, id_lst: Iterable[int]
1567
+ ) -> Tuple[Dict[int, AnySEAR], List[int]]:
1568
+ return self._get_cached_persistent_items(id_lst, self.element_cache)
1569
+
1570
+ def _get_cached_persistent_tasks(self, id_lst: Iterable[int]):
1571
+ return self._get_cached_persistent_items(id_lst, self.task_cache)
1572
+
1573
+ def _get_cached_persistent_param_sources(self, id_lst: Iterable[int]):
1574
+ return self._get_cached_persistent_items(id_lst, self.param_sources_cache)
1575
+
1576
+ def _get_cached_persistent_parameters(self, id_lst: Iterable[int]):
1577
+ return self._get_cached_persistent_items(id_lst, self.parameter_cache)
1578
+
1460
1579
  def get_EAR_skipped(self, EAR_ID: int) -> bool:
1461
1580
  self.logger.debug(f"PersistentStore.get_EAR_skipped: EAR_ID={EAR_ID!r}")
1462
1581
  return self.get_EARs([EAR_ID])[0].skip
1463
1582
 
1583
+ @TimeIt.decorator
1464
1584
  def get_parameters(
1465
1585
  self,
1466
1586
  id_lst: Iterable[int],
@@ -1487,6 +1607,7 @@ class PersistentStore(ABC):
1487
1607
 
1488
1608
  return params
1489
1609
 
1610
+ @TimeIt.decorator
1490
1611
  def get_parameter_set_statuses(self, id_lst: Iterable[int]) -> List[bool]:
1491
1612
  # separate pending and persistent IDs:
1492
1613
  id_set = set(id_lst)
@@ -1500,6 +1621,7 @@ class PersistentStore(ABC):
1500
1621
  # order as requested:
1501
1622
  return [set_status[id_] for id_ in id_lst]
1502
1623
 
1624
+ @TimeIt.decorator
1503
1625
  def get_parameter_sources(self, id_lst: Iterable[int]) -> List[Dict]:
1504
1626
  # separate pending and persistent IDs:
1505
1627
  id_set = set(id_lst)
@@ -1523,15 +1645,23 @@ class PersistentStore(ABC):
1523
1645
 
1524
1646
  return src_new
1525
1647
 
1526
- def get_task_elements(self, task_id, idx_sel: slice) -> List[Dict]:
1527
- """Get element data by an index slice within a given task.
1648
+ @TimeIt.decorator
1649
+ def get_task_elements(
1650
+ self,
1651
+ task_id,
1652
+ idx_lst: Optional[Iterable[int]] = None,
1653
+ ) -> List[Dict]:
1654
+ """Get element data by an indices within a given task.
1528
1655
 
1529
1656
  Element iterations and EARs belonging to the elements are included.
1530
1657
 
1531
1658
  """
1532
1659
 
1533
1660
  all_elem_IDs = self.get_task(task_id).element_IDs
1534
- req_IDs = all_elem_IDs[idx_sel]
1661
+ if idx_lst is None:
1662
+ req_IDs = all_elem_IDs
1663
+ else:
1664
+ req_IDs = [all_elem_IDs[i] for i in idx_lst]
1535
1665
  store_elements = self.get_elements(req_IDs)
1536
1666
  iter_IDs = [i.iteration_IDs for i in store_elements]
1537
1667
  iter_IDs_flat, iter_IDs_lens = flatten(iter_IDs)
@@ -229,9 +229,10 @@ class JSONPersistentStore(PersistentStore):
229
229
  with self.using_resource("metadata", action="update") as md:
230
230
  md["runs"].extend(i.encode(self.ts_fmt) for i in EARs)
231
231
 
232
- def _update_EAR_submission_index(self, EAR_id: int, sub_idx: int):
232
+ def _update_EAR_submission_indices(self, sub_indices: Dict[int, int]):
233
233
  with self.using_resource("metadata", action="update") as md:
234
- md["runs"][EAR_id]["submission_idx"] = sub_idx
234
+ for EAR_ID_i, sub_idx_i in sub_indices.items():
235
+ md["runs"][EAR_ID_i]["submission_idx"] = sub_idx_i
235
236
 
236
237
  def _update_EAR_start(self, EAR_id: int, s_time: datetime, s_snap: Dict, s_hn: str):
237
238
  with self.using_resource("metadata", action="update") as md:
@@ -264,20 +265,6 @@ class JSONPersistentStore(PersistentStore):
264
265
  params["data"][str(param_i.id_)] = param_i.encode()
265
266
  params["sources"][str(param_i.id_)] = param_i.source
266
267
 
267
- def _set_parameter_value(self, param_id: int, value: Any, is_file: bool):
268
- """Set an unset persistent parameter."""
269
-
270
- # the `decode` call in `_get_persistent_parameters` should be quick:
271
- param = self._get_persistent_parameters([param_id])[param_id]
272
- if is_file:
273
- param = param.set_file(value)
274
- else:
275
- param = param.set_data(value)
276
-
277
- with self.using_resource("parameters", "update") as params:
278
- # no need to update sources array:
279
- params["data"][str(param_id)] = param.encode()
280
-
281
268
  def _set_parameter_values(self, set_parameters: Dict[int, Tuple[Any, bool]]):
282
269
  """Set multiple unset persistent parameters."""
283
270
  param_ids = list(set_parameters.keys())
@@ -310,8 +297,13 @@ class JSONPersistentStore(PersistentStore):
310
297
 
311
298
  def _get_num_persistent_tasks(self) -> int:
312
299
  """Get the number of persistent tasks."""
313
- with self.using_resource("metadata", action="read") as md:
314
- return len(md["tasks"])
300
+ if self.num_tasks_cache is not None:
301
+ num = self.num_tasks_cache
302
+ else:
303
+ with self.using_resource("metadata", action="read") as md:
304
+ num = len(md["tasks"])
305
+ self.num_tasks_cache = num
306
+ return num
315
307
 
316
308
  def _get_num_persistent_loops(self) -> int:
317
309
  """Get the number of persistent loops."""
@@ -386,16 +378,18 @@ class JSONPersistentStore(PersistentStore):
386
378
  with self.using_resource("metadata", "read") as md:
387
379
  return md["template"]
388
380
 
389
- def _get_persistent_tasks(
390
- self, id_lst: Optional[Iterable[int]] = None
391
- ) -> Dict[int, StoreTask]:
392
- with self.using_resource("metadata", action="read") as md:
393
- task_dat = {
394
- i["id_"]: StoreTask.decode({**i, "index": idx})
395
- for idx, i in enumerate(md["tasks"])
396
- if id_lst is None or i["id_"] in id_lst
397
- }
398
- return task_dat
381
+ def _get_persistent_tasks(self, id_lst: Iterable[int]) -> Dict[int, StoreTask]:
382
+ tasks, id_lst = self._get_cached_persistent_tasks(id_lst)
383
+ if id_lst:
384
+ with self.using_resource("metadata", action="read") as md:
385
+ new_tasks = {
386
+ i["id_"]: StoreTask.decode({**i, "index": idx})
387
+ for idx, i in enumerate(md["tasks"])
388
+ if id_lst is None or i["id_"] in id_lst
389
+ }
390
+ self.task_cache.update(new_tasks)
391
+ tasks.update(new_tasks)
392
+ return tasks
399
393
 
400
394
  def _get_persistent_loops(self, id_lst: Optional[Iterable[int]] = None):
401
395
  with self.using_resource("metadata", "read") as md:
@@ -428,54 +422,81 @@ class JSONPersistentStore(PersistentStore):
428
422
  return subs_dat
429
423
 
430
424
  def _get_persistent_elements(self, id_lst: Iterable[int]) -> Dict[int, StoreElement]:
431
- # could convert `id_lst` to e.g. slices if more efficient for a given store
432
- with self.using_resource("metadata", action="read") as md:
433
- try:
434
- elem_dat = {i: md["elements"][i] for i in id_lst}
435
- except KeyError:
436
- raise MissingStoreElementError(id_lst) from None
437
- return {k: StoreElement.decode(v) for k, v in elem_dat.items()}
425
+ elems, id_lst = self._get_cached_persistent_elements(id_lst)
426
+ if id_lst:
427
+ # could convert `id_lst` to e.g. slices if more efficient for a given store
428
+ with self.using_resource("metadata", action="read") as md:
429
+ try:
430
+ elem_dat = {i: md["elements"][i] for i in id_lst}
431
+ except KeyError:
432
+ raise MissingStoreElementError(id_lst) from None
433
+ new_elems = {k: StoreElement.decode(v) for k, v in elem_dat.items()}
434
+ self.element_cache.update(new_elems)
435
+ elems.update(new_elems)
436
+ return elems
438
437
 
439
438
  def _get_persistent_element_iters(
440
439
  self, id_lst: Iterable[int]
441
440
  ) -> Dict[int, StoreElementIter]:
442
- with self.using_resource("metadata", action="read") as md:
443
- try:
444
- iter_dat = {i: md["iters"][i] for i in id_lst}
445
- except KeyError:
446
- raise MissingStoreElementIterationError(id_lst) from None
447
- return {k: StoreElementIter.decode(v) for k, v in iter_dat.items()}
441
+ iters, id_lst = self._get_cached_persistent_element_iters(id_lst)
442
+ if id_lst:
443
+ with self.using_resource("metadata", action="read") as md:
444
+ try:
445
+ iter_dat = {i: md["iters"][i] for i in id_lst}
446
+ except KeyError:
447
+ raise MissingStoreElementIterationError(id_lst) from None
448
+ new_iters = {k: StoreElementIter.decode(v) for k, v in iter_dat.items()}
449
+ self.element_iter_cache.update(new_iters)
450
+ iters.update(new_iters)
451
+ return iters
448
452
 
449
453
  def _get_persistent_EARs(self, id_lst: Iterable[int]) -> Dict[int, StoreEAR]:
450
- with self.using_resource("metadata", action="read") as md:
451
- try:
452
- EAR_dat = {i: md["runs"][i] for i in id_lst}
453
- except KeyError:
454
- raise MissingStoreEARError(id_lst) from None
455
- return {k: StoreEAR.decode(v, self.ts_fmt) for k, v in EAR_dat.items()}
454
+ runs, id_lst = self._get_cached_persistent_EARs(id_lst)
455
+ if id_lst:
456
+ with self.using_resource("metadata", action="read") as md:
457
+ try:
458
+ EAR_dat = {i: md["runs"][i] for i in id_lst}
459
+ except KeyError:
460
+ raise MissingStoreEARError(id_lst) from None
461
+ new_runs = {
462
+ k: StoreEAR.decode(v, self.ts_fmt) for k, v in EAR_dat.items()
463
+ }
464
+ self.EAR_cache.update(new_runs)
465
+ runs.update(new_runs)
466
+ return runs
456
467
 
457
468
  def _get_persistent_parameters(
458
469
  self,
459
470
  id_lst: Iterable[int],
460
471
  ) -> Dict[int, StoreParameter]:
461
- with self.using_resource("parameters", "read") as params:
462
- try:
463
- param_dat = {i: params["data"][str(i)] for i in id_lst}
464
- src_dat = {i: params["sources"][str(i)] for i in id_lst}
465
- except KeyError:
466
- raise MissingParameterData(id_lst) from None
467
-
468
- return {
469
- k: StoreParameter.decode(id_=k, data=v, source=src_dat[k])
470
- for k, v in param_dat.items()
471
- }
472
+ params, id_lst = self._get_cached_persistent_parameters(id_lst)
473
+ if id_lst:
474
+ with self.using_resource("parameters", "read") as params:
475
+ try:
476
+ param_dat = {i: params["data"][str(i)] for i in id_lst}
477
+ src_dat = {i: params["sources"][str(i)] for i in id_lst}
478
+ except KeyError:
479
+ raise MissingParameterData(id_lst) from None
480
+
481
+ new_params = {
482
+ k: StoreParameter.decode(id_=k, data=v, source=src_dat[k])
483
+ for k, v in param_dat.items()
484
+ }
485
+ self.parameter_cache.update(new_params)
486
+ params.update(new_params)
487
+ return params
472
488
 
473
489
  def _get_persistent_param_sources(self, id_lst: Iterable[int]) -> Dict[int, Dict]:
474
- with self.using_resource("parameters", "read") as params:
475
- try:
476
- return {i: params["sources"][str(i)] for i in id_lst}
477
- except KeyError:
478
- raise MissingParameterData(id_lst) from None
490
+ sources, id_lst = self._get_cached_persistent_param_sources(id_lst)
491
+ if id_lst:
492
+ with self.using_resource("parameters", "read") as params:
493
+ try:
494
+ new_sources = {i: params["sources"][str(i)] for i in id_lst}
495
+ except KeyError:
496
+ raise MissingParameterData(id_lst) from None
497
+ self.param_sources_cache.update(new_sources)
498
+ sources.update(new_sources)
499
+ return sources
479
500
 
480
501
  def _get_persistent_parameter_set_status(
481
502
  self, id_lst: Iterable[int]
@@ -142,6 +142,7 @@ class PendingChanges:
142
142
  task_ids = list(self.add_tasks.keys())
143
143
  self.logger.debug(f"commit: adding pending tasks with IDs: {task_ids!r}")
144
144
  self.store._append_tasks(tasks)
145
+ self.store.num_tasks_cache = None # invalidate cache
145
146
  # pending element IDs that belong to pending tasks are now committed:
146
147
  self.add_elem_IDs = {
147
148
  k: v for k, v in self.add_elem_IDs.items() if k not in task_ids
@@ -187,6 +188,7 @@ class PendingChanges:
187
188
  f"commit: adding pending element IDs to task {task_ID!r}: {elem_IDs!r}."
188
189
  )
189
190
  self.store._append_task_element_IDs(task_ID, elem_IDs)
191
+ self.store.task_cache.pop(task_ID, None) # invalidate cache
190
192
  self.clear_add_elem_IDs()
191
193
 
192
194
  @TimeIt.decorator
@@ -219,6 +221,7 @@ class PendingChanges:
219
221
  f"{iter_IDs!r}."
220
222
  )
221
223
  self.store._append_elem_iter_IDs(elem_ID, iter_IDs)
224
+ self.store.element_cache.pop(elem_ID, None) # invalidate cache
222
225
  self.clear_add_elem_iter_IDs()
223
226
 
224
227
  @TimeIt.decorator
@@ -250,6 +253,7 @@ class PendingChanges:
250
253
  )
251
254
  for act_idx, EAR_IDs in act_EAR_IDs.items():
252
255
  self.store._append_elem_iter_EAR_IDs(iter_ID, act_idx, EAR_IDs)
256
+ self.store.element_iter_cache.pop(iter_ID, None) # invalidate cache
253
257
  self.clear_add_elem_iter_EAR_IDs()
254
258
 
255
259
  @TimeIt.decorator
@@ -286,19 +290,21 @@ class PendingChanges:
286
290
  )
287
291
  # TODO: could be batched up?
288
292
  for i in iter_ids:
289
- self._store._update_elem_iter_EARs_initialised(i)
293
+ self.store._update_elem_iter_EARs_initialised(i)
294
+ self.store.element_iter_cache.pop(i, None) # invalidate cache
290
295
  self.clear_set_EARs_initialised()
291
296
 
292
297
  @TimeIt.decorator
293
298
  def commit_EAR_submission_indices(self) -> None:
294
- # TODO: could be batched up?
295
- for EAR_id, sub_idx in self.set_EAR_submission_indices.items():
299
+ if self.set_EAR_submission_indices:
296
300
  self.logger.debug(
297
- f"commit: adding pending submission index ({sub_idx!r}) to EAR ID "
298
- f"{EAR_id!r}."
301
+ f"commit: updating submission indices: "
302
+ f"{self.set_EAR_submission_indices!r}."
299
303
  )
300
- self.store._update_EAR_submission_index(EAR_id, sub_idx)
301
- self.clear_set_EAR_submission_indices()
304
+ self.store._update_EAR_submission_indices(self.set_EAR_submission_indices)
305
+ for EAR_ID_i in self.set_EAR_submission_indices.keys():
306
+ self.store.EAR_cache.pop(EAR_ID_i, None) # invalidate cache
307
+ self.clear_set_EAR_submission_indices()
302
308
 
303
309
  @TimeIt.decorator
304
310
  def commit_EAR_starts(self) -> None:
@@ -309,6 +315,7 @@ class PendingChanges:
309
315
  f"({hostname!r}), and directory snapshot to EAR ID {EAR_id!r}."
310
316
  )
311
317
  self.store._update_EAR_start(EAR_id, time, snap, hostname)
318
+ self.store.EAR_cache.pop(EAR_id, None) # invalidate cache
312
319
  self.clear_set_EAR_starts()
313
320
 
314
321
  @TimeIt.decorator
@@ -320,6 +327,7 @@ class PendingChanges:
320
327
  f"exit code ({ext!r}), and success status {suc!r} to EAR ID {EAR_id!r}."
321
328
  )
322
329
  self.store._update_EAR_end(EAR_id, time, snap, ext, suc)
330
+ self.store.EAR_cache.pop(EAR_id, None) # invalidate cache
323
331
  self.clear_set_EAR_ends()
324
332
 
325
333
  @TimeIt.decorator
@@ -328,6 +336,7 @@ class PendingChanges:
328
336
  for EAR_id in self.set_EAR_skips:
329
337
  self.logger.debug(f"commit: setting EAR ID {EAR_id!r} as skipped.")
330
338
  self.store._update_EAR_skip(EAR_id)
339
+ self.store.EAR_cache.pop(EAR_id, None) # invalidate cache
331
340
  self.clear_set_EAR_skips()
332
341
 
333
342
  @TimeIt.decorator
@@ -353,6 +362,8 @@ class PendingChanges:
353
362
  param_ids = list(self.set_parameters.keys())
354
363
  self.logger.debug(f"commit: setting values of parameter IDs {param_ids!r}.")
355
364
  self.store._set_parameter_values(self.set_parameters)
365
+ for id_i in param_ids:
366
+ self.store.parameter_cache.pop(id_i, None)
356
367
 
357
368
  self.clear_set_parameters()
358
369
 
@@ -378,6 +389,8 @@ class PendingChanges:
378
389
  param_ids = list(self.update_param_sources.keys())
379
390
  self.logger.debug(f"commit: updating sources of parameter IDs {param_ids!r}.")
380
391
  self.store._update_parameter_sources(self.update_param_sources)
392
+ for id_i in param_ids:
393
+ self.store.param_sources_cache.pop(id_i, None) # invalidate cache
381
394
  self.clear_update_param_sources()
382
395
 
383
396
  @TimeIt.decorator
@@ -389,6 +402,7 @@ class PendingChanges:
389
402
  f"{loop_idx!r}."
390
403
  )
391
404
  self.store._update_loop_index(iter_ID, loop_idx)
405
+ self.store.element_iter_cache.pop(iter_ID, None) # invalidate cache
392
406
  self.clear_update_loop_indices()
393
407
 
394
408
  @TimeIt.decorator
@@ -1,10 +1,11 @@
1
1
  from getpass import getpass
2
- from paramiko.ssh_exception import SSHException
3
2
 
4
3
  from hpcflow.sdk.core.errors import WorkflowNotFoundError
5
4
 
6
5
 
7
6
  def ask_pw_on_auth_exc(f, *args, add_pw_to=None, **kwargs):
7
+ from paramiko.ssh_exception import SSHException
8
+
8
9
  try:
9
10
  out = f(*args, **kwargs)
10
11
  pw = None