hpcflow-new2 0.2.0a222__py3-none-any.whl → 0.2.0a227__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.
hpcflow/_version.py CHANGED
@@ -1 +1 @@
1
- __version__ = "0.2.0a222"
1
+ __version__ = "0.2.0a227"
hpcflow/sdk/app.py CHANGED
@@ -3315,12 +3315,13 @@ class BaseApp(metaclass=Singleton):
3315
3315
  out_item["deleted"] = True
3316
3316
 
3317
3317
  else:
3318
- if status:
3319
- status.update(
3320
- f"Reading workflow {file_dat_i['path']!r} submission info..."
3321
- )
3322
3318
  with wk_i._store.cache_ctx():
3323
3319
  sub = wk_i.submissions[file_dat_i["sub_idx"]]
3320
+ if status:
3321
+ status.update(
3322
+ f"Loading workflow {file_dat_i['path']!r} run metadata..."
3323
+ )
3324
+ sub.use_EARs_cache = True # pre-cache EARs of this submission
3324
3325
 
3325
3326
  all_jobscripts = sub._submission_parts[submit_time_str]
3326
3327
  out_item["jobscripts"] = all_jobscripts
@@ -3583,7 +3584,7 @@ class BaseApp(metaclass=Singleton):
3583
3584
  )
3584
3585
  all_cells["status"] = "/".join(
3585
3586
  js_state.rich_repr
3586
- for js_state in sorted(act_js_states, key=lambda x: x.value)
3587
+ for js_state in sorted(act_js_states, key=lambda x: x.id)
3587
3588
  )
3588
3589
  else:
3589
3590
  if deleted:
@@ -3645,7 +3646,7 @@ class BaseApp(metaclass=Singleton):
3645
3646
  all_cells["actions_compact"] = " | ".join(
3646
3647
  f"[{k.colour}]{k.symbol}[/{k.colour}]:{v}" # type: ignore
3647
3648
  for k, v in dict(
3648
- sorted(EAR_stat_count.items(), key=lambda x: x[0].value)
3649
+ sorted(EAR_stat_count.items(), key=lambda x: x[0].id)
3649
3650
  ).items()
3650
3651
  )
3651
3652
  else:
hpcflow/sdk/cli.py CHANGED
@@ -4,6 +4,7 @@ Command line interface implementation.
4
4
 
5
5
  from __future__ import annotations
6
6
  import contextlib
7
+ import datetime
7
8
  import json
8
9
  import os
9
10
  import time
@@ -772,6 +773,12 @@ def _make_submission_CLI(app: BaseApp):
772
773
  def get_login_nodes(scheduler: SGEPosix):
773
774
  pprint(scheduler.get_login_nodes())
774
775
 
776
+ class _DateTimeJSONEncoder(json.JSONEncoder):
777
+ def default(self, obj):
778
+ if isinstance(obj, datetime.datetime):
779
+ return obj.isoformat()
780
+ return super().default(obj)
781
+
775
782
  @submission.command()
