parsl 2024.4.22__py3-none-any.whl → 2024.4.29__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 (28) hide show
  1. parsl/config.py +10 -1
  2. parsl/data_provider/zip.py +32 -0
  3. parsl/dataflow/dflow.py +33 -23
  4. parsl/executors/high_throughput/executor.py +7 -1
  5. parsl/providers/kubernetes/kube.py +20 -1
  6. parsl/tests/configs/local_threads_checkpoint_periodic.py +8 -10
  7. parsl/tests/conftest.py +12 -1
  8. parsl/tests/test_bash_apps/test_std_uri.py +128 -0
  9. parsl/tests/test_checkpointing/test_periodic.py +20 -33
  10. parsl/tests/test_htex/test_basic.py +2 -2
  11. parsl/tests/test_htex/test_missing_worker.py +0 -4
  12. parsl/tests/test_mpi_apps/test_resource_spec.py +2 -8
  13. parsl/tests/test_staging/test_zip_in.py +42 -0
  14. parsl/tests/test_staging/test_zip_to_zip.py +44 -0
  15. parsl/tests/unit/__init__.py +0 -0
  16. parsl/tests/unit/test_file.py +99 -0
  17. parsl/usage_tracking/api.py +66 -0
  18. parsl/usage_tracking/usage.py +39 -26
  19. parsl/version.py +1 -1
  20. {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/METADATA +2 -2
  21. {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/RECORD +28 -22
  22. {parsl-2024.4.22.data → parsl-2024.4.29.data}/scripts/exec_parsl_function.py +0 -0
  23. {parsl-2024.4.22.data → parsl-2024.4.29.data}/scripts/parsl_coprocess.py +0 -0
  24. {parsl-2024.4.22.data → parsl-2024.4.29.data}/scripts/process_worker_pool.py +0 -0
  25. {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/LICENSE +0 -0
  26. {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/WHEEL +0 -0
  27. {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/entry_points.txt +0 -0
  28. {parsl-2024.4.22.dist-info → parsl-2024.4.29.dist-info}/top_level.txt +0 -0
parsl/config.py CHANGED
@@ -10,11 +10,12 @@ from parsl.executors.threads import ThreadPoolExecutor
10
10
  from parsl.errors import ConfigurationError
11
11
  from parsl.dataflow.taskrecord import TaskRecord
12
12
  from parsl.monitoring import MonitoringHub
13
+ from parsl.usage_tracking.api import UsageInformation
13
14
 
14
15
  logger = logging.getLogger(__name__)
15
16
 
16
17
 
17
- class Config(RepresentationMixin):
18
+ class Config(RepresentationMixin, UsageInformation):
18
19
  """
19
20
  Specification of Parsl configuration options.
20
21
 
@@ -50,6 +51,9 @@ class Config(RepresentationMixin):
50
51
  of 1.
51
52
  run_dir : str, optional
52
53
  Path to run directory. Default is 'runinfo'.
54
+ std_autopath : function, optional
55
+ Sets the function used to generate stdout/stderr specifications when parsl.AUTO_LOGPATH is used. If no function
56
+ is specified, generates paths that look like: ``rundir/NNN/task_logs/X/task_{id}_{name}{label}.{out/err}``
53
57
  strategy : str, optional
54
58
  Strategy to use for scaling blocks according to workflow needs. Can be 'simple', 'htex_auto_scale', 'none'
55
59
  or `None`.
@@ -89,6 +93,7 @@ class Config(RepresentationMixin):
89
93
  retries: int = 0,
90
94
  retry_handler: Optional[Callable[[Exception, TaskRecord], float]] = None,
91
95
  run_dir: str = 'runinfo',
96
+ std_autopath: Optional[Callable] = None,
92
97
  strategy: Optional[str] = 'simple',
93
98
  strategy_period: Union[float, int] = 5,
94
99
  max_idletime: float = 120.0,
@@ -129,6 +134,7 @@ class Config(RepresentationMixin):
129
134
  self.usage_tracking = usage_tracking
130
135
  self.initialize_logging = initialize_logging
131
136
  self.monitoring = monitoring
137
+ self.std_autopath: Optional[Callable] = std_autopath
132
138
 
133
139
  @property
134
140
  def executors(self) -> Sequence[ParslExecutor]:
@@ -144,3 +150,6 @@ class Config(RepresentationMixin):
144
150
  if len(duplicates) > 0:
145
151
  raise ConfigurationError('Executors must have unique labels ({})'.format(
146
152
  ', '.join(['label={}'.format(repr(d)) for d in duplicates])))
153
+
154
+ def get_usage_information(self):
155
+ return {"executors_len": len(self.executors)}
@@ -42,6 +42,12 @@ class ZipFileStaging(Staging):
42
42
  """
43
43
 
44
44
  def can_stage_out(self, file: File) -> bool:
45
+ return self.is_zip_url(file)
46
+
47
+ def can_stage_in(self, file: File) -> bool:
48
+ return self.is_zip_url(file)
49
+
50
+ def is_zip_url(self, file: File) -> bool:
45
51
  logger.debug("archive provider checking File {}".format(repr(file)))
46
52
 
47
53
  # First check if this is the scheme we care about
@@ -76,6 +82,20 @@ class ZipFileStaging(Staging):
76
82
  app_fut = stage_out_app(zip_path, inside_path, working_dir, inputs=[file], _parsl_staging_inhibit=True, parent_fut=parent_fut)
77
83
  return app_fut
78
84
 
85
+ def stage_in(self, dm, executor, file, parent_fut):
86
+ assert file.scheme == 'zip'
87
+
88
+ zip_path, inside_path = zip_path_split(file.path)
89
+
90
+ working_dir = dm.dfk.executors[executor].working_dir
91
+
92
+ if working_dir:
93
+ file.local_path = os.path.join(working_dir, inside_path)
94
+
95
+ stage_in_app = _zip_stage_in_app(dm)
96
+ app_fut = stage_in_app(zip_path, inside_path, working_dir, outputs=[file], _parsl_staging_inhibit=True, parent_fut=parent_fut)
97
+ return app_fut._outputs[0]
98
+
79
99
 
80
100
  def _zip_stage_out(zip_file, inside_path, working_dir, parent_fut=None, inputs=[], _parsl_staging_inhibit=True):
81
101
  file = inputs[0]
@@ -93,6 +113,18 @@ def _zip_stage_out_app(dm):
93
113
  return parsl.python_app(executors=['_parsl_internal'], data_flow_kernel=dm.dfk)(_zip_stage_out)
94
114
 
95
115
 
116
+ def _zip_stage_in(zip_file, inside_path, working_dir, *, parent_fut, outputs, _parsl_staging_inhibit=True):
117
+ with filelock.FileLock(zip_file + ".lock"):
118
+ with zipfile.ZipFile(zip_file, mode='r') as z:
119
+ content = z.read(inside_path)
120
+ with open(outputs[0], "wb") as of:
121
+ of.write(content)
122
+
123
+
124
+ def _zip_stage_in_app(dm):
125
+ return parsl.python_app(executors=['_parsl_internal'], data_flow_kernel=dm.dfk)(_zip_stage_in)
126
+
127
+
96
128
  def zip_path_split(path: str) -> Tuple[str, str]:
97
129
  """Split zip: path into a zipfile name and a contained-file name.
98
130
  """
parsl/dataflow/dflow.py CHANGED
@@ -798,7 +798,6 @@ class DataFlowKernel:
798
798
  # be the original function wrapped with an in-task stageout wrapper), a
799
799
  # rewritten File object to be passed to task to be executed
800
800
 
801
- @typechecked
802
801
  def stageout_one_file(file: File, rewritable_func: Callable):
803
802
  if not self.check_staging_inhibited(kwargs):
804
803
  # replace a File with a DataFuture - either completing when the stageout
@@ -996,32 +995,16 @@ class DataFlowKernel:
996
995
  executor = random.choice(choices)
997
996
  logger.debug("Task {} will be sent to executor {}".format(task_id, executor))
998
997
 
999
- # The below uses func.__name__ before it has been wrapped by any staging code.
1000
-
1001
- label = app_kwargs.get('label')
1002
- for kw in ['stdout', 'stderr']:
1003
- if kw in app_kwargs:
1004
- if app_kwargs[kw] == parsl.AUTO_LOGNAME:
1005
- if kw not in ignore_for_cache:
1006
- ignore_for_cache += [kw]
1007
- app_kwargs[kw] = os.path.join(
1008
- self.run_dir,
1009
- 'task_logs',
1010
- str(int(task_id / 10000)).zfill(4), # limit logs to 10k entries per directory
1011
- 'task_{}_{}{}.{}'.format(
1012
- str(task_id).zfill(4),
1013
- func.__name__,
1014
- '' if label is None else '_{}'.format(label),
1015
- kw)
1016
- )
1017
-
1018
998
  resource_specification = app_kwargs.get('parsl_resource_specification', {})
1019
999
 
1020
1000
  task_record: TaskRecord
1021
- task_record = {'depends': [],
1001
+ task_record = {'args': app_args,
1002
+ 'depends': [],
1022
1003
  'dfk': self,
1023
1004
  'executor': executor,
1005
+ 'func': func,
1024
1006
  'func_name': func.__name__,
1007
+ 'kwargs': app_kwargs,
1025
1008
  'memoize': cache,
1026
1009
  'hashsum': None,
1027
1010
  'exec_fu': None,
@@ -1043,18 +1026,30 @@ class DataFlowKernel:
1043
1026
 
1044
1027
  self.update_task_state(task_record, States.unsched)
1045
1028
 
1029
+ for kw in ['stdout', 'stderr']:
1030
+ if kw in app_kwargs:
1031
+ if app_kwargs[kw] == parsl.AUTO_LOGNAME:
1032
+ if kw not in ignore_for_cache:
1033
+ ignore_for_cache += [kw]
1034
+ if self.config.std_autopath is None:
1035
+ app_kwargs[kw] = self.default_std_autopath(task_record, kw)
1036
+ else:
1037
+ app_kwargs[kw] = self.config.std_autopath(task_record, kw)
1038
+
1046
1039
  app_fu = AppFuture(task_record)
1040
+ task_record['app_fu'] = app_fu
1047
1041
 
1048
1042
  # Transform remote input files to data futures
1049
1043
  app_args, app_kwargs, func = self._add_input_deps(executor, app_args, app_kwargs, func)
1050
1044
 
1051
1045
  func = self._add_output_deps(executor, app_args, app_kwargs, app_fu, func)
1052
1046
 
1047
+ # Replace the function invocation in the TaskRecord with whatever file-staging
1048
+ # substitutions have been made.
1053
1049
  task_record.update({
1054
1050
  'args': app_args,
1055
1051
  'func': func,
1056
- 'kwargs': app_kwargs,
1057
- 'app_fu': app_fu})
1052
+ 'kwargs': app_kwargs})
1058
1053
 
1059
1054
  assert task_id not in self.tasks
1060
1055
 
@@ -1245,8 +1240,10 @@ class DataFlowKernel:
1245
1240
  self._checkpoint_timer.close()
1246
1241
 
1247
1242
  # Send final stats
1243
+ logger.info("Sending end message for usage tracking")
1248
1244
  self.usage_tracker.send_end_message()
1249
1245
  self.usage_tracker.close()
1246
+ logger.info("Closed usage tracking")
1250
1247
 
1251
1248
  logger.info("Closing job status poller")
1252
1249
  self.job_status_poller.close()
@@ -1440,6 +1437,19 @@ class DataFlowKernel:
1440
1437
  log_std_stream("Standard out", task_record['app_fu'].stdout)
1441
1438
  log_std_stream("Standard error", task_record['app_fu'].stderr)
1442
1439
 
1440
+ def default_std_autopath(self, taskrecord, kw):
1441
+ label = taskrecord['kwargs'].get('label')
1442
+ task_id = taskrecord['id']
1443
+ return os.path.join(
1444
+ self.run_dir,
1445
+ 'task_logs',
1446
+ str(int(task_id / 10000)).zfill(4), # limit logs to 10k entries per directory
1447
+ 'task_{}_{}{}.{}'.format(
1448
+ str(task_id).zfill(4),
1449
+ taskrecord['func_name'],
1450
+ '' if label is None else '_{}'.format(label),
1451
+ kw))
1452
+
1443
1453
 
1444
1454
  class DataFlowKernelLoader:
1445
1455
  """Manage which DataFlowKernel is active.
@@ -14,6 +14,7 @@ import math
14
14
  import warnings
15
15
 
16
16
  import parsl.launchers
17
+ from parsl.usage_tracking.api import UsageInformation
17
18
  from parsl.serialize import pack_res_spec_apply_message, deserialize
18
19
  from parsl.serialize.errors import SerializationError, DeserializationError
19
20
  from parsl.app.errors import RemoteExceptionWrapper
@@ -62,7 +63,7 @@ DEFAULT_LAUNCH_CMD = ("process_worker_pool.py {debug} {max_workers_per_node} "
62
63
  "--available-accelerators {accelerators}")
63
64
 
64
65
 
65
- class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin):
66
+ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageInformation):
66
67
  """Executor designed for cluster-scale
67
68
 
68
69
  The HighThroughputExecutor system has the following components:
@@ -818,4 +819,9 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin):
818
819
  logger.info("Unable to terminate Interchange process; sending SIGKILL")
819
820
  self.interchange_proc.kill()
820
821
 
822
+ self.interchange_proc.close()
823
+
821
824
  logger.info("Finished HighThroughputExecutor shutdown attempt")
825
+
826
+ def get_usage_information(self):
827
+ return {"mpi": self.enable_mpi_mode}
@@ -105,7 +105,26 @@ class KubernetesProvider(ExecutionProvider, RepresentationMixin):
105
105
  if not _kubernetes_enabled:
106
106
  raise OptionalModuleMissing(['kubernetes'],
107
107
  "Kubernetes provider requires kubernetes module and config.")
108
- config.load_kube_config()
108
+ try:
109
+ config.load_kube_config()
110
+ except config.config_exception.ConfigException:
111
+ # `load_kube_config` assumes a local kube-config file, and fails if not
112
+ # present, raising:
113
+ #
114
+ # kubernetes.config.config_exception.ConfigException: Invalid
115
+ # kube-config file. No configuration found.
116
+ #
117
+ # Since running a parsl driver script on a kubernetes cluster is a common
118
+ # pattern to enable worker-interchange communication, this enables an
119
+ # in-cluster config to be loaded if a kube-config file isn't found.
120
+ #
121
+ # Based on: https://github.com/kubernetes-client/python/issues/1005
122
+ try:
123
+ config.load_incluster_config()
124
+ except config.config_exception.ConfigException:
125
+ raise config.config_exception.ConfigException(
126
+ "Failed to load both kube-config file and in-cluster configuration."
127
+ )
109
128
 
110
129
  self.namespace = namespace
111
130
  self.image = image
@@ -1,13 +1,11 @@
1
1
  from parsl.config import Config
2
2
  from parsl.executors.threads import ThreadPoolExecutor
3
3
 
4
- config = Config(
5
- executors=[
6
- ThreadPoolExecutor(
7
- label='local_threads_checkpoint_periodic',
8
- max_threads=1
9
- )
10
- ],
11
- checkpoint_mode='periodic',
12
- checkpoint_period='00:00:05'
13
- )
4
+
5
+ def fresh_config():
6
+ tpe = ThreadPoolExecutor(label='local_threads_checkpoint_periodic', max_threads=1)
7
+ return Config(
8
+ executors=[tpe],
9
+ checkpoint_mode='periodic',
10
+ checkpoint_period='00:00:02'
11
+ )
parsl/tests/conftest.py CHANGED
@@ -3,8 +3,10 @@ import itertools
3
3
  import logging
4
4
  import os
5
5
  import pathlib
6
+ import random
6
7
  import re
7
8
  import shutil
9
+ import string
8
10
  import time
9
11
  import types
10
12
  import signal
@@ -139,7 +141,7 @@ def pytest_configure(config):
139
141
  )
140
142
  config.addinivalue_line(
141
143
  'markers',
142
- 'staging_required: Marks tests that require a staging provider, when there is no sharedFS)'
144
+ 'staging_required: Marks tests that require a staging provider, when there is no sharedFS'
143
145
  )
144
146
  config.addinivalue_line(
145
147
  'markers',
@@ -245,6 +247,7 @@ def load_dfk_local_module(request, pytestconfig, tmpd_cwd_session):
245
247
 
246
248
  if callable(local_teardown):
247
249
  local_teardown()
250
+ assert DataFlowKernelLoader._dfk is None, "Expected teardown to clear DFK"
248
251
 
249
252
  if local_config:
250
253
  if parsl.dfk() != dfk:
@@ -421,3 +424,11 @@ def try_assert():
421
424
  raise AssertionError("Bad assert call: no attempts or timeout period")
422
425
 
423
426
  yield _impl
427
+
428
+
429
+ @pytest.fixture
430
+ def randomstring():
431
+ def func(length=5, alphabet=string.ascii_letters):
432
+ return "".join(random.choice(alphabet) for _ in range(length))
433
+
434
+ return func
@@ -0,0 +1,128 @@
1
+ import logging
2
+ import parsl
3
+ import pytest
4
+ import zipfile
5
+
6
+ from functools import partial
7
+ from parsl.app.futures import DataFuture
8
+ from parsl.data_provider.files import File
9
+ from parsl.executors import ThreadPoolExecutor
10
+
11
+
12
+ @parsl.bash_app
13
+ def app_stdout(stdout=parsl.AUTO_LOGNAME):
14
+ return "echo hello"
15
+
16
+
17
+ def const_str(cpath, task_record, err_or_out):
18
+ return cpath
19
+
20
+
21
+ def const_with_cpath(autopath_specifier, content_path, caplog):
22
+ with parsl.load(parsl.Config(std_autopath=partial(const_str, autopath_specifier))):
23
+ fut = app_stdout()
24
+
25
+ # we don't have to wait for a result to check this attributes
26
+ assert fut.stdout is autopath_specifier
27
+
28
+ # there is no DataFuture to wait for in the str case: the model is that
29
+ # the stdout will be immediately available on task completion.
30
+ fut.result()
31
+
32
+ with open(content_path, "r") as file:
33
+ assert file.readlines() == ["hello\n"]
34
+
35
+ for record in caplog.records:
36
+ assert record.levelno < logging.ERROR
37
+
38
+ parsl.clear()
39
+
40
+
41
+ @pytest.mark.local
42
+ def test_std_autopath_const_str(caplog, tmpd_cwd):
43
+ """Tests str and tuple mode autopaths with constant autopath, which should
44
+ all be passed through unmodified.
45
+ """
46
+ cpath = str(tmpd_cwd / "CONST")
47
+ const_with_cpath(cpath, cpath, caplog)
48
+
49
+
50
+ @pytest.mark.local
51
+ def test_std_autopath_const_pathlike(caplog, tmpd_cwd):
52
+ cpath = tmpd_cwd / "CONST"
53
+ const_with_cpath(cpath, cpath, caplog)
54
+
55
+
56
+ @pytest.mark.local
57
+ def test_std_autopath_const_tuples(caplog, tmpd_cwd):
58
+ file = tmpd_cwd / "CONST"
59
+ cpath = (file, "w")
60
+ const_with_cpath(cpath, file, caplog)
61
+
62
+
63
+ class URIFailError(Exception):
64
+ pass
65
+
66
+
67
+ def fail_uri(task_record, err_or_out):
68
+ raise URIFailError("Deliberate failure in std stream filename generation")
69
+
70
+
71
+ @pytest.mark.local
72
+ def test_std_autopath_fail(caplog):
73
+ with parsl.load(parsl.Config(std_autopath=fail_uri)):
74
+ with pytest.raises(URIFailError):
75
+ app_stdout()
76
+
77
+ parsl.clear()
78
+
79
+
80
+ @parsl.bash_app
81
+ def app_both(stdout=parsl.AUTO_LOGNAME, stderr=parsl.AUTO_LOGNAME):
82
+ return "echo hello; echo goodbye >&2"
83
+
84
+
85
+ def zip_uri(base, task_record, err_or_out):
86
+ """Should generate Files in base.zip like app_both.0.out or app_both.123.err"""
87
+ zip_path = base / "base.zip"
88
+ file = f"{task_record['func_name']}.{task_record['id']}.{task_record['try_id']}.{err_or_out}"
89
+ return File(f"zip:{zip_path}/{file}")
90
+
91
+
92
+ @pytest.mark.local
93
+ def test_std_autopath_zip(caplog, tmpd_cwd):
94
+ with parsl.load(parsl.Config(run_dir=str(tmpd_cwd),
95
+ executors=[ThreadPoolExecutor(working_dir=str(tmpd_cwd))],
96
+ std_autopath=partial(zip_uri, tmpd_cwd))):
97
+ futs = []
98
+
99
+ for _ in range(10):
100
+ fut = app_both()
101
+
102
+ # assertions that should hold after submission
103
+ assert isinstance(fut.stdout, DataFuture)
104
+ assert fut.stdout.file_obj.url.startswith("zip")
105
+
106
+ futs.append(fut)
107
+
108
+ # Barrier for all the stageouts to complete so that we can
109
+ # poke at the zip file.
110
+ [(fut.stdout.result(), fut.stderr.result()) for fut in futs]
111
+
112
+ with zipfile.ZipFile(tmpd_cwd / "base.zip") as z:
113
+ for fut in futs:
114
+
115
+ assert fut.done(), "AppFuture should be done if stageout is done"
116
+
117
+ stdout_relative_path = f"app_both.{fut.tid}.0.stdout"
118
+ with z.open(stdout_relative_path) as f:
119
+ assert f.readlines() == [b'hello\n']
120
+
121
+ stderr_relative_path = f"app_both.{fut.tid}.0.stderr"
122
+ with z.open(stderr_relative_path) as f:
123
+ assert f.readlines()[-1] == b'goodbye\n'
124
+
125
+ for record in caplog.records:
126
+ assert record.levelno < logging.ERROR
127
+
128
+ parsl.clear()
@@ -1,16 +1,12 @@
1
- import argparse
2
- import time
3
-
4
1
  import pytest
5
2
 
6
3
  import parsl
7
4
  from parsl.app.app import python_app
8
- from parsl.tests.configs.local_threads_checkpoint_periodic import config
5
+ from parsl.tests.configs.local_threads_checkpoint_periodic import fresh_config
9
6
 
10
7
 
11
8
  def local_setup():
12
- global dfk
13
- dfk = parsl.load(config)
9
+ parsl.load(fresh_config())
14
10
 
15
11
 
16
12
  def local_teardown():
@@ -27,40 +23,31 @@ def slow_double(x, sleep_dur=1):
27
23
 
28
24
 
29
25
  def tstamp_to_seconds(line):
30
- print("Parsing line: ", line)
31
26
  f = line.partition(" ")[0]
32
27
  return float(f)
33
28
 
34
29
 
35
30
  @pytest.mark.local
36
- def test_periodic(n=4):
31
+ def test_periodic():
37
32
  """Test checkpointing with task_periodic behavior
38
33
  """
39
-
40
- d = {}
41
-
42
- print("Launching : ", n)
43
- for i in range(0, n):
44
- d[i] = slow_double(i)
45
- print("Done launching")
46
-
47
- for i in range(0, n):
48
- d[i].result()
49
- print("Done sleeping")
50
-
51
- time.sleep(16)
52
- dfk.cleanup()
34
+ h, m, s = map(int, parsl.dfk().config.checkpoint_period.split(":"))
35
+ assert h == 0, "Verify test setup"
36
+ assert m == 0, "Verify test setup"
37
+ assert s > 0, "Verify test setup"
38
+ sleep_for = s + 1
39
+ with parsl.dfk():
40
+ futs = [slow_double(sleep_for) for _ in range(4)]
41
+ [f.result() for f in futs]
53
42
 
54
43
  # Here we will check if the loglines came back with 5 seconds deltas
55
- print("Rundir: ", dfk.run_dir)
56
-
57
- with open("{}/parsl.log".format(dfk.run_dir), 'r') as f:
44
+ with open("{}/parsl.log".format(parsl.dfk().run_dir)) as f:
58
45
  log_lines = f.readlines()
59
- expected_msg = " Done checkpointing"
60
- expected_msg2 = " No tasks checkpointed in this pass"
61
-
62
- lines = [line for line in log_lines if expected_msg in line or expected_msg2 in line]
63
- assert len(lines) >= 3, "Insufficient checkpoint lines in logfile"
64
- deltas = [tstamp_to_seconds(line) for line in lines]
65
- assert deltas[1] - deltas[0] < 5.5, "Delta between checkpoints exceeded period"
66
- assert deltas[2] - deltas[1] < 5.5, "Delta between checkpoints exceeded period"
46
+ expected_msg = " Done checkpointing"
47
+ expected_msg2 = " No tasks checkpointed in this pass"
48
+
49
+ lines = [line for line in log_lines if expected_msg in line or expected_msg2 in line]
50
+ assert len(lines) >= 3, "Insufficient checkpoint lines in logfile"
51
+ deltas = [tstamp_to_seconds(line) for line in lines]
52
+ assert deltas[1] - deltas[0] < 5.5, "Delta between checkpoints exceeded period"
53
+ assert deltas[2] - deltas[1] < 5.5, "Delta between checkpoints exceeded period"
@@ -23,6 +23,6 @@ def dummy():
23
23
 
24
24
 
25
25
  @pytest.mark.local
26
- def test_that_it_fails():
26
+ def test_app():
27
27
  x = dummy()
28
- x.result()
28
+ assert x.result() is None
@@ -37,7 +37,3 @@ def test_that_it_fails():
37
37
  raise Exception("The app somehow ran without a valid worker")
38
38
 
39
39
  assert parsl.dfk().config.executors[0]._executor_bad_state.is_set()
40
-
41
- # htex needs shutting down explicitly because dfk.cleanup() will not
42
- # do that, as it is in bad state
43
- parsl.dfk().config.executors[0].shutdown()
@@ -25,17 +25,11 @@ from parsl.executors.high_throughput.mpi_prefix_composer import (
25
25
  EXECUTOR_LABEL = "MPI_TEST"
26
26
 
27
27
 
28
- def local_setup():
28
+ def local_config():
29
29
  config = fresh_config()
30
30
  config.executors[0].label = EXECUTOR_LABEL
31
31
  config.executors[0].max_workers_per_node = 1
32
- parsl.load(config)
33
-
34
-
35
- def local_teardown():
36
- logging.warning("Exiting")
37
- parsl.dfk().cleanup()
38
- parsl.clear()
32
+ return config
39
33
 
40
34
 
41
35
  @python_app
@@ -0,0 +1,42 @@
1
+ import parsl
2
+ import pytest
3
+ import random
4
+ import zipfile
5
+
6
+ from parsl.data_provider.files import File
7
+ from parsl.data_provider.zip import ZipAuthorityError, ZipFileStaging
8
+
9
+ from parsl.providers import LocalProvider
10
+ from parsl.channels import LocalChannel
11
+ from parsl.launchers import SimpleLauncher
12
+
13
+ from parsl.config import Config
14
+ from parsl.executors import HighThroughputExecutor
15
+
16
+ from parsl.tests.configs.htex_local import fresh_config as local_config
17
+
18
+
19
+ @parsl.python_app
20
+ def count_lines(file):
21
+ with open(file, "r") as f:
22
+ return len(f.readlines())
23
+
24
+
25
+ @pytest.mark.local
26
+ def test_zip_in(tmpd_cwd):
27
+ # basic test of zip file stage-in
28
+ zip_path = tmpd_cwd / "container.zip"
29
+ file_base = "data.txt"
30
+ zip_file = File(f"zip:{zip_path / file_base}")
31
+
32
+ # create a zip file containing one file with some abitrary number of lines
33
+ n_lines = random.randint(0, 1000)
34
+
35
+ with zipfile.ZipFile(zip_path, mode='w') as z:
36
+ with z.open(file_base, mode='w') as f:
37
+ for _ in range(n_lines):
38
+ f.write(b'someline\n')
39
+
40
+ app_future = count_lines(zip_file)
41
+
42
+ assert app_future.result() == n_lines
@@ -0,0 +1,44 @@
1
+ import parsl
2
+ import pytest
3
+ import random
4
+ import zipfile
5
+
6
+ from parsl.data_provider.files import File
7
+ from parsl.data_provider.zip import ZipAuthorityError, ZipFileStaging
8
+
9
+ from parsl.providers import LocalProvider
10
+ from parsl.channels import LocalChannel
11
+ from parsl.launchers import SimpleLauncher
12
+
13
+ from parsl.config import Config
14
+ from parsl.executors import HighThroughputExecutor
15
+
16
+ from parsl.tests.configs.htex_local import fresh_config as local_config
17
+
18
+
19
+ @parsl.python_app
20
+ def generate_lines(n: int, *, outputs):
21
+ with open(outputs[0], "w") as f:
22
+ for x in range(n):
23
+ # write numbered lines
24
+ f.write(str(x) + "\n")
25
+
26
+
27
+ @parsl.python_app
28
+ def count_lines(file):
29
+ with open(file, "r") as f:
30
+ return len(f.readlines())
31
+
32
+
33
+ @pytest.mark.local
34
+ def test_zip_pipeline(tmpd_cwd):
35
+ # basic test of zip file stage-in
36
+ zip_path = tmpd_cwd / "container.zip"
37
+ file_base = "data.txt"
38
+ zip_file = File(f"zip:{zip_path / file_base}")
39
+
40
+ n_lines = random.randint(0, 1000)
41
+ generate_fut = generate_lines(n_lines, outputs=[zip_file])
42
+ n_lines_out = count_lines(generate_fut.outputs[0]).result()
43
+
44
+ assert n_lines == n_lines_out
File without changes
@@ -0,0 +1,99 @@
1
+ import os
2
+ from unittest import mock
3
+
4
+ import pytest
5
+
6
+ from parsl import File
7
+
8
+ _MOCK_BASE = "parsl.data_provider.files."
9
+
10
+
11
+ @pytest.mark.local
12
+ @pytest.mark.parametrize("scheme", ("http", "https", "ftp", "ftps", "asdfasdf"))
13
+ def test_file_init_scheme(scheme):
14
+ basename = "some_base_name"
15
+ path = f"/some/path/1/2/3/{basename}"
16
+ fqdn = "some.fqdn.example.com"
17
+ exp_url = f"{scheme}://{fqdn}{path}"
18
+ f = File(exp_url)
19
+ assert f.url == exp_url, "Expected given url to be stored"
20
+ assert f.scheme == scheme
21
+ assert f.netloc == fqdn
22
+ assert f.path == path
23
+ assert f.filename == basename
24
+ assert f.local_path is None, "Expect only set by API consumer, not constructor"
25
+
26
+
27
+ @pytest.mark.local
28
+ @pytest.mark.parametrize("url", ("some weird :// url", "", "a"))
29
+ def test_file_init_file_url_fallback(url):
30
+ exp_url = "some weird :// url"
31
+ f = File(exp_url)
32
+ assert f.url == exp_url
33
+ assert not f.netloc, "invalid host, should be no netloc"
34
+ assert f.path == exp_url, "Should fail to fully parse, so path is whole url"
35
+ assert f.filename == exp_url.rsplit("/", 1)[-1]
36
+
37
+ assert f.scheme == "file"
38
+
39
+
40
+ @pytest.mark.local
41
+ def test_file_proxies_for_filepath(randomstring):
42
+ # verify (current) expected internal hookup
43
+ exp_filepath = randomstring()
44
+ with mock.patch(
45
+ f"{_MOCK_BASE}File.filepath", new_callable=mock.PropertyMock
46
+ ) as mock_fpath:
47
+ mock_fpath.return_value = exp_filepath
48
+ f = File("")
49
+ assert str(f) == exp_filepath
50
+ assert os.fspath(f) == exp_filepath
51
+
52
+
53
+ @pytest.mark.local
54
+ @pytest.mark.parametrize("scheme", ("file://", ""))
55
+ def test_file_filepath_local_path_is_priority(scheme, randomstring):
56
+ exp_path = "/some/local/path"
57
+ url = f"{scheme}{exp_path}"
58
+ f = File(url)
59
+
60
+ f.local_path = randomstring()
61
+ assert f.filepath == f.local_path
62
+
63
+ f.local_path = None
64
+ assert f.filepath == exp_path
65
+
66
+
67
+ @pytest.mark.local
68
+ def test_file_filepath_requires_local_accessible_path():
69
+ with pytest.raises(ValueError) as pyt_exc:
70
+ _ = File("http://").filepath
71
+
72
+ assert "No local_path" in str(pyt_exc.value), "Expected reason in exception"
73
+
74
+
75
+ @pytest.mark.local
76
+ @pytest.mark.parametrize("scheme", ("https", "ftps", "", "file", "asdfasdf"))
77
+ def test_file_repr(scheme):
78
+ netloc = "some.netloc"
79
+ filename = "some_file_name"
80
+ path = f"/some/path/{filename}"
81
+ if scheme:
82
+ url = f"{scheme}://{netloc}{path}"
83
+ else:
84
+ scheme = "file"
85
+ url = path
86
+
87
+ f = File(url)
88
+ r = repr(f)
89
+ assert r.startswith("<")
90
+ assert r.endswith(">")
91
+ assert f"<{type(f).__name__} " in r
92
+ assert f" at 0x{id(f):x}" in r
93
+ assert f" url={url}" in r
94
+ assert f" scheme={scheme}" in r
95
+ assert f" path={path}" in r
96
+ assert f" filename={filename}" in r
97
+
98
+ if scheme != "file":
99
+ assert f" netloc={netloc}" in r
@@ -0,0 +1,66 @@
1
+ import inspect
2
+
3
+ from parsl.utils import RepresentationMixin
4
+
5
+ from abc import abstractmethod
6
+ from functools import singledispatch
7
+ from typing import Any, List, Sequence
8
+
9
+
10
+ # Traverse the configuration hierarchy, returning a JSON component
11
+ # for each one. Configuration components which implement
12
+ # RepresentationMixin will be in the right form for inspecting
13
+ # object attributes. Configuration components which are lists or tuples
14
+ # are traversed in sequence. Other types default to reporting no
15
+ # usage information.
16
+
17
+ @singledispatch
18
+ def get_parsl_usage(obj) -> List[Any]:
19
+ return []
20
+
21
+
22
+ @get_parsl_usage.register
23
+ def get_parsl_usage_representation_mixin(obj: RepresentationMixin) -> List[Any]:
24
+ t = type(obj)
25
+ qualified_name = t.__module__ + "." + t.__name__
26
+
27
+ # me can contain anything that can be rendered as JSON
28
+ me: List[Any] = []
29
+
30
+ if isinstance(obj, UsageInformation):
31
+ # report rich usage information for this component
32
+ attrs = {'c': qualified_name}
33
+ attrs.update(obj.get_usage_information())
34
+ me = [attrs]
35
+ else:
36
+ # report the class name of this component
37
+ me = [qualified_name]
38
+
39
+ # unwrap typeguard-style unwrapping
40
+ init: Any = type(obj).__init__
41
+ if hasattr(init, '__wrapped__'):
42
+ init = init.__wrapped__
43
+
44
+ argspec = inspect.getfullargspec(init)
45
+
46
+ for arg in argspec.args[1:]: # skip first arg, self
47
+ arg_value = getattr(obj, arg)
48
+ d = get_parsl_usage(arg_value)
49
+ me += d
50
+
51
+ return me
52
+
53
+
54
+ @get_parsl_usage.register(list)
55
+ @get_parsl_usage.register(tuple)
56
+ def get_parsl_usage_sequence(obj: Sequence) -> List[Any]:
57
+ result = []
58
+ for v in obj:
59
+ result += get_parsl_usage(v)
60
+ return result
61
+
62
+
63
+ class UsageInformation:
64
+ @abstractmethod
65
+ def get_usage_information(self) -> dict:
66
+ pass
@@ -7,6 +7,7 @@ import socket
7
7
  import sys
8
8
  import platform
9
9
 
10
+ from parsl.usage_tracking.api import get_parsl_usage
10
11
  from parsl.utils import setproctitle
11
12
  from parsl.multiprocessing import ForkProcess
12
13
  from parsl.dataflow.states import States
@@ -17,6 +18,13 @@ logger = logging.getLogger(__name__)
17
18
  from typing import Callable
18
19
  from typing_extensions import ParamSpec
19
20
 
21
+ # protocol version byte: when (for example) compression parameters are changed
22
+ # that cannot be inferred from the compressed message itself, this version
23
+ # ID needs to imply those parameters.
24
+
25
+ # Earlier protocol versions: b'{' - the original pure-JSON protocol pre-March 2024
26
+ PROTOCOL_VERSION = b'1'
27
+
20
28
  P = ParamSpec("P")
21
29
 
22
30
 
@@ -32,7 +40,7 @@ def async_process(fn: Callable[P, None]) -> Callable[P, None]:
32
40
 
33
41
 
34
42
  @async_process
35
- def udp_messenger(domain_name: str, UDP_PORT: int, sock_timeout: int, message: str) -> None:
43
+ def udp_messenger(domain_name: str, UDP_PORT: int, sock_timeout: int, message: bytes) -> None:
36
44
  """Send UDP messages to usage tracker asynchronously
37
45
 
38
46
  This multiprocessing based messenger was written to overcome the limitations
@@ -46,16 +54,11 @@ def udp_messenger(domain_name: str, UDP_PORT: int, sock_timeout: int, message: s
46
54
  setproctitle("parsl: Usage tracking")
47
55
 
48
56
  try:
49
- encoded_message = bytes(message, "utf-8")
50
-
51
57
  UDP_IP = socket.gethostbyname(domain_name)
52
58
 
53
- if UDP_PORT is None:
54
- raise Exception("UDP_PORT is None")
55
-
56
59
  sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) # UDP
57
60
  sock.settimeout(sock_timeout)
58
- sock.sendto(encoded_message, (UDP_IP, UDP_PORT))
61
+ sock.sendto(message, (UDP_IP, UDP_PORT))
59
62
  sock.close()
60
63
 
61
64
  except socket.timeout:
@@ -102,7 +105,7 @@ class UsageTracker:
102
105
  self.procs = []
103
106
  self.dfk = dfk
104
107
  self.config = self.dfk.config
105
- self.uuid = str(uuid.uuid4())
108
+ self.correlator_uuid = str(uuid.uuid4())
106
109
  self.parsl_version = PARSL_VERSION
107
110
  self.python_version = "{}.{}.{}".format(sys.version_info.major,
108
111
  sys.version_info.minor,
@@ -130,22 +133,23 @@ class UsageTracker:
130
133
 
131
134
  return track
132
135
 
133
- def construct_start_message(self) -> str:
136
+ def construct_start_message(self) -> bytes:
134
137
  """Collect preliminary run info at the start of the DFK.
135
138
 
136
139
  Returns :
137
140
  - Message dict dumped as json string, ready for UDP
138
141
  """
139
- message = {'uuid': self.uuid,
142
+ message = {'correlator': self.correlator_uuid,
140
143
  'parsl_v': self.parsl_version,
141
144
  'python_v': self.python_version,
142
- 'os': platform.system(),
143
- 'os_v': platform.release(),
144
- 'start': time.time()}
145
+ 'platform.system': platform.system(),
146
+ 'start': int(time.time()),
147
+ 'components': get_parsl_usage(self.dfk._config)}
148
+ logger.debug(f"Usage tracking start message: {message}")
145
149
 
146
- return json.dumps(message)
150
+ return self.encode_message(message)
147
151
 
148
- def construct_end_message(self) -> str:
152
+ def construct_end_message(self) -> bytes:
149
153
  """Collect the final run information at the time of DFK cleanup.
150
154
 
151
155
  Returns:
@@ -153,20 +157,26 @@ class UsageTracker:
153
157
  """
154
158
  app_count = self.dfk.task_count
155
159
 
156
- site_count = len(self.dfk.config.executors)
157
-
158
160
  app_fails = self.dfk.task_state_counts[States.failed] + self.dfk.task_state_counts[States.dep_fail]
159
161
 
160
- message = {'uuid': self.uuid,
161
- 'end': time.time(),
162
- 't_apps': app_count,
163
- 'sites': site_count,
164
- 'failed': app_fails
165
- }
162
+ # the DFK is tangled into this code as a god-object, so it is
163
+ # handled separately from the usual traversal code, but presenting
164
+ # the same protocol-level report.
165
+ dfk_component = {'c': type(self.dfk).__module__ + "." + type(self.dfk).__name__,
166
+ 'app_count': app_count,
167
+ 'app_fails': app_fails}
168
+
169
+ message = {'correlator': self.correlator_uuid,
170
+ 'end': int(time.time()),
171
+ 'components': [dfk_component] + get_parsl_usage(self.dfk._config)}
172
+ logger.debug(f"Usage tracking end message (unencoded): {message}")
166
173
 
167
- return json.dumps(message)
174
+ return self.encode_message(message)
168
175
 
169
- def send_UDP_message(self, message: str) -> None:
176
+ def encode_message(self, obj):
177
+ return PROTOCOL_VERSION + json.dumps(obj).encode()
178
+
179
+ def send_UDP_message(self, message: bytes) -> None:
170
180
  """Send UDP message."""
171
181
  if self.tracking_enabled:
172
182
  try:
@@ -191,7 +201,10 @@ class UsageTracker:
191
201
  or won't respond to SIGTERM.
192
202
  """
193
203
  for proc in self.procs:
204
+ logger.debug("Joining usage tracking process %s", proc)
194
205
  proc.join(timeout=timeout)
195
206
  if proc.is_alive():
196
- logger.info("Usage tracking process did not end itself; sending SIGKILL")
207
+ logger.warning("Usage tracking process did not end itself; sending SIGKILL")
197
208
  proc.kill()
209
+
210
+ proc.close()
parsl/version.py CHANGED
@@ -3,4 +3,4 @@
3
3
  Year.Month.Day[alpha/beta/..]
4
4
  Alphas will be numbered like this -> 2024.12.10a0
5
5
  """
6
- VERSION = '2024.04.22'
6
+ VERSION = '2024.04.29'
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsl
3
- Version: 2024.4.22
3
+ Version: 2024.4.29
4
4
  Summary: Simple data dependent workflows in Python
5
5
  Home-page: https://github.com/Parsl/parsl
6
- Download-URL: https://github.com/Parsl/parsl/archive/2024.04.22.tar.gz
6
+ Download-URL: https://github.com/Parsl/parsl/archive/2024.04.29.tar.gz
7
7
  Author: The Parsl Team
8
8
  Author-email: parsl@googlegroups.com
9
9
  License: Apache 2.0
@@ -1,6 +1,6 @@
1
1
  parsl/__init__.py,sha256=hq8rJmP59wzd9-yxaGcmq5gPpshOopH-Y1K0BkUBNY0,1843
2
2
  parsl/addresses.py,sha256=0wPo-4HjW0l4ndqCKLmSdbbSWE_3WK7pxRvqBEp-3Lk,4821
3
- parsl/config.py,sha256=E90pKPeagHpIdk9XYifHqSpTAaKdDQN59NPDi8PrTAc,7038
3
+ parsl/config.py,sha256=nT_XBE2ToRpp6jHCO1tFagGBxF4ShJEpxLx1BNvKHVw,7594
4
4
  parsl/curvezmq.py,sha256=FtZEYP1IWDry39cH-tOKUm9TnaR1U7krOmvVYpATcOk,6939
5
5
  parsl/errors.py,sha256=SzINzQFZDBDbj9l-DPQznD0TbGkNhHIRAPkcBCogf_A,1019
6
6
  parsl/log_utils.py,sha256=Ckeb7YiIoK0FA8dA5CsWJDe28i9Sf4sxhFwp__VsD3o,3274
@@ -8,7 +8,7 @@ parsl/multiprocessing.py,sha256=hakfdg-sgxEjwloZeDrt6EhzwdzecvjJhkPHHxh8lII,1938
8
8
  parsl/process_loggers.py,sha256=1G3Rfrh5wuZNo2X03grG4kTYPGOxz7hHCyG6L_A3b0A,1137
9
9
  parsl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  parsl/utils.py,sha256=DUPrl9ZdzwJzz2rmlRws77OMs43iQo_CT-Kr3uJs-fo,11202
11
- parsl/version.py,sha256=gYdglqtXYC99qyF-3eJE7_3lJq1gYemHXeDA1r1F6Cc,131
11
+ parsl/version.py,sha256=ZkQP1dxgTC3ws9wp7Qeu8XyvkriqFeJIGzAfXff0J3M,131
12
12
  parsl/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  parsl/app/app.py,sha256=wAHchJetgnicT1pn0NJKDeDX0lV3vDFlG8cQd_Ciax4,8522
14
14
  parsl/app/bash.py,sha256=VufxGROrlJB3dP03syNutU0x8rLzfI-gooWwBZ4FFQ8,5676
@@ -59,9 +59,9 @@ parsl/data_provider/globus.py,sha256=ss7R8XD64mR3p-y9lxNAb11rymiOlxI1hQzkPEW51ZI
59
59
  parsl/data_provider/http.py,sha256=nDHTW7XmJqAukWJjPRQjyhUXt8r6GsQ36mX9mv_wOig,2986
60
60
  parsl/data_provider/rsync.py,sha256=2-ZxqrT-hBj39x082NusJaBqsGW4Jd2qCW6JkVPpEl0,4254
61
61
  parsl/data_provider/staging.py,sha256=l-mAXFburs3BWPjkSmiQKuAgJpsxCG62yATPDbrafYI,4523
62
- parsl/data_provider/zip.py,sha256=qzsSpHYp3EUqS3LOaPiZPitr_9zu6oGybVaFOApVbuY,3348
62
+ parsl/data_provider/zip.py,sha256=50xvXms3UoEFc4v_w6joLAcCrcdKuOjIPoPLgUz1Jio,4498
63
63
  parsl/dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
- parsl/dataflow/dflow.py,sha256=2l8UCQ1m30cK3vxerUG5Ahzy_nthO_wU0280hUw3igw,65633
64
+ parsl/dataflow/dflow.py,sha256=oKlzZlhftcHN2l1Pgh6EhZx1kWTmD8WnfPTGlk1decI,66037
65
65
  parsl/dataflow/errors.py,sha256=w2vOt_ymzG2dOqJUO4IDcmTlrCIHlMZL8nBVyVq0O_8,2176
66
66
  parsl/dataflow/futures.py,sha256=XGgaoaT3N2U3vvZ7DEoLkGBrZscq_VzffZ9goLB87ko,6081
67
67
  parsl/dataflow/memoization.py,sha256=AsJO6c6cRp2ac6H8uGn2USlEi78_nX3QWvpxYt4XdYE,9583
@@ -79,7 +79,7 @@ parsl/executors/flux/executor.py,sha256=0omXRPvykdW5VZb8mwgBJjxVk4H6G8xoL5D_R9pu
79
79
  parsl/executors/flux/flux_instance_manager.py,sha256=tTEOATClm9SwdgLeBRWPC6D55iNDuh0YxqJOw3c3eQ4,2036
80
80
  parsl/executors/high_throughput/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
81
81
  parsl/executors/high_throughput/errors.py,sha256=vl69wLuVOplbKxHI9WphEGBExHWkTn5n8T9QhBXuNH0,380
82
- parsl/executors/high_throughput/executor.py,sha256=K82dr0CUKpbsX3eWPDC9vTfcUagHYPT5nij_zp2ku9I,37124
82
+ parsl/executors/high_throughput/executor.py,sha256=Er0QUG-HNatDPVBsLYZbbEStyNJsu4oE9UQ7LtQYH7w,37318
83
83
  parsl/executors/high_throughput/interchange.py,sha256=Rt6HyFvQYFuqUJ1ytXmUFTDIK9wOBm4l96IHoL6OFRc,31491
84
84
  parsl/executors/high_throughput/manager_record.py,sha256=w5EwzVqPtsLOyOW8jP44U3uaogt8H--tkwp7FNyKN_o,385
85
85
  parsl/executors/high_throughput/monitoring_info.py,sha256=3gQpwQjjNDEBz0cQqJZB6hRiwLiWwXs83zkQDmbOwxY,297
@@ -173,7 +173,7 @@ parsl/providers/grid_engine/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMp
173
173
  parsl/providers/grid_engine/grid_engine.py,sha256=xzh5clmo2xKQYm56ONkszfrx7aE_n2dfubEd5mLGexg,8565
174
174
  parsl/providers/grid_engine/template.py,sha256=a7iViKr8LXcFTPmsf_qQeVK5o_RekOAIlUOF0X1q-2M,273
175
175
  parsl/providers/kubernetes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
176
- parsl/providers/kubernetes/kube.py,sha256=uOr-sPgp73r1JFNc6wYhGhNGCvqkI8xBZznuJvfIfyk,12819
176
+ parsl/providers/kubernetes/kube.py,sha256=nhaGoOFHxfkNSpKMFd9UuctjcroTQrPYNRbxUxd05rQ,13810
177
177
  parsl/providers/kubernetes/template.py,sha256=VsRz6cmNaII-y4OdMT6sCwzQy95SJX6NMB0hmmFBhX4,50
178
178
  parsl/providers/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
179
179
  parsl/providers/local/local.py,sha256=7OYug8UaRr1QW2g3z-SYmdo1InSSZRP0tQOySvrkwbI,11372
@@ -197,7 +197,7 @@ parsl/serialize/facade.py,sha256=SpKGSpI8PQb3hhxuKRJUYoQoq284t5np9ouTpogKmtU,679
197
197
  parsl/serialize/proxystore.py,sha256=Yo-38odKlSKSuQfXU4cB5YM9sYV_302uPn1z_en19SU,1623
198
198
  parsl/tests/__init__.py,sha256=s_zoz7Ipgykh-QTQvctdpxENrMnmpXY8oe1bJbUmpqY,204
199
199
  parsl/tests/callables_helper.py,sha256=ceP1YYsNtrZgKT6MAIvpgdccEjQ_CpFEOnZBGHKGOx0,30
200
- parsl/tests/conftest.py,sha256=suQb6CmTixETxCES0gEDSTHXls5ps408Ni6RJoIaGJU,14284
200
+ parsl/tests/conftest.py,sha256=9AFrtThVnHasfyujoPw6iudxTvLlmqKQytbarzi3sWY,14579
201
201
  parsl/tests/test_aalst_patterns.py,sha256=fi6JHKidV7vMJLv2nnu_-Q0ngGLc89mRm8rFrGIwiUM,9615
202
202
  parsl/tests/test_callables.py,sha256=_QsdS8v2nGgOj4_X69NFHZOGUnqbOrOMCA9pCJColZw,1974
203
203
  parsl/tests/test_curvezmq.py,sha256=yyhlS4vmaZdMitiySoy4l_ih9H1bsPiN-tMdwIh3H20,12431
@@ -227,7 +227,7 @@ parsl/tests/configs/local_radical_mpi.py,sha256=K6V2HbARujaow5DBAUYSIWt1RaYbt898
227
227
  parsl/tests/configs/local_threads.py,sha256=oEnQSlom_JMLFX9_Ln49JAfOP3nSMbw8gTaDJo_NYfo,202
228
228
  parsl/tests/configs/local_threads_checkpoint.py,sha256=Ex7CI1Eo6wVRsem9uXTtbVJrkKc_vOYlVvCNa2RLpIo,286
229
229
  parsl/tests/configs/local_threads_checkpoint_dfk_exit.py,sha256=ECL1n0uBsXDuW3sLCmjiwe8s3Xd7EFIj5wt446w6bh4,254
230
- parsl/tests/configs/local_threads_checkpoint_periodic.py,sha256=XmeZTxIJFLq8KosB6dTEGBer7c1DAn1P6fpSOuAaEz8,314
230
+ parsl/tests/configs/local_threads_checkpoint_periodic.py,sha256=F2MVlwJZk-hkCgCrsAm_rKsv4mtLgsf5cyPsRoHm0ig,319
231
231
  parsl/tests/configs/local_threads_checkpoint_task_exit.py,sha256=zHKN68T-xhAVQwQp3fSWPIEcWOx-F7NBGZTOhF07iL8,256
232
232
  parsl/tests/configs/local_threads_ftp_in_task.py,sha256=c9odRbxgj1bM_ttpkWTh2Ch_MV7f5cmn-68BOjLeJ70,444
233
233
  parsl/tests/configs/local_threads_globus.py,sha256=NhY27cD4vcqLh762Ye0BINZnt63EmTyHXg7FQMffOBw,1097
@@ -302,11 +302,12 @@ parsl/tests/test_bash_apps/test_memoize_ignore_args.py,sha256=zBB4zNMZe4VZExwKZH
302
302
  parsl/tests/test_bash_apps/test_memoize_ignore_args_regr.py,sha256=zQDQktIA9bYdqciZ17bCCGZw7XCQf3XmGqDsZ4SU_e0,1362
303
303
  parsl/tests/test_bash_apps/test_multiline.py,sha256=stpMEv2eopGG-ietxjUtD5gYMOVpwPdLauDizjUfTdA,1082
304
304
  parsl/tests/test_bash_apps/test_pipeline.py,sha256=439Ml2bMn5NgiQiNcg4DPeWhsXK8WQaatvZy2PleQzQ,2444
305
+ parsl/tests/test_bash_apps/test_std_uri.py,sha256=cIQVc8tCN0jM32oXwEKzxkhZT20l259AriM_jmtHdI0,3808
305
306
  parsl/tests/test_bash_apps/test_stdout.py,sha256=3jVDVjUfoQMDzp-f1Ifr7B6ivMq7U5ZzxXU3ZyUKogU,2787
306
307
  parsl/tests/test_channels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
307
308
  parsl/tests/test_channels/test_large_output.py,sha256=PGeNSW_sN5mR7KF1hVL2CPfktydYxo4oNz1wVQ-ENN0,595
308
309
  parsl/tests/test_checkpointing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
309
- parsl/tests/test_checkpointing/test_periodic.py,sha256=Nb_3_eHYnMUJxYvI3y2Tum6cU07ZUtEawAtYsEsXPd0,1662
310
+ parsl/tests/test_checkpointing/test_periodic.py,sha256=8R9Plxdm-dUvaptJsw-pEKLOjO5q74lHnaB2kxbF5-M,1613
310
311
  parsl/tests/test_checkpointing/test_python_checkpoint_1.py,sha256=7p_q5aFYYoRQpYmkFekuLOsPgTaILbj5-MMVCDP3Bsg,745
311
312
  parsl/tests/test_checkpointing/test_python_checkpoint_2.py,sha256=f1s-qRzIzaCFJauEGU08fhFw6od3yGrMelk792WQuYI,1106
312
313
  parsl/tests/test_checkpointing/test_python_checkpoint_3.py,sha256=8Np2OpDeQ8sE1Hmd5rYZo1qgt2xOuR4t-d-41JyLCHI,823
@@ -333,14 +334,14 @@ parsl/tests/test_error_handling/test_serialization_fail.py,sha256=jIJBKK-ItVe8B1
333
334
  parsl/tests/test_error_handling/test_wrap_with_logs.py,sha256=AHQZj7wae759FOcAUeHZX47mx1MKOfDv2YBLnaDVW7Q,741
334
335
  parsl/tests/test_flowcontrol/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
335
336
  parsl/tests/test_htex/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
336
- parsl/tests/test_htex/test_basic.py,sha256=GIOF3cr6A87QDVMxeN0LrvJnXO2Nap0c-TH462OaBK4,464
337
+ parsl/tests/test_htex/test_basic.py,sha256=CTOXEkzrVW_kSp_scz3BGYwc1c6KYopTooXICiAsMso,469
337
338
  parsl/tests/test_htex/test_connected_blocks.py,sha256=0628oJ_0_aVsda5xuFwG3_3q8ZiEAM-sfIOINkUHQrk,1639
338
339
  parsl/tests/test_htex/test_cpu_affinity_explicit.py,sha256=tv12ojw4DdymlVBjVNnrFX7_mhwix2jnBLkRbKOQRao,1422
339
340
  parsl/tests/test_htex/test_disconnected_blocks.py,sha256=iga7wmhGACwUN6gkEFPw1dLodj6SzNZpevgSHNYSyjI,1856
340
341
  parsl/tests/test_htex/test_drain.py,sha256=BvPQIo0xx-z191eVR2rG51x22yzqD-6dLSH7bCAUhOg,2288
341
342
  parsl/tests/test_htex/test_htex.py,sha256=4dXtcthZQvgEDtMc00g6Pw7FnqNWB_0j8fuJqHKO-IE,3896
342
343
  parsl/tests/test_htex/test_manager_failure.py,sha256=gemQopZoDEoZLOvep5JZkY6tQlZoko8Z0Kmpj1-Gbws,1161
343
- parsl/tests/test_htex/test_missing_worker.py,sha256=oiDN3ylsf-72jmX-Y5OWA2kQWpbVbvmoSLnu2vnyZeY,976
344
+ parsl/tests/test_htex/test_missing_worker.py,sha256=R8FaUNnpmXY9E_ZCC797La4jWsJvp136xBhLY9yIgyU,818
344
345
  parsl/tests/test_htex/test_multiple_disconnected_blocks.py,sha256=L4vw_Mo-upp2p9-TyPDfluNJJQ2BxHHNXgS3xhhuE28,1993
345
346
  parsl/tests/test_htex/test_worker_failure.py,sha256=Uz-RHI-LK78FMjXUvrUFmo4iYfmpDVBUcBxxRb3UG9M,603
346
347
  parsl/tests/test_htex/test_zmq_binding.py,sha256=2-y8HZPzNLrumVqyqG9yZl-lqefSIpez2jr5Ghrtd80,3013
@@ -360,7 +361,7 @@ parsl/tests/test_mpi_apps/test_mpi_mode_disabled.py,sha256=bMO66LHEvdVsV8sMFxESF
360
361
  parsl/tests/test_mpi_apps/test_mpi_mode_enabled.py,sha256=V8dccWWVSCfKkWPJUaL1nHc7YeFL8KZhMB-xwIIKF4A,5034
361
362
  parsl/tests/test_mpi_apps/test_mpi_prefix.py,sha256=OJ95-95t7DmDF86uYCtT2iRcY2gn9LFH45OWyjjL2h8,1950
362
363
  parsl/tests/test_mpi_apps/test_mpi_scheduler.py,sha256=dZ8_mzLMx5Us7mra2nPfxaeWZnhZyqNJ8vPWkWHaEB8,6317
363
- parsl/tests/test_mpi_apps/test_resource_spec.py,sha256=S-78HjvieW3OVD-BXj8Ln5lSZV-q-E7Ai7LrDXdFIVM,3710
364
+ parsl/tests/test_mpi_apps/test_resource_spec.py,sha256=8AKIXWFq9Tat8Mj0IRp-epNGVGFTqcxqZgF7jiLjE4Q,3607
364
365
  parsl/tests/test_providers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
365
366
  parsl/tests/test_providers/test_cobalt_deprecation_warning.py,sha256=Fy5XXDkVs3KIX3tHyRjyReXEr35X1LWyEXcVXmWccDs,389
366
367
  parsl/tests/test_providers/test_local_provider.py,sha256=G6Fuko22SvAtD7xhfQv8k_8HtJuFhZ8aHYcWQt073Pg,6968
@@ -440,20 +441,25 @@ parsl/tests/test_staging/test_staging_ftp_in_task.py,sha256=kR2XrGvbvVFDpHg53Nnj
440
441
  parsl/tests/test_staging/test_staging_globus.py,sha256=ds8nDH5dNbI10FV_GxMHyVaY6GPnuPPzkX9IiqROLF0,2339
441
442
  parsl/tests/test_staging/test_staging_https.py,sha256=ESNuvdc_P5JoPaMjBM3ofi1mNJM0U6vahi9JgbCsrPw,3307
442
443
  parsl/tests/test_staging/test_staging_stdout.py,sha256=i4ksb9ehu-bKPgALxm6ADB12EQVTM_xusyFGmSwByLs,2025
444
+ parsl/tests/test_staging/test_zip_in.py,sha256=xLTXXAHgMyg5XAMTB_oq3oEhrNfq8JGMnuRu2vu5tdo,1143
443
445
  parsl/tests/test_staging/test_zip_out.py,sha256=qR_iK8wqKOxisMBD5MqKr2RoyoTUmRejAj_O3jgJA2A,3512
446
+ parsl/tests/test_staging/test_zip_to_zip.py,sha256=Hz3fYvj-ePpfV4Hh6Fs1vM8iZ14iRxPkJhaI0spr8dE,1168
444
447
  parsl/tests/test_threads/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
445
448
  parsl/tests/test_threads/test_configs.py,sha256=QA9YjIMAtZ2jmkfOWqBzEfzQQcFVCDizH7Qwiy2HIMQ,909
446
449
  parsl/tests/test_threads/test_lazy_errors.py,sha256=nGhYfCMHFZYSy6YJ4gnAmiLl9SfYs0WVnuvj8DXQ9bw,560
447
450
  parsl/tests/test_utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
448
451
  parsl/tests/test_utils/test_representation_mixin.py,sha256=kUZeIDwA2rlbJ3-beGzLLwf3dOplTMCrWJN87etHcyY,1633
452
+ parsl/tests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
453
+ parsl/tests/unit/test_file.py,sha256=vLycnYcv3bvSzL-FV8WdoibqTyb41BrH1LUYBavobsg,2850
449
454
  parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
450
- parsl/usage_tracking/usage.py,sha256=pSADeogWqvkYI_n2pojv4IWDEFAQ3KwXNx6LDTohMHQ,6684
451
- parsl-2024.4.22.data/scripts/exec_parsl_function.py,sha256=NtWNeBvRqksej38eRPw8zPBJ1CeW6vgaitve0tfz_qc,7801
452
- parsl-2024.4.22.data/scripts/parsl_coprocess.py,sha256=Y7Tc-h9WGui-YDe3w_h91w2Sm1JNL1gJ9kAV4PE_gw8,5722
453
- parsl-2024.4.22.data/scripts/process_worker_pool.py,sha256=31tyTtU7hrrsatGReuCbLM-3GWkaYK1bvlFE1MhKYQg,41253
454
- parsl-2024.4.22.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
455
- parsl-2024.4.22.dist-info/METADATA,sha256=qMMaRpBRciAOHn3sQoQd3udOTdP8-uWhfhIH8hUX2HU,4012
456
- parsl-2024.4.22.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
457
- parsl-2024.4.22.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
458
- parsl-2024.4.22.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
459
- parsl-2024.4.22.dist-info/RECORD,,
455
+ parsl/usage_tracking/api.py,sha256=V_IMgpxUloP--MtlZGY5Uf6SfHVQSPP8poa3PedOs_I,1822
456
+ parsl/usage_tracking/usage.py,sha256=IGNPVEo3YJ3SI48WGESrip7PiCqEAheQ0Kgu8dRgQag,7616
457
+ parsl-2024.4.29.data/scripts/exec_parsl_function.py,sha256=NtWNeBvRqksej38eRPw8zPBJ1CeW6vgaitve0tfz_qc,7801
458
+ parsl-2024.4.29.data/scripts/parsl_coprocess.py,sha256=Y7Tc-h9WGui-YDe3w_h91w2Sm1JNL1gJ9kAV4PE_gw8,5722
459
+ parsl-2024.4.29.data/scripts/process_worker_pool.py,sha256=31tyTtU7hrrsatGReuCbLM-3GWkaYK1bvlFE1MhKYQg,41253
460
+ parsl-2024.4.29.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
461
+ parsl-2024.4.29.dist-info/METADATA,sha256=77gJi-9dKlrSb6gATxE7Rhq8lVIdziv7RbmtMtHPnCA,4012
462
+ parsl-2024.4.29.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
463
+ parsl-2024.4.29.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
464
+ parsl-2024.4.29.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
465
+ parsl-2024.4.29.dist-info/RECORD,,