776
783
  @click.option(
777
784
  "as_json",
@@ -784,7 +791,7 @@ def _make_submission_CLI(app: BaseApp):
784
791
  """Print known-submissions information as a formatted Python object."""
785
792
  out = app.get_known_submissions(as_json=as_json)
786
793
  if as_json:
787
- click.echo(json.dumps(out))
794
+ click.echo(json.dumps(out, cls=_DateTimeJSONEncoder))
788
795
  else:
789
796
  pprint(out)
790
797
 
hpcflow/sdk/core/enums.py CHANGED
@@ -26,45 +26,85 @@ class ActionScopeType(Enum):
26
26
 
27
27
 
28
28
  @dataclass(frozen=True)
29
- class _EARStatus:
29
+ class _ReportableStateData:
30
30
  """
31
- Model of the state of an EARStatus.
31
+ Model of the state of something that is renderable using a symbol and colour.
32
+
33
+ Notes
34
+ -----
35
+ This class is used as the value in the the enumeration subclasses
36
+ :py:class:`EARStatus` and :py:class:`JobscriptElementState`.
37
+
32
38
  """
33
39
 
34
- _value: int
35
- #: Symbol to use when rendering a status.
40
+ #: ID that distinguishes the state.
41
+ id: int
42
+ #: Symbol to use when rendering a state.
36
43
  symbol: str
37
- #: Colour to use when rendering a status.
44
+ #: Colour to use when rendering a state.
38
45
  colour: str
46
+ #: Documentation of the meaning of the state.
39
47
  __doc__: str = ""
40
48
 
41
49
 
42
- class EARStatus(_EARStatus, Enum):
50
+ class _ReportableStateEnum(Enum):
51
+ """Enumeration superclass for reportable state subclasses with some shared methods."""
52
+
53
+ @property
54
+ def id(self) -> int:
55
+ """
56
+ The integer ID associated with this state.
57
+ """
58
+ return self.value.id
59
+
60
+ @property
61
+ def colour(self) -> str:
62
+ """
63
+ The colour associated with this state.
64
+ """
65
+ return self.value.colour
66
+
67
+ @property
68
+ def symbol(self) -> str:
69
+ """
70
+ The symbol associated with this state.
71
+ """
72
+ return self.value.symbol
73
+
74
+ @property
75
+ def rich_repr(self) -> str:
76
+ """
77
+ Rich representation of this enumeration element.
78
+ """
79
+ return f"[{self.colour}]{self.symbol}[/{self.colour}]"
80
+
81
+
82
+ class EARStatus(_ReportableStateEnum):
43
83
  """Enumeration of all possible EAR statuses, and their associated status colour."""
44
84
 
45
85
  #: Not yet associated with a submission.
46
- pending = (
86
+ pending = _ReportableStateData(
47
87
  0,
48
88
  ".",
49
89
  "grey46",
50
90
  "Not yet associated with a submission.",
51
91
  )
52
92
  #: Associated with a prepared submission that is not yet submitted.
53
- prepared = (
93
+ prepared = _ReportableStateData(
54
94
  1,
55
95
  ".",
56
96
  "grey46",
57
97
  "Associated with a prepared submission that is not yet submitted.",
58
98
  )
59
99
  #: Submitted for execution.
60
- submitted = (
100
+ submitted = _ReportableStateData(
61
101
  2,
62
102
  ".",
63
103
  "grey46",
64
104
  "Submitted for execution.",
65
105
  )
66
106
  #: Executing now.
67
- running = (
107
+ running = _ReportableStateData(
68
108
  3,
69
109
  "●",
70
110
  "dodger_blue1",
@@ -72,7 +112,7 @@ class EARStatus(_EARStatus, Enum):
72
112
  )
73
113
  #: Not attempted due to a failure of an upstream action on which this depends,
74
114
  #: or a loop termination condition being satisfied.
75
- skipped = (
115
+ skipped = _ReportableStateData(
76
116
  4,
77
117
  "s",
78
118
  "dark_orange",
@@ -82,32 +122,27 @@ class EARStatus(_EARStatus, Enum):
82
122
  ),
83
123
  )
84
124
  #: Aborted by the user; downstream actions will be attempted.
85
- aborted = (
125
+ aborted = _ReportableStateData(
86
126
  5,
87
127
  "A",
88
128
  "deep_pink4",
89
129
  "Aborted by the user; downstream actions will be attempted.",
90
130
  )
91
131
  #: Probably exited successfully.
92
- success = (
132
+ success = _ReportableStateData(
93
133
  6,
94
134
  "■",
95
135
  "green3",
96
136
  "Probably exited successfully.",
97
137
  )
98
138
  #: Probably failed.
99
- error = (
139
+ error = _ReportableStateData(
100
140
  7,
101
141
  "E",
102
142
  "red3",
103
143
  "Probably failed.",
104
144
  )
105
145
 
106
- @property
107
- def value(self) -> int:
108
- #: The value of the status.
109
- return self._value
110
-
111
146
  @classmethod
112
147
  def get_non_running_submitted_states(cls) -> frozenset[EARStatus]:
113
148
  """Return the set of all non-running states, excluding those before submission."""
@@ -120,13 +155,6 @@ class EARStatus(_EARStatus, Enum):
120
155
  }
121
156
  )
122
157
 
123
- @property
124
- def rich_repr(self) -> str:
125
- """
126
- The rich representation of the value.
127
- """
128
- return f"[{self.colour}]{self.symbol}[/{self.colour}]"
129
-
130
158
 
131
159
  class InputSourceType(Enum):
132
160
  """
@@ -1584,7 +1584,7 @@ class MultiPathSequence(_BaseSequence):
1584
1584
  paths: Sequence[str],
1585
1585
  num_samples: int,
1586
1586
  *,
1587
- bounds: dict[str, Sequence[float]] | None = None,
1587
+ bounds: dict[str, dict[str, str | Sequence[float]]] | None = None,
1588
1588
  scramble: bool = True,
1589
1589
  strength: int = 1,
1590
1590
  optimization: Literal["random-cd", "lloyd"] | None = None,
@@ -1602,10 +1602,20 @@ class MultiPathSequence(_BaseSequence):
1602
1602
 
1603
1603
  bounds = bounds or {}
1604
1604
 
1605
- parameter_ranges = np.array([bounds.get(path, [0, 1]) for path in paths]).T
1605
+ scaling = np.asarray(
1606
+ [bounds.get(path, {}).get("scaling", "linear") for path in paths]
1607
+ )
1608
+
1609
+ # extents including defaults for unspecified:
1610
+ all_extents = [bounds.get(path, {}).get("extent", [0, 1]) for path in paths]
1606
1611
 
1607
- lower_bound = parameter_ranges[0]
1608
- upper_bound = parameter_ranges[1]
1612
+ # extents accounting for scaling type:
1613
+ extent = np.asarray(
1614
+ [
1615
+ np.log10(all_extents[i]) if scaling[i] == "log" else all_extents[i]
1616
+ for i in range(len(scaling))
1617
+ ]
1618
+ ).T
1609
1619
 
1610
1620
  try:
1611
1621
  sampler = LatinHypercube(**kwargs)
@@ -1614,9 +1624,15 @@ class MultiPathSequence(_BaseSequence):
1614
1624
  kwargs["seed"] = kwargs.pop("rng")
1615
1625
  sampler = LatinHypercube(**kwargs)
1616
1626
 
1617
- samples = scale(sampler.random(n=num_samples), lower_bound, upper_bound).T
1627
+ samples = scale(
1628
+ sampler.random(n=num_samples), l_bounds=extent[0], u_bounds=extent[1]
1629
+ )
1630
+
1631
+ for i in range(len(scaling)):
1632
+ if scaling[i] == "log":
1633
+ samples[:, i] = 10 ** samples[:, i]
1618
1634
 
1619
- return samples
1635
+ return samples.T
1620
1636
 
1621
1637
  @classmethod
1622
1638
  def from_latin_hypercube(
@@ -1624,7 +1640,7 @@ class MultiPathSequence(_BaseSequence):
1624
1640
  paths: Sequence[str],
1625
1641
  num_samples: int,
1626
1642
  *,
1627
- bounds: dict[str, Sequence[float]] | None = None,
1643
+ bounds: dict[str, dict[str, str | Sequence[float]]] | None = None,
1628
1644
  scramble: bool = True,
1629
1645
  strength: int = 1,
1630
1646
  optimization: Literal["random-cd", "lloyd"] | None = None,
@@ -1634,6 +1650,32 @@ class MultiPathSequence(_BaseSequence):
1634
1650
  ) -> Self:
1635
1651
  """
1636
1652
  Generate values from SciPy's latin hypercube sampler: :class:`scipy.stats.qmc.LatinHypercube`.
1653
+
1654
+ Parameters
1655
+ ----------
1656
+ paths : Sequence[str]
1657
+ List of dot-delimited paths within the parameter's nested data structure for which
1658
+ 'value' should be set.
1659
+ num_samples : int
1660
+ Number of random hypercube samples to take.
1661
+ bounds : dict[str, dict[str, str | Sequence[float]]] | None, optional
1662
+ Bounds dictionary structure which takes a path as a key and returns another dictionary
1663
+ which takes `scaling` and `extent` as keys. `extent` defines the width of the parameter
1664
+ space, and `scaling` defines whether to take logarithmically spaced samples ("log") or not ("linear"). By default,
1665
+ linear scaling and an extent between 0 and 1 is used.
1666
+ scramble : bool, optional
1667
+ See `scipy.stats.qmc.LatinHypercube`, by default True
1668
+ strength : int, optional
1669
+ See 'scipy.stats.qmc.LatinHypercube', by default 1
1670
+ optimization : Literal["random, optional
1671
+ See 'scipy.stats.qmc.LatinHypercube', by default None
1672
+ rng : _type_, optional
1673
+ See 'scipy.stats.qmc.LatinHypercube', by default None
1674
+
1675
+ Returns
1676
+ -------
1677
+ NDArray
1678
+ Array of hypercube samples.
1637
1679
  """
1638
1680
  kwargs = {
1639
1681
  "paths": paths,
@@ -1218,12 +1218,15 @@ class PersistentStore(
1218
1218
  @contextlib.contextmanager
1219
1219
  def cache_ctx(self) -> Iterator[None]:
1220
1220
  """Context manager for using the persistent element/iteration/run cache."""
1221
- self._use_cache = True
1222
- try:
1221
+ if self._use_cache:
1223
1222
  yield
1224
- finally:
1225
- self._use_cache = False
1226
- self._reset_cache()
1223
+ else:
1224
+ self._use_cache = True
1225
+ try:
1226
+ yield
1227
+ finally:
1228
+ self._use_cache = False
1229
+ self._reset_cache()
1227
1230
 
1228
1231
  @contextlib.contextmanager
1229
1232
  def parameters_metadata_cache(self):
@@ -18,6 +18,7 @@ from numpy.ma.core import MaskedArray
18
18
  import zarr # type: ignore
19
19
  from zarr.errors import BoundsCheckError # type: ignore
20
20
  from zarr.storage import DirectoryStore, FSStore # type: ignore
21
+ from zarr.util import guess_chunks # type: ignore
21
22
  from fsspec.implementations.zip import ZipFileSystem # type: ignore
22
23
  from rich.console import Console
23
24
  from numcodecs import MsgPack, VLenArray, blosc, Blosc, Zstd # type: ignore
@@ -59,6 +60,7 @@ from hpcflow.sdk.submission.submission import (
59
60
  SUBMISSION_SUBMIT_TIME_KEYS,
60
61
  )
61
62
  from hpcflow.sdk.utils.arrays import get_2D_idx, split_arr
63
+ from hpcflow.sdk.utils.patches import override_module_attrs
62
64
  from hpcflow.sdk.utils.strings import shorten_list_str
63
65
 
64
66
  if TYPE_CHECKING:
@@ -89,6 +91,11 @@ if TYPE_CHECKING:
89
91
  ListAny: TypeAlias = "list[Any]"
90
92
  #: Zarr attribute mapping context.
91
93
  ZarrAttrs: TypeAlias = "dict[str, Any]"
94
+ #: Soft lower limit for the number of bytes in an array chunk
95
+ _ARRAY_CHUNK_MIN: int = 500 * 1024 * 1024 # 500 MiB
96
+ #: Hard upper limit for the number of bytes in an array chunk. Should be lower than the
97
+ #: maximum buffer size of the blosc encoder, if we're using it (2 GiB)
98
+ _ARRAY_CHUNK_MAX: int = 1024 * 1024 * 1024 # 1 GiB
92
99
  _JS: TypeAlias = "dict[str, list[dict[str, dict]]]"
93
100
 
94
101
 
@@ -124,7 +131,13 @@ def _encode_numpy_array(
124
131
  new_idx = (
125
132
  max((int(i.removeprefix("arr_")) for i in param_arr_group.keys()), default=-1) + 1
126
133
  )
127
- param_arr_group.create_dataset(name=f"arr_{new_idx}", data=obj, chunks=obj.shape)
134
+ with override_module_attrs(
135
+ "zarr.util", {"CHUNK_MIN": _ARRAY_CHUNK_MIN, "CHUNK_MAX": _ARRAY_CHUNK_MAX}
136
+ ):
137
+ # `guess_chunks` also ensures chunk shape is at least 1 in each dimension:
138
+ chunk_shape = guess_chunks(obj.shape, obj.dtype.itemsize)
139
+
140
+ param_arr_group.create_dataset(name=f"arr_{new_idx}", data=obj, chunks=chunk_shape)
128
141
  type_lookup["arrays"].append([path, new_idx])
129
142
 
130
143
  return len(type_lookup["arrays"]) - 1
@@ -6,82 +6,56 @@ from __future__ import annotations
6
6
  from dataclasses import dataclass
7
7
  from enum import Enum
8
8
 
9
+ from hpcflow.sdk.core.enums import _ReportableStateData, _ReportableStateEnum
9
10
 
10
- @dataclass(frozen=True)
11
- class _JES:
12
- """
13
- Model of the state of a JobscriptElementState
14
- """
15
-
16
- _value: int
17
- #: The symbol used to render the state.
18
- symbol: str
19
- #: The colour used to render the state.
20
- colour: str
21
- __doc__: str = ""
22
11
 
23
-
24
- class JobscriptElementState(_JES, Enum):
12
+ class JobscriptElementState(_ReportableStateEnum):
25
13
  """Enumeration to convey a particular jobscript element state as reported by the
26
14
  scheduler."""
27
15
 
28
16
  #: Waiting for resource allocation.
29
- pending = (
17
+ pending = _ReportableStateData(
30
18
  0,
31
19
  "○",
32
20
  "yellow",
33
21
  "Waiting for resource allocation.",
34
22
  )
35
23
  #: Waiting for one or more dependencies to finish.
36
- waiting = (
24
+ waiting = _ReportableStateData(
37
25
  1,
38
26
  "◊",
39
27
  "grey46",
40
28
  "Waiting for one or more dependencies to finish.",
41
29
  )
42
30
  #: Executing now.
43
- running = (
31
+ running = _ReportableStateData(
44
32
  2,
45
33
  "●",
46
34
  "dodger_blue1",
47
35
  "Executing now.",
48
36
  )
49
37
  #: Previously submitted but is no longer active.
50
- finished = (
38
+ finished = _ReportableStateData(
51
39
  3,
52
40
  "■",
53
41
  "grey46",
54
42
  "Previously submitted but is no longer active.",
55
43
  )
56
44
  #: Cancelled by the user.
57
- cancelled = (
45
+ cancelled = _ReportableStateData(
58
46
  4,
59
47
  "C",
60
48
  "red3",
61
49
  "Cancelled by the user.",
62
50
  )
63
51
  #: The scheduler reports an error state.
64
- errored = (
52
+ errored = _ReportableStateData(
65
53
  5,
66
54
  "E",
67
55
  "red3",
68
56
  "The scheduler reports an error state.",
69
57
  )
70
58
 
71
- @property
72
- def value(self) -> int:
73
- """
74
- The numerical value of this state.
75
- """
76
- return self._value
77
-
78
- @property
79
- def rich_repr(self) -> str:
80
- """
81
- Rich representation of this enumeration element.
82
- """
83
- return f"[{self.colour}]{self.symbol}[/{self.colour}]"
84
-
85
59
 
86
60
  class SubmissionStatus(Enum):
87
61
  """
@@ -901,6 +901,7 @@ class Jobscript(JSONLike):
901
901
  )
902
902
 
903
903
  @property
904
+ @TimeIt.decorator
904
905
  def all_EAR_IDs(self) -> NDArray:
905
906
  """Return all run IDs of this jobscripts (across all blocks), removing missing
906
907
  run IDs (i.e. -1 values)"""
@@ -912,6 +913,8 @@ class Jobscript(JSONLike):
912
913
  """
913
914
  Description of EAR information for this jobscript.
914
915
  """
916
+ if self.submission._use_EARs_cache:
917
+ return [self.submission._EARs_cache[ear_id] for ear_id in self.all_EAR_IDs]
915
918
  return self.workflow.get_EARs_from_IDs(self.all_EAR_IDs)
916
919
 
917
920
  @property
@@ -11,6 +11,7 @@ from textwrap import indent
11
11
  from typing import Any, Literal, overload, TYPE_CHECKING
12
12
  from typing_extensions import override
13
13
  import warnings
14
+ from contextlib import contextmanager
14
15
 
15
16
 
16
17
  from hpcflow.sdk.utils.strings import shorten_list_str
@@ -135,6 +136,10 @@ class Submission(JSONLike):
135
136
  None # assigned on first access
136
137
  )
137
138
 
139
+ # updated in _submission_EARs_cache context manager:
140
+ self._use_EARs_cache = False
141
+ self._EARs_cache: dict[int, ElementActionRun] = {}
142
+
138
143
  if workflow:
139
144
  #: The workflow this is part of.
140
145
  self.workflow = workflow
@@ -226,6 +231,8 @@ class Submission(JSONLike):
226
231
  del dct["_workflow"]
227
232
  del dct["_index"]
228
233
  del dct["_submission_parts_lst"]
234
+ del dct["_use_EARs_cache"]
235
+ del dct["_EARs_cache"]
229
236
  return {k.lstrip("_"): v for k, v in dct.items()}
230
237
 
231
238
  @property
@@ -265,6 +272,30 @@ class Submission(JSONLike):
265
272
  ]
266
273
  return self._submission_parts_lst
267
274
 
275
+ @property
276
+ @TimeIt.decorator
277
+ def use_EARs_cache(self) -> bool:
278
+ """Whether to pre-cache all EARs associated with the submission."""
279
+ return self._use_EARs_cache
280
+
281
+ @use_EARs_cache.setter
282
+ @TimeIt.decorator
283
+ def use_EARs_cache(self, value: bool):
284
+ """Toggle the EAR caching facility."""
285
+ if self._use_EARs_cache == value:
286
+ return
287
+ self._use_EARs_cache = value
288
+ if value:
289
+ all_EAR_IDs = list(self.all_EAR_IDs)
290
+ self._EARs_cache = {
291
+ ear_ID: ear
292
+ for ear_ID, ear in zip(
293
+ all_EAR_IDs, self.workflow.get_EARs_from_IDs(all_EAR_IDs)
294
+ )
295
+ }
296
+ else:
297
+ self._EARs_cache = {} # reset the cache
298
+
268
299
  @TimeIt.decorator
269
300
  def get_start_time(self, submit_time: str) -> datetime | None:
270
301
  """Get the start time of a given submission part."""
@@ -283,17 +314,36 @@ class Submission(JSONLike):
283
314
  @TimeIt.decorator
284
315
  def start_time(self) -> datetime | None:
285
316
  """Get the first non-None start time over all submission parts."""
286
- times = (
287
- self.get_start_time(submit_time) for submit_time in self._submission_parts
288
- )
289
- return min((t for t in times if t is not None), default=None)
317
+ with self.using_EARs_cache():
318
+ times = (
319
+ self.get_start_time(submit_time) for submit_time in self._submission_parts
320
+ )
321
+ return min((t for t in times if t is not None), default=None)
290
322
 
291
323
  @property
292
324
  @TimeIt.decorator
293
325
  def end_time(self) -> datetime | None:
294
326
  """Get the final non-None end time over all submission parts."""
295
- times = (self.get_end_time(submit_time) for submit_time in self._submission_parts)
296
- return max((t for t in times if t is not None), default=None)
327
+ with self.using_EARs_cache():
328
+ times = (
329
+ self.get_end_time(submit_time) for submit_time in self._submission_parts
330
+ )
331
+ return max((t for t in times if t is not None), default=None)
332
+
333
+ @contextmanager
334
+ def using_EARs_cache(self):
335
+ """
336
+ A context manager to load and cache all EARs associated with this submission (and
337
+ its jobscripts).
338
+ """
339
+ if self.use_EARs_cache:
340
+ yield
341
+ else:
342
+ self.use_EARs_cache = True
343
+ try:
344
+ yield
345
+ finally:
346
+ self.use_EARs_cache = False
297
347
 
298
348
  @property
299
349
  def jobscripts(self) -> list[Jobscript]:
@@ -592,15 +642,18 @@ class Submission(JSONLike):
592
642
  """
593
643
  The IDs of all EARs in this submission.
594
644
  """
595
- return (i for js in self.jobscripts for i in js.all_EAR_IDs)
645
+ return (int(i) for js in self.jobscripts for i in js.all_EAR_IDs)
596
646
 
597
647
  @property
598
648
  @TimeIt.decorator
599
- def all_EARs(self) -> Iterable[ElementActionRun]:
649
+ def all_EARs(self) -> list[ElementActionRun]:
600
650
  """
601
651
  All EARs in this submission.
602
652
  """
603
- return (ear for js in self.jobscripts for ear in js.all_EARs)
653
+ if self.use_EARs_cache:
654
+ return list(self._EARs_cache.values())
655
+ else:
656
+ return self.workflow.get_EARs_from_IDs(self.all_EAR_IDs)
604
657
 
605
658
  @property
606
659
  @TimeIt.decorator
@@ -610,9 +663,10 @@ class Submission(JSONLike):
610
663
  @property
611
664
  @TimeIt.decorator
612
665
  def all_EARs_by_jobscript(self) -> list[list[ElementActionRun]]:
613
- ids = [i.all_EAR_IDs for i in self.jobscripts]
614
- all_EARs = {i.id_: i for i in self.workflow.get_EARs_from_IDs(self.all_EAR_IDs)}
615
- return [[all_EARs[i] for i in js_ids] for js_ids in ids]
666
+ all_EARs = {i.id_: i for i in self.all_EARs}
667
+ return [
668
+ [all_EARs[i] for i in js_ids] for js_ids in self.all_EARs_IDs_by_jobscript
669
+ ]
616
670
 
617
671
  @property
618
672
  @TimeIt.decorator
@@ -650,11 +704,12 @@ class Submission(JSONLike):
650
704
  """Get jobscripts that are active on this machine, and their active states."""
651
705
  # this returns: {JS_IDX: {BLOCK_IDX: {JS_ELEMENT_IDX: STATE}}}
652
706
  # TODO: query the scheduler once for all jobscripts?
653
- return {
654
- js.index: act_states
655
- for js in self.jobscripts
656
- if (act_states := js.get_active_states(as_json=as_json))
657
- }
707
+ with self.using_EARs_cache():
708
+ return {
709
+ js.index: act_states
710
+ for js in self.jobscripts
711
+ if (act_states := js.get_active_states(as_json=as_json))
712
+ }
658
713
 
659
714
  @TimeIt.decorator
660
715
  def _write_scripts(
@@ -1,4 +1,7 @@
1
+ from contextlib import contextmanager
1
2
  from pathlib import Path
3
+ import sys
4
+ from typing import Any
2
5
 
3
6
 
4
7
  def resolve_path(path):
@@ -10,3 +13,19 @@ def resolve_path(path):
10
13
  """
11
14
  # TODO: this only seems to be used in a test; remove?
12
15
  return Path.cwd() / Path(path).resolve() # cwd is ignored if already absolute
16
+
17
+
18
+ @contextmanager
19
+ def override_module_attrs(module_name: str, overrides: dict[str, Any]):
20
+ """Context manager to temporarily override module-level attributes. The module must be
21
+ imported (i.e. within `sys.modules`)."""
22
+
23
+ module = sys.modules[module_name]
24
+ original_values = {k: getattr(module, k) for k in overrides}
25
+ try:
26
+ for k, v in overrides.items():
27
+ setattr(module, k, v)
28
+ yield
29
+ finally:
30
+ for k, v in original_values.items():
31
+ setattr(module, k, v)
@@ -172,25 +172,32 @@ def test_MPS_latin_hypercube_sequence_values():
172
172
 
173
173
  def test_MPS_latin_hypercube_sequence_bounds():
174
174
 
175
- bounds = {"inputs.a": [16789.2, 17812.5]}
175
+ bounds = {
176
+ "inputs.a": {"extent": [16789.2, 17812.5], "scaling": "linear"},
177
+ "inputs.c": {"extent": [1.0e-10, 1.0e-5], "scaling": "log"},
178
+ }
176
179
 
177
180
  mps = hf.MultiPathSequence.from_latin_hypercube(
178
- paths=["inputs.a", "inputs.b"],
181
+ paths=["inputs.a", "inputs.b", "inputs.c"],
179
182
  num_samples=10,
180
183
  bounds=bounds,
181
184
  )
182
185
 
183
186
  vals_arr = np.array(mps.values)
184
- assert vals_arr.shape == (2, 10)
185
187
 
186
- vals_0 = vals_arr[0]
187
- bounds_0 = list(bounds.values())[0]
188
+ assert vals_arr.shape == (3, 10)
188
189
 
189
- vals_1 = vals_arr[1]
190
- bounds_1 = [0, 1]
190
+ vals_a = vals_arr[0]
191
+ vals_b = vals_arr[1]
192
+ vals_c = vals_arr[2]
191
193
 
192
- assert np.logical_and(vals_0 > bounds_0[0], vals_0 < bounds_0[1]).all()
193
- assert np.logical_and(vals_1 > bounds_1[0], vals_1 < bounds_1[1]).all()
194
+ extent_a = bounds["inputs.a"]["extent"]
195
+ extent_b = [0.0, 1.0]
196
+ extent_c = bounds["inputs.c"]["extent"]
197
+
198
+ assert np.logical_and(vals_a > extent_a[0], vals_a < extent_a[1]).all()
199
+ assert np.logical_and(vals_b > extent_b[0], vals_b < extent_b[1]).all()
200
+ assert np.logical_and(vals_c > extent_c[0], vals_c < extent_c[1]).all()
194
201
 
195
202
 
196
203
  def test_MPS_move_from_sequences_list():
@@ -6,7 +6,11 @@ from typing import cast, TYPE_CHECKING
6
6
  import numpy as np
7
7
  import zarr # type: ignore
8
8
  import pytest
9
- from hpcflow.sdk.core.test_utils import make_test_data_YAML_workflow, make_workflow
9
+ from hpcflow.sdk.core.test_utils import (
10
+ make_schemas,
11
+ make_test_data_YAML_workflow,
12
+ make_workflow,
13
+ )
10
14
  from hpcflow.sdk.persistence.json import (
11
15
  JSONPersistentStore,
12
16
  JsonStoreElement,
@@ -551,3 +555,38 @@ def test_zarr_encode_decode_jobscript_block_dependencies_large_one_to_one():
551
555
  arr = ZarrPersistentStore._encode_jobscript_block_dependencies(deps_t)
552
556
  deps_rt = ZarrPersistentStore._decode_jobscript_block_dependencies(arr)
553
557
  assert deps_rt == deps
558
+
559
+
560
+ @pytest.mark.parametrize(
561
+ "array",
562
+ (
563
+ np.array([]),
564
+ np.empty(0),
565
+ np.empty((0, 1, 2)),
566
+ np.array([1, 2, 3]),
567
+ np.array([[1, 2, 3], [4, 5, 6]]),
568
+ ),
569
+ )
570
+ def test_zarr_save_persistent_array_shape(null_config, tmp_path, array):
571
+ s1 = make_schemas(({"p1": None}, ()))
572
+ t1 = hf.Task(schema=s1, inputs={"p1": array})
573
+ wk = hf.Workflow.from_template_data(
574
+ template_name="test_save_empty_array",
575
+ tasks=[t1],
576
+ path=tmp_path,
577
+ )
578
+ assert array.shape == wk.tasks[0].elements[0].get("inputs.p1")[:].shape
579
+
580
+
581
+ def test_zarr_single_chunk_threshold(null_config, tmp_path):
582
+ # test very large arrays (> ~1 GB) are saved using multiple chunks
583
+ array = np.zeros(
584
+ 268_435_456
585
+ ) # ~ 2.147483647 GB; greater than blosc's max buffer size
586
+ s1 = make_schemas(({"p1": None}, ()))
587
+ t1 = hf.Task(schema=s1, inputs={"p1": array})
588
+ wk = hf.Workflow.from_template_data(
589
+ template_name="test_large_array",
590
+ tasks=[t1],
591
+ path=tmp_path,
592
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hpcflow-new2
3
- Version: 0.2.0a222
3
+ Version: 0.2.0a227
4
4
  Summary: Computational workflow management
5
5
  License: MPL-2.0
6
6
  Author: aplowman
@@ -1,7 +1,7 @@
1
1
  hpcflow/__init__.py,sha256=WIETuRHeOp2SqUqHUzpjQ-lk9acbYv-6aWOhZPRdlhs,64
2
2
  hpcflow/__pyinstaller/__init__.py,sha256=YOzBlPSck6slucv6lJM9K80JtsJWxXRL00cv6tRj3oc,98
3
3
  hpcflow/__pyinstaller/hook-hpcflow.py,sha256=P2b-8QdQqkSS7cJB6CB3CudUuJ9iZzTh2fQF4hNdCa4,1118
4
- hpcflow/_version.py,sha256=phew7JkOXGWTg0xqEm-B1aQnoPYpQVwtKpvMwtWxwkU,26
4
+ hpcflow/_version.py,sha256=Jgo3POaN0i9GWVRAfP7uEOtWUMY-0Fis95oTeRaSmHQ,26
5
5
  hpcflow/app.py,sha256=gl2viVS65PbpDhUp2DARaYHFDqDWQjuoyB3ikrCNRW4,1367
6
6
  hpcflow/cli.py,sha256=G2J3D9v6MnMWOWMMWK6UEKLn_6wnV9lT_qygEBBxg-I,66
7
7
  hpcflow/data/demo_data_manifest/__init__.py,sha256=Hsq0jT8EXM13wu1MpGy5FQgyuz56ygep4VWOnulFn50,41
@@ -64,8 +64,8 @@ hpcflow/data/workflows/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
64
64
  hpcflow/data/workflows/workflow_1.yaml,sha256=lF7Re2SVc_5gQk5AwB0gXaq-n-T5ia4su3zNQ9oMRV0,220
65
65
  hpcflow/examples.ipynb,sha256=cLKp4QsxwwMXRanDnfWY9kqsV23q6G4raOpu6IZXnMw,28553
66
66
  hpcflow/sdk/__init__.py,sha256=BEbIz8adCAX06ZmvEcquEu29PmQDvVP1I1qn_q78dYc,6289
67
- hpcflow/sdk/app.py,sha256=ZFMnEV43dlZlnTpJNzBsfRZ1ndiMGyeJ3hKxlhrm39I,146726
68
- hpcflow/sdk/cli.py,sha256=C_7Ts6TbspTZtUXlPwF37Vt-t71NABJ_2zNpZLYFPUE,45563
67
+ hpcflow/sdk/app.py,sha256=UWnpxdEqVD07VE-Rr23xhVHesF3zZI6ryLJ1-yXKp7M,146820
68
+ hpcflow/sdk/cli.py,sha256=qMFACsIA32gD2oLC8_FD9JjFYgidCoGxOH-L3qM2xx0,45818
69
69
  hpcflow/sdk/cli_common.py,sha256=MO0TbC280JdZTLnxSV15wE7PDNx83sotpJsyAN1E2F4,10458
70
70
  hpcflow/sdk/config/__init__.py,sha256=pkufl_CXm_Kswk1x7LTavuplWjUbUCitK0AJ2E60gwM,137
71
71
  hpcflow/sdk/config/callbacks.py,sha256=cuW31ZW0YTOi5K5P-NYHuiK1YLsglMEH3c_u6mddVE8,8078
@@ -81,7 +81,7 @@ hpcflow/sdk/core/cache.py,sha256=6b0emvJGP-yf5UxKmEY_s4_gdyUfaNaXafq-qQaoBfU,817
81
81
  hpcflow/sdk/core/command_files.py,sha256=LcrK7wjQnxmTDUrsC0Xgbjsy1NGodf62LrrTSNAAxxE,24942
82
82
  hpcflow/sdk/core/commands.py,sha256=vFyZ9Cv_tvGqSGtVEnAHAeXA-HAxWoF0nmf1e0bFCvs,15993
83
83
  hpcflow/sdk/core/element.py,sha256=pPRezx0HATKH55qEstXp1xOG4S8oGLVnJ3sQcEmLsXo,68152
84
- hpcflow/sdk/core/enums.py,sha256=BV_cFqTWkME3zVLswmb21QXeIiCBb9_9avr6VIhL3HM,4387
84
+ hpcflow/sdk/core/enums.py,sha256=scp7b7gbFSefu1bks2M2u6amq6G_lB_j_h8U_TPM4bU,5316
85
85
  hpcflow/sdk/core/environment.py,sha256=m0Qp4Y1XcPJ1yPrdHp6RJv9_H61nA9Qo2ZJV_-wGne8,7959
86
86
  hpcflow/sdk/core/errors.py,sha256=8PxlFewrRZU2MSPoNUiQ2jAX9iQTr9-Qr-O5hDywU_A,28869
87
87
  hpcflow/sdk/core/execute.py,sha256=Gc2SauA4QNA1W4Gi2FYoKoqkoJtGg57cgn1p87WNCIs,7221
@@ -89,7 +89,7 @@ hpcflow/sdk/core/json_like.py,sha256=tzCpaarGCHr_NXA-vLcX1jrDYlWbZuAbUPjR6jREhX8
89
89
  hpcflow/sdk/core/loop.py,sha256=vs7pNVe3Io5zNO6Xl_Tqmra9mCJUw8IESLZC4p2QFvo,50084
90
90
  hpcflow/sdk/core/loop_cache.py,sha256=C0t9UpvHFdTmw3bViZ2wccjZWtkbSbN9APvcwzBSREg,10801
91
91
  hpcflow/sdk/core/object_list.py,sha256=WvUBb-2DA7-7_7oNoPykSsTihVbGB-1cgHh8W78IzKI,29882
92
- hpcflow/sdk/core/parameters.py,sha256=5iuqTCz9ZxQnhgDElDIRBB32PDSHnqMJ1701v-M5GVI,98597
92
+ hpcflow/sdk/core/parameters.py,sha256=BlRSdPp_fA4jOfBmaIZ45Y-jGouMYz4tnn5J8Pcmv4w,100384
93
93
  hpcflow/sdk/core/rule.py,sha256=LiNyKp09a6TRGJvtiB1CcNqgyAIuPhuSW2RVfDWf5FE,6274
94
94
  hpcflow/sdk/core/run_dir_files.py,sha256=yh49BSlswqqyr97x5nDT87qu2MlOLMu7Arzl_B-fxy0,2061
95
95
  hpcflow/sdk/core/skip_reason.py,sha256=xPm8bd8sbPpiZ3sx9ZeW0jxgRQ0TBokSv0XIJKrEW2Y,113
@@ -117,7 +117,7 @@ hpcflow/sdk/helper/helper.py,sha256=T45BEwhPgTlwvqDNbT4Us4UVmdRKRoszIMt8a9B69kE,
117
117
  hpcflow/sdk/helper/watcher.py,sha256=aQyuSnBiPhsp1ijBylwR5mxjIqlwD4rT_zH9DYc83no,5744
118
118
  hpcflow/sdk/log.py,sha256=_wqB0qt65nt4oNNV6eyD39bNZyciyH6biyyOgU_MViQ,8285
119
119
  hpcflow/sdk/persistence/__init__.py,sha256=1e6nVADj6xEsE7XGXHaIsHZur4IkUJAxLuxBlvHi0CE,408
120
- hpcflow/sdk/persistence/base.py,sha256=y7L7FFyuTJvy9K_CRFZZH8Cost2seiNz_KKaKZ2XREo,92727
120
+ hpcflow/sdk/persistence/base.py,sha256=WBkVpbFsNiGjhSNTqmlBs3GO_3yMWCGufSbATYaNlbE,92811
121
121
  hpcflow/sdk/persistence/defaults.py,sha256=hpZk2R9HFqo7YTGaIlts6CGBRzQLn-JXebWc2ZHLBe4,129
122
122
  hpcflow/sdk/persistence/discovery.py,sha256=jEREI651S3FK-Qoz150oIHP72dsuhCFawXRQIy6j_WQ,1320
123
123
  hpcflow/sdk/persistence/json.py,sha256=QUa_xwgcspZWXi6zlIusVRnh4TzDa4xU5UCDcCU6BXE,36746
@@ -125,11 +125,11 @@ hpcflow/sdk/persistence/pending.py,sha256=JB42Emk2cN639bPlNtQtoFhe5WXGWDJeFm7aFU
125
125
  hpcflow/sdk/persistence/store_resource.py,sha256=P-VZF7gMIsVvgOsHJEplK1Se4jHsaAqbdoKoQo-7_LU,5903
126
126
  hpcflow/sdk/persistence/types.py,sha256=c4vd8RW8bg8Meoz1HNXjGT8Za9Hpfl1Sxp4pLAgdV0g,9125
127
127
  hpcflow/sdk/persistence/utils.py,sha256=TuQp0BKwiy1-5WTgiMaYXn00bZb6HWHAjs4BBFE9khw,1804
128
- hpcflow/sdk/persistence/zarr.py,sha256=yCsiNteaok-eM9x5jogeDoCPdnIpx1Yv7LgYm8qoVeA,88267
128
+ hpcflow/sdk/persistence/zarr.py,sha256=ozJSx0W8L5yVRVvF7gM7M8psF5vd_-dNDnXNa-Rh3sI,88979
129
129
  hpcflow/sdk/runtime.py,sha256=vPNu4_DXYMnRyJenkVLq8yt6_XG4tCuwX69Oew4OWmo,11269
130
130
  hpcflow/sdk/submission/__init__.py,sha256=79xJXVkddsuj3uJz3BV9iOnAV7vCeJvLrhKccOA_dnU,67
131
- hpcflow/sdk/submission/enums.py,sha256=DykNHISQDmfa0oDggj6iIKugeFXJd8iF9NwqV3MN2oA,2049
132
- hpcflow/sdk/submission/jobscript.py,sha256=QvvJAwOAYQ6gkNkRAU1Ke4zJnQd6OneMUkAJapmPMGw,90846
131
+ hpcflow/sdk/submission/enums.py,sha256=W6KDSkDBs-DYj5yZeU0Yt42R7LmomHKJTtPh6jPx3aE,1667
132
+ hpcflow/sdk/submission/jobscript.py,sha256=xxrrcYnmmzyt1nZadIPLdO-ByHYibUJgHk5xfxm7onE,91000
133
133
  hpcflow/sdk/submission/schedulers/__init__.py,sha256=E-XBhSMflInTrQ5ZBUy8Hhq-JtAuIcCx6IM_7fI1Axw,9291
134
134
  hpcflow/sdk/submission/schedulers/direct.py,sha256=b_LS_xB0vu2tRHs-Xd08-16PC-nojRB6aNXwT9qtv0A,7493
135
135
  hpcflow/sdk/submission/schedulers/sge.py,sha256=WRohm-1eBEcadcKGg5QPd1DHvGEUV759IVtLbbDx2qU,13927
@@ -140,13 +140,13 @@ hpcflow/sdk/submission/shells/base.py,sha256=orjshO0jBmcpt2frMk41Cyc6CeFz7mjiiYu
140
140
  hpcflow/sdk/submission/shells/bash.py,sha256=x-0hBRX1OlpylWcrHlSS72Bxw8us2ZY7m2PbgWml0-c,16351
141
141
  hpcflow/sdk/submission/shells/os_version.py,sha256=_RK5A56png1Zk4_rzumy17xZ4xVguuPMS0Mll99SKaA,3521
142
142
  hpcflow/sdk/submission/shells/powershell.py,sha256=OFxSEC9Quh-WDFOeYNCQLX_vg-VzO8KdpsYCS9AQfo0,11725
143
- hpcflow/sdk/submission/submission.py,sha256=nbbSxf6Kkn2omsZLk5ZQ1PiW4mMttxR_EH7KPLOAXCQ,51985
143
+ hpcflow/sdk/submission/submission.py,sha256=_sCV88L6yFE4BWNpnDjMYzabD_i95IiYqPos0kDOj2s,53659
144
144
  hpcflow/sdk/submission/types.py,sha256=8xYzjrFbRlTLtZoOwZJZeY3mDI4nCJlLSJ4ekbDoGVI,4220
145
145
  hpcflow/sdk/typing.py,sha256=gu2h7a0RJs_r5UtV8AFVh9TNjo8kGe1n6UOxeH3zdgg,5060
146
146
  hpcflow/sdk/utils/arrays.py,sha256=7znIaqfl_bGElZ6U_da0vumWPez6ITyNrSDu_AlACDI,2084
147
147
  hpcflow/sdk/utils/deferred_file.py,sha256=LYIjTPjAmbOflwUaq_-o-O6DZJCQzRHX8dl_rIWTO5c,1381
148
148
  hpcflow/sdk/utils/hashing.py,sha256=40H7yO-oYgrt6RCdlTDDzJV9YIGiQeDx6zGpMJrRKZo,484
149
- hpcflow/sdk/utils/patches.py,sha256=jpbodcOjhE5ccrHPz5-L-r9yC7s2ymQccIdJi6Zd2Jo,398
149
+ hpcflow/sdk/utils/patches.py,sha256=sVK8TjeeIEBgngm5PobxkKcIiuDORLaOzI_8ua1H3bw,988
150
150
  hpcflow/sdk/utils/strings.py,sha256=MGdVeQpEZjo_9_VSMNI2ArAesQpgAx4-RkHL7IAQInQ,1966
151
151
  hpcflow/tests/api/test_api.py,sha256=h0HT9W0Jd1pChrXYaBOVwGThaI3slGkloS0sbq2YX88,962
152
152
  hpcflow/tests/conftest.py,sha256=xtSqhOxjZYioiAPvrKwf7NFicZoA4BR9Si4J1A8mWHw,4083
@@ -185,10 +185,10 @@ hpcflow/tests/unit/test_jobscript_unit.py,sha256=E_uszw3-3Lf2nBcIUZUngMMIgd0S1X6
185
185
  hpcflow/tests/unit/test_json_like.py,sha256=b0_BHW0zfBmc_J848s0ZcYIl4NJkTxNciiAMMRvUCtA,30434
186
186
  hpcflow/tests/unit/test_loop.py,sha256=pN2xVcHhDPYOxR0WA9knVP0F3_PQ3qiXu97wmX32Byc,90046
187
187
  hpcflow/tests/unit/test_meta_task.py,sha256=FEWLuUrP-6e6BO0CRJe2fQDDOZUjz1jfljL_-V7n5Gs,9592
188
- hpcflow/tests/unit/test_multi_path_sequences.py,sha256=6cVYD2e7jMt-m4sr3Z6ixnKewvs-s--slP344trKIWM,9079
188
+ hpcflow/tests/unit/test_multi_path_sequences.py,sha256=eodhujCQkN3DWSyrUHxNlcrIHSOOBE9BnGtRyQnEunc,9362
189
189
  hpcflow/tests/unit/test_object_list.py,sha256=uWBou5QCyScaM3DLqL_7RCguMdYZKMkeZR0V8Lpterk,3342
190
190
  hpcflow/tests/unit/test_parameter.py,sha256=_ImfDhmroXzPKwIuKiehsAw9qQPPHC9FQa8jN7OJqoA,7440
191
- hpcflow/tests/unit/test_persistence.py,sha256=cwQoa2czgZ0ZDtCRoRSdkfoFMgMQ5qjnef_mhsMWzrc,17472
191
+ hpcflow/tests/unit/test_persistence.py,sha256=bpBiL4YPMosbZk18Ug1JRqYJ69HHJzDYUff_2wDi-5w,18544
192
192
  hpcflow/tests/unit/test_resources.py,sha256=48zC3A5YaSu0MlLlGiohowDpVZNgORsikVX8mP5agvI,8011
193
193
  hpcflow/tests/unit/test_run.py,sha256=jFRC1-wV5G2ZCEU-KBKYeHVnJdDBUXX6MbxTrB-RJSE,6205
194
194
  hpcflow/tests/unit/test_run_directories.py,sha256=VA2xhDRe7agKO7q1Y8kP0101BstkjLiC1RReKrMI6jE,1088
@@ -218,8 +218,8 @@ hpcflow/tests/workflows/test_submission.py,sha256=SUbBUbD8C8LSulrI7aETkzP9RqED48
218
218
  hpcflow/tests/workflows/test_workflows.py,sha256=9z3rtXjA5iMOp4C0q4TkD_9kLzwourCY-obpeOtnNt0,18927
219
219
  hpcflow/tests/workflows/test_zip.py,sha256=MzEwsIAYV_1A3bD0XRo23zUwUKVzkkmNd8_cil6YdWQ,578
220
220
  hpcflow/viz_demo.ipynb,sha256=6D9uBbWK3oMfbaf93Tnv5riFPtW-2miUTWNr9kGcnd4,228913
221
- hpcflow_new2-0.2.0a222.dist-info/LICENSE,sha256=Xhxf_KsrJNJFGMogumZhXSTPhUOVHCWf7nU-TDzqg0E,16763
222
- hpcflow_new2-0.2.0a222.dist-info/METADATA,sha256=93L_wYR_GPmtZ6f2vhlM4QwFdppn3S1p6EjiupYAbx4,2663
223
- hpcflow_new2-0.2.0a222.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
224
- hpcflow_new2-0.2.0a222.dist-info/entry_points.txt,sha256=aoGtCnFdfPcXfBdu2zZyMOJoz6fPgdR0elqsgrE-USU,106
225
- hpcflow_new2-0.2.0a222.dist-info/RECORD,,
221
+ hpcflow_new2-0.2.0a227.dist-info/LICENSE,sha256=Xhxf_KsrJNJFGMogumZhXSTPhUOVHCWf7nU-TDzqg0E,16763
222
+ hpcflow_new2-0.2.0a227.dist-info/METADATA,sha256=Q7I7tq_F1uXnwwuKuvhGjdBbBs-AujyfsWgJ4KZ_NFg,2663
223
+ hpcflow_new2-0.2.0a227.dist-info/WHEEL,sha256=kLuE8m1WYU0Ig0_YEGrXyTtiJvKPpLpDEiChiNyei5Y,88
224
+ hpcflow_new2-0.2.0a227.dist-info/entry_points.txt,sha256=aoGtCnFdfPcXfBdu2zZyMOJoz6fPgdR0elqsgrE-USU,106
225
+ hpcflow_new2-0.2.0a227.dist-info/RECORD,,