parsl 2025.5.26__py3-none-any.whl → 2025.6.2__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.
@@ -8,7 +8,7 @@ import warnings
8
8
  from collections import defaultdict
9
9
  from concurrent.futures import Future
10
10
  from dataclasses import dataclass
11
- from typing import Callable, Dict, List, Optional, Sequence, Set, Tuple, Union
11
+ from typing import Callable, Dict, List, Optional, Sequence, Tuple, Union
12
12
 
13
13
  import typeguard
14
14
 
@@ -363,7 +363,9 @@ class HighThroughputExecutor(BlockProviderExecutor, RepresentationMixin, UsageIn
363
363
 
364
364
  def validate_resource_spec(self, resource_specification: dict):
365
365
  if resource_specification:
366
- acceptable_fields: Set[str] = set() # add new resource spec field names here to make htex accept them
366
+ """HTEX supports the following *Optional* resource specifications:
367
+ priority: lower value is higher priority"""
368
+ acceptable_fields = {'priority'} # add new resource spec field names here to make htex accept them
367
369
  keys = set(resource_specification.keys())
368
370
  invalid_keys = keys - acceptable_fields
369
371
  if invalid_keys:
@@ -5,13 +5,13 @@ import logging
5
5
  import os
6
6
  import pickle
7
7
  import platform
8
- import queue
9
8
  import sys
10
9
  import threading
11
10
  import time
12
11
  from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, cast
13
12
 
14
13
  import zmq
14
+ from sortedcontainers import SortedList
15
15
 
16
16
  from parsl import curvezmq
17
17
  from parsl.addresses import tcp_url
@@ -131,7 +131,7 @@ class Interchange:
131
131
  self.hub_address = hub_address
132
132
  self.hub_zmq_port = hub_zmq_port
133
133
 
134
- self.pending_task_queue: queue.Queue[Any] = queue.Queue(maxsize=10 ** 6)
134
+ self.pending_task_queue: SortedList[Any] = SortedList(key=lambda tup: (tup[0], tup[1]))
135
135
 
136
136
  # count of tasks that have been received from the submit side
137
137
  self.task_counter = 0
@@ -196,13 +196,12 @@ class Interchange:
196
196
  eg. [{'task_id':<x>, 'buffer':<buf>} ... ]
197
197
  """
198
198
  tasks = []
199
- for _ in range(0, count):
200
- try:
201
- x = self.pending_task_queue.get(block=False)
202
- except queue.Empty:
203
- break
204
- else:
205
- tasks.append(x)
199
+ try:
200
+ for _ in range(count):
201
+ *_, task = self.pending_task_queue.pop()
202
+ tasks.append(task)
203
+ except IndexError:
204
+ pass
206
205
 
207
206
  return tasks
208
207
 
@@ -342,8 +341,15 @@ class Interchange:
342
341
  if self.task_incoming in self.socks and self.socks[self.task_incoming] == zmq.POLLIN:
343
342
  logger.debug("start task_incoming section")
344
343
  msg = self.task_incoming.recv_pyobj()
344
+
345
+ # Process priority, higher number = lower priority
346
+ resource_spec = msg.get('resource_spec', {})
347
+ priority = resource_spec.get('priority', float('inf'))
348
+ queue_entry = (-priority, -self.task_counter, msg)
349
+
345
350
  logger.debug("putting message onto pending_task_queue")
346
- self.pending_task_queue.put(msg)
351
+
352
+ self.pending_task_queue.add(queue_entry)
347
353
  self.task_counter += 1
348
354
  logger.debug(f"Fetched {self.task_counter} tasks so far")
349
355
 
@@ -378,23 +384,24 @@ class Interchange:
378
384
  return
379
385
 
380
386
  if msg['type'] == 'registration':
381
- # We set up an entry only if registration works correctly
382
- self._ready_managers[manager_id] = {'last_heartbeat': time.time(),
383
- 'idle_since': time.time(),
384
- 'block_id': None,
385
- 'start_time': msg['start_time'],
386
- 'max_capacity': 0,
387
- 'worker_count': 0,
388
- 'active': True,
389
- 'draining': False,
390
- 'parsl_version': msg['parsl_v'],
391
- 'python_version': msg['python_v'],
392
- 'tasks': []}
393
- self.connected_block_history.append(msg['block_id'])
394
-
395
- interesting_managers.add(manager_id)
396
- logger.info(f"Adding manager: {manager_id!r} to ready queue")
397
- m = self._ready_managers[manager_id]
387
+ ix_minor_py = self.current_platform['python_v'].rsplit(".", 1)[0]
388
+ ix_parsl_v = self.current_platform['parsl_v']
389
+ mgr_minor_py = msg['python_v'].rsplit(".", 1)[0]
390
+ mgr_parsl_v = msg['parsl_v']
391
+
392
+ m = ManagerRecord(
393
+ block_id=None,
394
+ start_time=msg['start_time'],
395
+ tasks=[],
396
+ worker_count=0,
397
+ max_capacity=0,
398
+ active=True,
399
+ draining=False,
400
+ last_heartbeat=time.time(),
401
+ idle_since=time.time(),
402
+ parsl_version=mgr_parsl_v,
403
+ python_version=msg['python_v'],
404
+ )
398
405
 
399
406
  # m is a ManagerRecord, but msg is a dict[Any,Any] and so can
400
407
  # contain arbitrary fields beyond those in ManagerRecord (and
@@ -405,23 +412,43 @@ class Interchange:
405
412
  logger.info(f"Registration info for manager {manager_id!r}: {msg}")
406
413
  self._send_monitoring_info(monitoring_radio, m)
407
414
 
408
- if (msg['python_v'].rsplit(".", 1)[0] != self.current_platform['python_v'].rsplit(".", 1)[0] or
409
- msg['parsl_v'] != self.current_platform['parsl_v']):
410
- logger.error(f"Manager {manager_id!r} has incompatible version info with the interchange")
411
- logger.debug("Setting kill event")
415
+ if (mgr_minor_py, mgr_parsl_v) != (ix_minor_py, ix_parsl_v):
412
416
  kill_event.set()
413
- e = VersionMismatch("py.v={} parsl.v={}".format(self.current_platform['python_v'].rsplit(".", 1)[0],
414
- self.current_platform['parsl_v']),
415
- "py.v={} parsl.v={}".format(msg['python_v'].rsplit(".", 1)[0],
416
- msg['parsl_v'])
417
- )
418
- result_package = {'type': 'result', 'task_id': -1, 'exception': serialize_object(e)}
417
+ e = VersionMismatch(
418
+ f"py.v={ix_minor_py} parsl.v={ix_parsl_v}",
419
+ f"py.v={mgr_minor_py} parsl.v={mgr_parsl_v}",
420
+ )
421
+ result_package = {
422
+ 'type': 'result',
423
+ 'task_id': -1,
424
+ 'exception': serialize_object(e),
425
+ }
419
426
  pkl_package = pickle.dumps(result_package)
420
427
  self.results_outgoing.send(pkl_package)
421
- logger.error("Sent failure reports, shutting down interchange")
428
+ logger.error(
429
+ "Manager has incompatible version info with the interchange;"
430
+ " sending failure reports and shutting down:"
431
+ f"\n Interchange: {e.interchange_version}"
432
+ f"\n Manager: {e.manager_version}"
433
+ )
434
+
422
435
  else:
423
- logger.info(f"Manager {manager_id!r} has compatible Parsl version {msg['parsl_v']}")
424
- logger.info(f"Manager {manager_id!r} has compatible Python version {msg['python_v'].rsplit('.', 1)[0]}")
436
+ # We really should update the associated data structure; but not
437
+ # at this time. *kicks can down the road*
438
+ assert m['block_id'] is not None, "Verified externally currently"
439
+
440
+ # set up entry only if we accept the registration
441
+ self._ready_managers[manager_id] = m
442
+ self.connected_block_history.append(m['block_id'])
443
+
444
+ interesting_managers.add(manager_id)
445
+
446
+ logger.info(
447
+ f"Registered manager {manager_id!r} (py{mgr_minor_py},"
448
+ f" {mgr_parsl_v}) and added to ready queue"
449
+ )
450
+ logger.debug("Manager %r -> %s", manager_id, m)
451
+
425
452
  elif msg['type'] == 'heartbeat':
426
453
  manager = self._ready_managers.get(manager_id)
427
454
  if manager:
@@ -461,10 +488,10 @@ class Interchange:
461
488
  len(self._ready_managers)
462
489
  )
463
490
 
464
- if interesting_managers and not self.pending_task_queue.empty():
491
+ if interesting_managers and self.pending_task_queue:
465
492
  shuffled_managers = self.manager_selector.sort_managers(self._ready_managers, interesting_managers)
466
493
 
467
- while shuffled_managers and not self.pending_task_queue.empty(): # cf. the if statement above...
494
+ while shuffled_managers and self.pending_task_queue: # cf. the if statement above...
468
495
  manager_id = shuffled_managers.pop()
469
496
  m = self._ready_managers[manager_id]
470
497
  tasks_inflight = len(m['tasks'])
@@ -1,8 +1,5 @@
1
1
  import logging
2
2
  from abc import ABCMeta, abstractmethod
3
- from typing import Optional
4
-
5
- _db_manager_excepts: Optional[Exception]
6
3
 
7
4
  logger = logging.getLogger(__name__)
8
5
 
@@ -2,6 +2,7 @@ import logging
2
2
  import math
3
3
  import os
4
4
  import re
5
+ import sys
5
6
  import time
6
7
  from typing import Any, Dict, Optional
7
8
 
@@ -50,6 +51,28 @@ squeue_translate_table = {
50
51
  }
51
52
 
52
53
 
54
+ if sys.version_info < (3, 12):
55
+ from itertools import islice
56
+ from typing import Iterable
57
+
58
+ def batched(
59
+ iterable: Iterable[tuple[object, Any]], n: int, *, strict: bool = False
60
+ ):
61
+ """Batched
62
+ Turns a list into a batch of size n. This code is from
63
+ https://docs.python.org/3.12/library/itertools.html#itertools.batched
64
+ and in versions 3.12+ this can be replaced with
65
+ itertools.batched
66
+ """
67
+ if n < 1:
68
+ raise ValueError("n must be at least one")
69
+ iterator = iter(iterable)
70
+ while batch := tuple(islice(iterator, n)):
71
+ yield batch
72
+ else:
73
+ from itertools import batched
74
+
75
+
53
76
  class SlurmProvider(ClusterProvider, RepresentationMixin):
54
77
  """Slurm Execution Provider
55
78
 
@@ -99,6 +122,12 @@ class SlurmProvider(ClusterProvider, RepresentationMixin):
99
122
  symbolic group for the job ID.
100
123
  worker_init : str
101
124
  Command to be run before starting a worker, such as 'module load Anaconda; source activate env'.
125
+ cmd_timeout : int (Default = 10)
126
+ Number of seconds to wait for slurm commands to finish. For schedulers with many this
127
+ may need to be increased to wait longer for scheduler information.
128
+ status_batch_size: int (Default = 50)
129
+ Number of jobs to batch together in calls to the scheduler status. For schedulers
130
+ with many jobs this may need to be decreased to get jobs in smaller batches.
102
131
  exclusive : bool (Default = True)
103
132
  Requests nodes which are not shared with other running jobs.
104
133
  launcher : Launcher
@@ -127,6 +156,7 @@ class SlurmProvider(ClusterProvider, RepresentationMixin):
127
156
  regex_job_id: str = r"Submitted batch job (?P<id>\S*)",
128
157
  worker_init: str = '',
129
158
  cmd_timeout: int = 10,
159
+ status_batch_size: int = 50,
130
160
  exclusive: bool = True,
131
161
  launcher: Launcher = SingleNodeLauncher()):
132
162
  label = 'slurm'
@@ -148,6 +178,8 @@ class SlurmProvider(ClusterProvider, RepresentationMixin):
148
178
  self.qos = qos
149
179
  self.constraint = constraint
150
180
  self.clusters = clusters
181
+ # Used to batch requests to sacct/squeue for long jobs lists
182
+ self.status_batch_size = status_batch_size
151
183
  self.scheduler_options = scheduler_options + '\n'
152
184
  if exclusive:
153
185
  self.scheduler_options += "#SBATCH --exclusive\n"
@@ -199,22 +231,23 @@ class SlurmProvider(ClusterProvider, RepresentationMixin):
199
231
  Returns:
200
232
  [status...] : Status list of all jobs
201
233
  '''
202
- job_id_list = ','.join(
203
- [jid for jid, job in self.resources.items() if not job['status'].terminal]
204
- )
205
- if not job_id_list:
206
- logger.debug('No active jobs, skipping status update')
234
+ active_jobs = {jid: job for jid, job in self.resources.items() if not job["status"].terminal}
235
+ if len(active_jobs) == 0:
236
+ logger.debug("No active jobs, skipping status update")
207
237
  return
208
238
 
209
- cmd = self._cmd.format(job_id_list)
210
- logger.debug("Executing %s", cmd)
211
- retcode, stdout, stderr = self.execute_wait(cmd)
212
- logger.debug("sacct/squeue returned %s %s", stdout, stderr)
213
-
214
- # Execute_wait failed. Do no update
215
- if retcode != 0:
216
- logger.warning("sacct/squeue failed with non-zero exit code {}".format(retcode))
217
- return
239
+ stdout = ""
240
+ for job_batch in batched(active_jobs.items(), self.status_batch_size):
241
+ job_id_list = ",".join(jid for jid, job in job_batch if not job["status"].terminal)
242
+ cmd = self._cmd.format(job_id_list)
243
+ logger.debug("Executing %s", cmd)
244
+ retcode, batch_stdout, batch_stderr = self.execute_wait(cmd)
245
+ logger.debug(f"sacct/squeue returned {retcode} {batch_stdout} {batch_stderr}")
246
+ stdout += batch_stdout
247
+ # Execute_wait failed. Do no update
248
+ if retcode != 0:
249
+ logger.warning("sacct/squeue failed with non-zero exit code {}".format(retcode))
250
+ return
218
251
 
219
252
  jobs_missing = set(self.resources.keys())
220
253
  for line in stdout.split('\n'):
parsl/tests/conftest.py CHANGED
@@ -167,6 +167,14 @@ def pytest_configure(config):
167
167
  'markers',
168
168
  'issue_3620: Marks tests that do not work correctly on GlobusComputeExecutor (ref: issue 3620)'
169
169
  )
170
+ config.addinivalue_line(
171
+ 'markers',
172
+ 'workqueue: Marks local tests that require a working Work Queue installation'
173
+ )
174
+ config.addinivalue_line(
175
+ 'markers',
176
+ 'taskvine: Marks local tests that require a working Task Vine installation'
177
+ )
170
178
 
171
179
 
172
180
  @pytest.fixture(autouse=True, scope='session')
@@ -400,13 +408,13 @@ def try_assert():
400
408
  check_period_ms: int = 20,
401
409
  ):
402
410
  tb = create_traceback(start=1)
403
- timeout_s = abs(timeout_ms) / 1000.0
404
411
  check_period_s = abs(check_period_ms) / 1000.0
405
412
  if attempts > 0:
406
413
  for _attempt_no in range(attempts):
414
+ time.sleep(random.random() * check_period_s) # jitter
415
+ check_period_s *= 2
407
416
  if test_func():
408
417
  return
409
- time.sleep(check_period_s)
410
418
  else:
411
419
  att_fail = (
412
420
  f"\n (Still failing after attempt limit [{attempts}], testing"
@@ -415,12 +423,15 @@ def try_assert():
415
423
  exc = AssertionError(f"{str(fail_msg)}{att_fail}".strip())
416
424
  raise exc.with_traceback(tb)
417
425
 
418
- elif timeout_s > 0:
426
+ elif timeout_ms > 0:
427
+ timeout_s = abs(timeout_ms) / 1000.0
419
428
  end = time.monotonic() + timeout_s
420
429
  while time.monotonic() < end:
430
+ wait_for = random.random() * check_period_s # jitter
431
+ time.sleep(min(wait_for, end - time.monotonic()))
432
+ check_period_s *= 2
421
433
  if test_func():
422
434
  return
423
- time.sleep(check_period_s)
424
435
  att_fail = (
425
436
  f"\n (Still failing after timeout [{timeout_ms}ms], with attempts "
426
437
  f"every {check_period_ms}ms)"
@@ -23,7 +23,7 @@ def test_resource(n=2):
23
23
  break
24
24
 
25
25
  # Specify incorrect number of resources
26
- spec = {'cores': 2, 'memory': 1000}
26
+ spec = {'cores': 1, 'memory': 1}
27
27
  fut = double(n, parsl_resource_specification=spec)
28
28
  try:
29
29
  fut.result()
@@ -35,7 +35,7 @@ def test_resource(n=2):
35
35
 
36
36
  # Specify resources with wrong types
37
37
  # 'cpus' is incorrect, should be 'cores'
38
- spec = {'cpus': 2, 'memory': 1000, 'disk': 1000}
38
+ spec = {'cpus': 1, 'memory': 1, 'disk': 1}
39
39
  fut = double(n, parsl_resource_specification=spec)
40
40
  try:
41
41
  fut.result()
@@ -0,0 +1,70 @@
1
+ import pytest
2
+
3
+ import parsl
4
+ from parsl.app.app import python_app
5
+ from parsl.config import Config
6
+ from parsl.executors import HighThroughputExecutor
7
+ from parsl.executors.high_throughput.manager_selector import RandomManagerSelector
8
+ from parsl.providers import LocalProvider
9
+ from parsl.usage_tracking.levels import LEVEL_1
10
+
11
+
12
+ @python_app
13
+ def fake_task(parsl_resource_specification=None):
14
+ import time
15
+ return time.time()
16
+
17
+
18
+ @pytest.mark.local
19
+ def test_priority_queue():
20
+ provider = LocalProvider(
21
+ init_blocks=0,
22
+ max_blocks=0,
23
+ min_blocks=0,
24
+ )
25
+
26
+ htex = HighThroughputExecutor(
27
+ label="htex_local",
28
+ max_workers_per_node=1,
29
+ manager_selector=RandomManagerSelector(),
30
+ provider=provider,
31
+ )
32
+
33
+ config = Config(
34
+ executors=[htex],
35
+ strategy="htex_auto_scale",
36
+ usage_tracking=LEVEL_1,
37
+ )
38
+
39
+ with parsl.load(config):
40
+ futures = {}
41
+
42
+ # Submit tasks with mixed priorities
43
+ # Priorities: [10, 10, 5, 5, 1, 1] to test fallback behavior
44
+ for i, priority in enumerate([10, 10, 5, 5, 1, 1]):
45
+ spec = {'priority': priority}
46
+ futures[(priority, i)] = fake_task(parsl_resource_specification=spec)
47
+
48
+ provider.max_blocks = 1
49
+
50
+ # Wait for completion
51
+ results = {
52
+ key: future.result() for key, future in futures.items()
53
+ }
54
+
55
+ # Sort by finish time
56
+ sorted_by_completion = sorted(results.items(), key=lambda item: item[1])
57
+ execution_order = [key for key, _ in sorted_by_completion]
58
+
59
+ # check priority queue functionality
60
+ priorities_only = [p for (p, i) in execution_order]
61
+ assert priorities_only == sorted(priorities_only), "Priority execution order failed"
62
+
63
+ # check FIFO fallback
64
+ from collections import defaultdict
65
+ seen = defaultdict(list)
66
+ for (priority, idx) in execution_order:
67
+ seen[priority].append(idx)
68
+
69
+ for priority, indices in seen.items():
70
+ assert indices == sorted(indices), f"FIFO fallback violated for priority {priority}"
@@ -36,3 +36,10 @@ def test_resource_spec_validation_bad_keys():
36
36
 
37
37
  with pytest.raises(InvalidResourceSpecification):
38
38
  htex.validate_resource_spec({"num_nodes": 2})
39
+
40
+
41
+ @pytest.mark.local
42
+ def test_resource_spec_validation_one_key():
43
+ htex = HighThroughputExecutor()
44
+ ret_val = htex.validate_resource_spec({"priority": 2})
45
+ assert ret_val is None
@@ -6,7 +6,6 @@ import pytest
6
6
  import parsl
7
7
  from parsl import HighThroughputExecutor
8
8
  from parsl.config import Config
9
- from parsl.executors.taskvine import TaskVineExecutor, TaskVineManagerConfig
10
9
  from parsl.monitoring import MonitoringHub
11
10
 
12
11
 
@@ -54,9 +53,27 @@ def htex_filesystem_config():
54
53
  return c
55
54
 
56
55
 
57
- @pytest.mark.local
58
- @pytest.mark.parametrize("fresh_config", [htex_config, htex_filesystem_config, htex_udp_config])
59
- def test_row_counts(tmpd_cwd, fresh_config):
56
+ def workqueue_config():
57
+ from parsl.tests.configs.workqueue_ex import fresh_config
58
+ c = fresh_config()
59
+ c.monitoring = MonitoringHub(
60
+ hub_address="localhost",
61
+ resource_monitoring_interval=1)
62
+ return c
63
+
64
+
65
+ def taskvine_config():
66
+ from parsl.executors.taskvine import TaskVineExecutor, TaskVineManagerConfig
67
+ c = Config(executors=[TaskVineExecutor(manager_config=TaskVineManagerConfig(port=9000),
68
+ worker_launch_method='provider')],
69
+ strategy_period=0.5,
70
+
71
+ monitoring=MonitoringHub(hub_address="localhost",
72
+ resource_monitoring_interval=1))
73
+ return c
74
+
75
+
76
+ def row_counts_parametrized(tmpd_cwd, fresh_config):
60
77
  # this is imported here rather than at module level because
61
78
  # it isn't available in a plain parsl install, so this module
62
79
  # would otherwise fail to import and break even a basic test
@@ -113,3 +130,23 @@ def test_row_counts(tmpd_cwd, fresh_config):
113
130
  result = connection.execute(text("SELECT COUNT(*) FROM resource"))
114
131
  (c, ) = result.first()
115
132
  assert c >= 1
133
+
134
+
135
+ @pytest.mark.local
136
+ @pytest.mark.parametrize("fresh_config", [htex_config, htex_filesystem_config, htex_udp_config])
137
+ def test_row_counts_base(tmpd_cwd, fresh_config):
138
+ row_counts_parametrized(tmpd_cwd, fresh_config)
139
+
140
+
141
+ @pytest.mark.workqueue
142
+ @pytest.mark.local
143
+ @pytest.mark.parametrize("fresh_config", [workqueue_config])
144
+ def test_row_counts_wq(tmpd_cwd, fresh_config):
145
+ row_counts_parametrized(tmpd_cwd, fresh_config)
146
+
147
+
148
+ @pytest.mark.taskvine
149
+ @pytest.mark.local
150
+ @pytest.mark.parametrize("fresh_config", [taskvine_config])
151
+ def test_row_counts_tv(tmpd_cwd, fresh_config):
152
+ row_counts_parametrized(tmpd_cwd, fresh_config)
@@ -37,6 +37,7 @@ def local_config():
37
37
  ],
38
38
  max_idletime=0.5,
39
39
  strategy='simple',
40
+ strategy_period=0.5,
40
41
  )
41
42
 
42
43
 
@@ -240,5 +240,6 @@ class UsageTracker:
240
240
  if proc.is_alive():
241
241
  logger.warning("Usage tracking process did not end itself; sending SIGKILL")
242
242
  proc.kill()
243
+ proc.join(timeout=timeout)
243
244
 
244
245
  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 = '2025.05.26'
6
+ VERSION = '2025.06.02'
@@ -5,13 +5,13 @@ import logging
5
5
  import os
6
6
  import pickle
7
7
  import platform
8
- import queue
9
8
  import sys
10
9
  import threading
11
10
  import time
12
11
  from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, cast
13
12
 
14
13
  import zmq
14
+ from sortedcontainers import SortedList
15
15
 
16
16
  from parsl import curvezmq
17
17
  from parsl.addresses import tcp_url
@@ -131,7 +131,7 @@ class Interchange:
131
131
  self.hub_address = hub_address
132
132
  self.hub_zmq_port = hub_zmq_port
133
133
 
134
- self.pending_task_queue: queue.Queue[Any] = queue.Queue(maxsize=10 ** 6)
134
+ self.pending_task_queue: SortedList[Any] = SortedList(key=lambda tup: (tup[0], tup[1]))
135
135
 
136
136
  # count of tasks that have been received from the submit side
137
137
  self.task_counter = 0
@@ -196,13 +196,12 @@ class Interchange:
196
196
  eg. [{'task_id':<x>, 'buffer':<buf>} ... ]
197
197
  """
198
198
  tasks = []
199
- for _ in range(0, count):
200
- try:
201
- x = self.pending_task_queue.get(block=False)
202
- except queue.Empty:
203
- break
204
- else:
205
- tasks.append(x)
199
+ try:
200
+ for _ in range(count):
201
+ *_, task = self.pending_task_queue.pop()
202
+ tasks.append(task)
203
+ except IndexError:
204
+ pass
206
205
 
207
206
  return tasks
208
207
 
@@ -342,8 +341,15 @@ class Interchange:
342
341
  if self.task_incoming in self.socks and self.socks[self.task_incoming] == zmq.POLLIN:
343
342
  logger.debug("start task_incoming section")
344
343
  msg = self.task_incoming.recv_pyobj()
344
+
345
+ # Process priority, higher number = lower priority
346
+ resource_spec = msg.get('resource_spec', {})
347
+ priority = resource_spec.get('priority', float('inf'))
348
+ queue_entry = (-priority, -self.task_counter, msg)
349
+
345
350
  logger.debug("putting message onto pending_task_queue")
346
- self.pending_task_queue.put(msg)
351
+
352
+ self.pending_task_queue.add(queue_entry)
347
353
  self.task_counter += 1
348
354
  logger.debug(f"Fetched {self.task_counter} tasks so far")
349
355
 
@@ -378,23 +384,24 @@ class Interchange:
378
384
  return
379
385
 
380
386
  if msg['type'] == 'registration':
381
- # We set up an entry only if registration works correctly
382
- self._ready_managers[manager_id] = {'last_heartbeat': time.time(),
383
- 'idle_since': time.time(),
384
- 'block_id': None,
385
- 'start_time': msg['start_time'],
386
- 'max_capacity': 0,
387
- 'worker_count': 0,
388
- 'active': True,
389
- 'draining': False,
390
- 'parsl_version': msg['parsl_v'],
391
- 'python_version': msg['python_v'],
392
- 'tasks': []}
393
- self.connected_block_history.append(msg['block_id'])
394
-
395
- interesting_managers.add(manager_id)
396
- logger.info(f"Adding manager: {manager_id!r} to ready queue")
397
- m = self._ready_managers[manager_id]
387
+ ix_minor_py = self.current_platform['python_v'].rsplit(".", 1)[0]
388
+ ix_parsl_v = self.current_platform['parsl_v']
389
+ mgr_minor_py = msg['python_v'].rsplit(".", 1)[0]
390
+ mgr_parsl_v = msg['parsl_v']
391
+
392
+ m = ManagerRecord(
393
+ block_id=None,
394
+ start_time=msg['start_time'],
395
+ tasks=[],
396
+ worker_count=0,
397
+ max_capacity=0,
398
+ active=True,
399
+ draining=False,
400
+ last_heartbeat=time.time(),
401
+ idle_since=time.time(),
402
+ parsl_version=mgr_parsl_v,
403
+ python_version=msg['python_v'],
404
+ )
398
405
 
399
406
  # m is a ManagerRecord, but msg is a dict[Any,Any] and so can
400
407
  # contain arbitrary fields beyond those in ManagerRecord (and
@@ -405,23 +412,43 @@ class Interchange:
405
412
  logger.info(f"Registration info for manager {manager_id!r}: {msg}")
406
413
  self._send_monitoring_info(monitoring_radio, m)
407
414
 
408
- if (msg['python_v'].rsplit(".", 1)[0] != self.current_platform['python_v'].rsplit(".", 1)[0] or
409
- msg['parsl_v'] != self.current_platform['parsl_v']):
410
- logger.error(f"Manager {manager_id!r} has incompatible version info with the interchange")
411
- logger.debug("Setting kill event")
415
+ if (mgr_minor_py, mgr_parsl_v) != (ix_minor_py, ix_parsl_v):
412
416
  kill_event.set()
413
- e = VersionMismatch("py.v={} parsl.v={}".format(self.current_platform['python_v'].rsplit(".", 1)[0],
414
- self.current_platform['parsl_v']),
415
- "py.v={} parsl.v={}".format(msg['python_v'].rsplit(".", 1)[0],
416
- msg['parsl_v'])
417
- )
418
- result_package = {'type': 'result', 'task_id': -1, 'exception': serialize_object(e)}
417
+ e = VersionMismatch(
418
+ f"py.v={ix_minor_py} parsl.v={ix_parsl_v}",
419
+ f"py.v={mgr_minor_py} parsl.v={mgr_parsl_v}",
420
+ )
421
+ result_package = {
422
+ 'type': 'result',
423
+ 'task_id': -1,
424
+ 'exception': serialize_object(e),
425
+ }
419
426
  pkl_package = pickle.dumps(result_package)
420
427
  self.results_outgoing.send(pkl_package)
421
- logger.error("Sent failure reports, shutting down interchange")
428
+ logger.error(
429
+ "Manager has incompatible version info with the interchange;"
430
+ " sending failure reports and shutting down:"
431
+ f"\n Interchange: {e.interchange_version}"
432
+ f"\n Manager: {e.manager_version}"
433
+ )
434
+
422
435
  else:
423
- logger.info(f"Manager {manager_id!r} has compatible Parsl version {msg['parsl_v']}")
424
- logger.info(f"Manager {manager_id!r} has compatible Python version {msg['python_v'].rsplit('.', 1)[0]}")
436
+ # We really should update the associated data structure; but not
437
+ # at this time. *kicks can down the road*
438
+ assert m['block_id'] is not None, "Verified externally currently"
439
+
440
+ # set up entry only if we accept the registration
441
+ self._ready_managers[manager_id] = m
442
+ self.connected_block_history.append(m['block_id'])
443
+
444
+ interesting_managers.add(manager_id)
445
+
446
+ logger.info(
447
+ f"Registered manager {manager_id!r} (py{mgr_minor_py},"
448
+ f" {mgr_parsl_v}) and added to ready queue"
449
+ )
450
+ logger.debug("Manager %r -> %s", manager_id, m)
451
+
425
452
  elif msg['type'] == 'heartbeat':
426
453
  manager = self._ready_managers.get(manager_id)
427
454
  if manager:
@@ -461,10 +488,10 @@ class Interchange:
461
488
  len(self._ready_managers)
462
489
  )
463
490
 
464
- if interesting_managers and not self.pending_task_queue.empty():
491
+ if interesting_managers and self.pending_task_queue:
465
492
  shuffled_managers = self.manager_selector.sort_managers(self._ready_managers, interesting_managers)
466
493
 
467
- while shuffled_managers and not self.pending_task_queue.empty(): # cf. the if statement above...
494
+ while shuffled_managers and self.pending_task_queue: # cf. the if statement above...
468
495
  manager_id = shuffled_managers.pop()
469
496
  m = self._ready_managers[manager_id]
470
497
  tasks_inflight = len(m['tasks'])
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsl
3
- Version: 2025.5.26
3
+ Version: 2025.6.2
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/2025.05.26.tar.gz
6
+ Download-URL: https://github.com/Parsl/parsl/archive/2025.06.02.tar.gz
7
7
  Author: The Parsl Team
8
8
  Author-email: parsl@googlegroups.com
9
9
  License: Apache 2.0
@@ -24,6 +24,7 @@ Requires-Dist: globus-sdk
24
24
  Requires-Dist: dill
25
25
  Requires-Dist: tblib
26
26
  Requires-Dist: requests
27
+ Requires-Dist: sortedcontainers
27
28
  Requires-Dist: psutil>=5.5.1
28
29
  Requires-Dist: setproctitle
29
30
  Requires-Dist: filelock<4,>=3.13
@@ -8,7 +8,7 @@ parsl/multiprocessing.py,sha256=JNAfgdZvQSsxVyUp229OOUqWwf_ZUhpmw8X9CdF3i6k,3614
8
8
  parsl/process_loggers.py,sha256=uQ7Gd0W72Jz7rrcYlOMfLsAEhkRltxXJL2MgdduJjEw,1136
9
9
  parsl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  parsl/utils.py,sha256=codTX6_KLhgeTwNkRzc1lo4bgc1M93eJ-lkqOO98fvk,14331
11
- parsl/version.py,sha256=EaScyVawSKbx-5eXxTDv4N5r-ccy_-Hm8yHlbdPlkiA,131
11
+ parsl/version.py,sha256=iOGq8naBMZwM3-DVQabgtw9WK1TvA8x_qTWhiLs8kd4,131
12
12
  parsl/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  parsl/app/app.py,sha256=0gbM4AH2OtFOLsv07I5nglpElcwMSOi-FzdZZfrk7So,8532
14
14
  parsl/app/bash.py,sha256=jm2AvePlCT9DZR7H_4ANDWxatp5dN_22FUlT_gWhZ-g,5528
@@ -73,8 +73,8 @@ parsl/executors/flux/executor.py,sha256=ii1i5V7uQnhf1BDq5nnMscmmpXJkCWtrFCuBbDaP
73
73
  parsl/executors/flux/flux_instance_manager.py,sha256=5T3Rp7ZM-mlT0Pf0Gxgs5_YmnaPrSF9ec7zvRfLfYJw,2129
74
74
  parsl/executors/high_throughput/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
75
75
  parsl/executors/high_throughput/errors.py,sha256=k2XuvvFdUfNs2foHFnxmS-BToRMfdXpYEa4EF3ELKq4,1554
76
- parsl/executors/high_throughput/executor.py,sha256=8iqced7UU44emOlFY6Z1iD5M8752EozY6UMNhYMbpQY,39685
77
- parsl/executors/high_throughput/interchange.py,sha256=gDCHe0UeKUAch7byR-FKJ7Tsrk39jGoTcVx3ERW6PkA,28719
76
+ parsl/executors/high_throughput/executor.py,sha256=x5759lCTIev1yzsJ4OwMp_0me7Cs108HLqKbz1r_zkg,39812
77
+ parsl/executors/high_throughput/interchange.py,sha256=lnPhV-5OR_rp4fa_Wj0tmG3avwUdPgJH4LTTBZQcrf8,28922
78
78
  parsl/executors/high_throughput/manager_record.py,sha256=ZMsqFxvreGLRXAw3N-JnODDa9Qfizw2tMmcBhm4lco4,490
79
79
  parsl/executors/high_throughput/manager_selector.py,sha256=UKcUE6v0tO7PDMTThpKSKxVpOpOUilxDL7UbNgpZCxo,2116
80
80
  parsl/executors/high_throughput/monitoring_info.py,sha256=HC0drp6nlXQpAop5PTUKNjdXMgtZVvrBL0JzZJebPP4,298
@@ -123,7 +123,7 @@ parsl/monitoring/types.py,sha256=oOCrzv-ab-_rv4pb8o58Sdb8G_RGp1aZriRbdf9zBEk,339
123
123
  parsl/monitoring/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
124
124
  parsl/monitoring/queries/pandas.py,sha256=0Z2r0rjTKCemf0eaDkF1irvVHn5g7KC5SYETvQPRxwU,2232
125
125
  parsl/monitoring/radios/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
126
- parsl/monitoring/radios/base.py,sha256=Ep5kHf07Sm-ApMBJVudRhoWRyuiu0udjO4NvEir5LEk,291
126
+ parsl/monitoring/radios/base.py,sha256=MBW3aAbxQY7TMuNGZEN15dWoy3mAELOqz-GMN1d7vF4,221
127
127
  parsl/monitoring/radios/filesystem.py,sha256=ioZ3jOKX5Qf0DYRtWmpCEorfuMVbS58OMS_QV7DOFOs,1765
128
128
  parsl/monitoring/radios/filesystem_router.py,sha256=kQkinktSpsVwfNESfUggSzBlRZ5JgwjM7IDN-jARAhM,2146
129
129
  parsl/monitoring/radios/htex.py,sha256=qBu4O5NYnSETHX0ptdwxSpqa2Pp3Z_V6a6lb3TbjKm4,1643
@@ -183,7 +183,7 @@ parsl/providers/pbspro/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
183
183
  parsl/providers/pbspro/pbspro.py,sha256=vzopQ80iXcl4Wx0_Bs5OtadYGeoGdbDgK7YHfjUjG_Y,8826
184
184
  parsl/providers/pbspro/template.py,sha256=y-Dher--t5Eury-c7cAuSZs9FEUXWiruFUI07v81558,315
185
185
  parsl/providers/slurm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
186
- parsl/providers/slurm/slurm.py,sha256=HHf3HMJWa2XcztvrM55eapqOs1r8iGg0EHn8b3xxJ_8,15773
186
+ parsl/providers/slurm/slurm.py,sha256=J58iSRqcVahhHmyIEvmTFlaL0EVaf7yxkD_fec_ja_o,17323
187
187
  parsl/providers/slurm/template.py,sha256=KpgBEFMc1ps-38jdrk13xUGx9TCivu-iF90jgQDdiEQ,315
188
188
  parsl/providers/torque/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
189
189
  parsl/providers/torque/template.py,sha256=4qfc2gmlEhRCAD7erFDOs4prJQ43I8s4E8DSUSVQx3A,358
@@ -196,7 +196,7 @@ parsl/serialize/facade.py,sha256=3uOuVp0epfyLn7qDzuWqLfsy971YVGD3sqwqcAiRwh0,668
196
196
  parsl/serialize/proxystore.py,sha256=o-ha9QAvVhbN8y9S1itk3W0O75eyHYZw2AvB2xu5_Lg,1624
197
197
  parsl/tests/__init__.py,sha256=VTtJzOzz_x6fWNh8IOnsgFqVbdiJShi2AZH21mcmID4,204
198
198
  parsl/tests/callables_helper.py,sha256=ceP1YYsNtrZgKT6MAIvpgdccEjQ_CpFEOnZBGHKGOx0,30
199
- parsl/tests/conftest.py,sha256=i8vJ2OSyhU1g8MoA1g6XPPEJ4VaavGN88dTcBROrhYE,15009
199
+ parsl/tests/conftest.py,sha256=ssY-ZP12a1TXj99ifRc0E83MQjri2bgwvMIe-YF5yd0,15485
200
200
  parsl/tests/test_aalst_patterns.py,sha256=lNIxb7nIgh1yX7hR2fr_ck_mxYJxx8ASKK9zHUVqPno,9614
201
201
  parsl/tests/test_callables.py,sha256=97vrIF1_hfDGd81FM1bhR6FemZMWFcALrH6pVHMTCt8,1974
202
202
  parsl/tests/test_curvezmq.py,sha256=yyhlS4vmaZdMitiySoy4l_ih9H1bsPiN-tMdwIh3H20,12431
@@ -309,7 +309,7 @@ parsl/tests/test_error_handling/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm
309
309
  parsl/tests/test_error_handling/test_fail.py,sha256=xx4TGWfL7le4cQ9nvnUkrlmKQJkskhD0l_3W1xwZSEI,282
310
310
  parsl/tests/test_error_handling/test_python_walltime.py,sha256=rdmGZHIkuann2Njt3i62odKJ0FaODGr7-L96rOXNVYg,950
311
311
  parsl/tests/test_error_handling/test_rand_fail.py,sha256=crFg4GmwdDpvx49_7w5Xt2P7H2R_V9f6i1Ar-QkASuU,3864
312
- parsl/tests/test_error_handling/test_resource_spec.py,sha256=uOYGHf2EC3cn4-G5ZYWK8WxXl_QONQfn9E0Tk7RJrGA,1467
312
+ parsl/tests/test_error_handling/test_resource_spec.py,sha256=dyuzMkS3M_BmZUbu1mF7yojwkJehDbdFvphNlYwU9yM,1458
313
313
  parsl/tests/test_error_handling/test_retries.py,sha256=zJ9D2hrvXQURnK2OIf5LfQFcSDVZ8rhdpp6peGccY7s,2372
314
314
  parsl/tests/test_error_handling/test_retry_handler.py,sha256=8fMHffMBLhRyNreIqkrwamx9TYRZ498uVYNlkcbAoLU,1407
315
315
  parsl/tests/test_error_handling/test_retry_handler_failure.py,sha256=GaGtZZCB9Wb7ieShqTrxUFEUSKy07ZZWytCY4Qixk9Y,552
@@ -332,12 +332,13 @@ parsl/tests/test_htex/test_manager_selector_by_block.py,sha256=VQqSE6MDhGpDSjShG
332
332
  parsl/tests/test_htex/test_managers_command.py,sha256=SCwkfyGB-Udgu5L2yDMpR5bsaT-aNjNkiXxtuRb25DI,1622
333
333
  parsl/tests/test_htex/test_missing_worker.py,sha256=gyp5i7_t-JHyJGtz_eXZKKBY5w8oqLOIxO6cJgGJMtQ,745
334
334
  parsl/tests/test_htex/test_multiple_disconnected_blocks.py,sha256=2vXZoIx4NuAWYuiNoL5Gxr85w72qZ7Kdb3JGh0FufTg,1867
335
- parsl/tests/test_htex/test_resource_spec_validation.py,sha256=JqboQRRFV0tEfWrGOdYT9pHazsUjyZLbF7qqnLFS_-A,914
335
+ parsl/tests/test_htex/test_priority_queue.py,sha256=vH58WwDZVpyIiMqhjwGkme7Cv5-jupTmM52EOcbdrEg,2106
336
+ parsl/tests/test_htex/test_resource_spec_validation.py,sha256=ZXW02jDd1rNxjBLh1jHyiz31zNoB9JzDw94aWllXFd4,1102
336
337
  parsl/tests/test_htex/test_worker_failure.py,sha256=Uz-RHI-LK78FMjXUvrUFmo4iYfmpDVBUcBxxRb3UG9M,603
337
338
  parsl/tests/test_htex/test_zmq_binding.py,sha256=G7D2_p9vOekgpB50MBiPRwtIz98DEkUpMqA3rdwzYTQ,4397
338
339
  parsl/tests/test_monitoring/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
339
340
  parsl/tests/test_monitoring/test_app_names.py,sha256=A-mOMCVhZDnUyJp32fsTUkHdcyval8o7WPEWacDkbD4,2208
340
- parsl/tests/test_monitoring/test_basic.py,sha256=3_lQ8KPxHPc6MT1UhACo7H-D7pBdy4Mxv4c4ztLv4-s,3947
341
+ parsl/tests/test_monitoring/test_basic.py,sha256=eAGHX1s7RweqZLSRmh7sY_l_zLGHQaHOXVuIN7R5SrI,5117
341
342
  parsl/tests/test_monitoring/test_db_locks.py,sha256=3s3c1xhKo230ZZIJ3f1Ca4U7LcEdXnanOGVXQyNlk2U,2895
342
343
  parsl/tests/test_monitoring/test_exit_helper.py,sha256=ob8Qd1hlkq_mowygfPetTnYN9LfuqeXHRpPilSfDSog,1232
343
344
  parsl/tests/test_monitoring/test_fuzz_zmq.py,sha256=SQNNHhXxHB_LwW4Ujqkgut3lbG0XVW-hliPagQQpiTc,3449
@@ -410,7 +411,7 @@ parsl/tests/test_scaling/test_block_error_handler.py,sha256=OS1IyiK8gjRFI1VzpmOv
410
411
  parsl/tests/test_scaling/test_regression_1621.py,sha256=e3-bkHR3d8LxA-uY0BugyWgYzksh00I_UbaA-jHOzKY,1872
411
412
  parsl/tests/test_scaling/test_regression_3568_scaledown_vs_MISSING.py,sha256=bjE_NIBoWK6heEz5LN0tzE1977vUA9kVemAYCqcIbzY,2942
412
413
  parsl/tests/test_scaling/test_regression_3696_oscillation.py,sha256=7Xc3vgocXXUbUegh9t5OyXlV91lRXDVMUlrOwErYOXA,3621
413
- parsl/tests/test_scaling/test_scale_down.py,sha256=vHJOMRUriW6xPtaY8GTKYXd5P0WJkQV6Q1IPui05aLU,2736
414
+ parsl/tests/test_scaling/test_scale_down.py,sha256=GmxzNtlG13SySVDGGlSqXEnaHxyCx6ZVn_Hi1GcBvj8,2765
414
415
  parsl/tests/test_scaling/test_scale_down_htex_auto_scale.py,sha256=EnVNllKO2AGKkGa6927cLrzvvG6mpNQeFDzVktv6x08,4521
415
416
  parsl/tests/test_scaling/test_scale_down_htex_unregistered.py,sha256=OrdnYmd58n7UfkANPJ7mzha4WSCPdbgJRX1O1Zdu0tI,1954
416
417
  parsl/tests/test_scaling/test_shutdown_scalein.py,sha256=sr40of5DwxeyQI97MDZxFqJILZSXZJb9Dv7qTf2gql8,2471
@@ -458,14 +459,14 @@ parsl/tests/unit/test_usage_tracking.py,sha256=xEfUlbBRpsFdUdOrCsk1Kz5AfmMxJT7f0
458
459
  parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
459
460
  parsl/usage_tracking/api.py,sha256=iaCY58Dc5J4UM7_dJzEEs871P1p1HdxBMtNGyVdzc9g,1821
460
461
  parsl/usage_tracking/levels.py,sha256=xbfzYEsd55KiZJ-mzNgPebvOH4rRHum04hROzEf41tU,291
461
- parsl/usage_tracking/usage.py,sha256=8hq1UPdFlVcC0V3aj0ve-MvCyvwK8Xr3CVuSto3dTW4,9165
462
- parsl-2025.5.26.data/scripts/exec_parsl_function.py,sha256=YXKVVIa4zXmOtz-0Ca4E_5nQfN_3S2bh2tB75uZZB4w,7774
463
- parsl-2025.5.26.data/scripts/interchange.py,sha256=ESVMe6rIAzTqZrXp1gzIPcl3jAStLfmYeH2-rG0RoZQ,28706
464
- parsl-2025.5.26.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
465
- parsl-2025.5.26.data/scripts/process_worker_pool.py,sha256=__gFeFQJpV5moRofj3WKQCnKp6gmzieXjzkmzVuTmX4,41123
466
- parsl-2025.5.26.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
467
- parsl-2025.5.26.dist-info/METADATA,sha256=ftY4oT1kBRYDTickFiyqTq7XZzR8klGI--Q1mNK2teI,4023
468
- parsl-2025.5.26.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
469
- parsl-2025.5.26.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
470
- parsl-2025.5.26.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
471
- parsl-2025.5.26.dist-info/RECORD,,
462
+ parsl/usage_tracking/usage.py,sha256=hbMo5BYgIWqMcFWqN-HYP1TbwNrTonpv-usfwnCFJKY,9212
463
+ parsl-2025.6.2.data/scripts/exec_parsl_function.py,sha256=YXKVVIa4zXmOtz-0Ca4E_5nQfN_3S2bh2tB75uZZB4w,7774
464
+ parsl-2025.6.2.data/scripts/interchange.py,sha256=_FRB1LxkL9vnT3y24NTXHOzotMlDJEXwF5ZZCjGmcww,28909
465
+ parsl-2025.6.2.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
466
+ parsl-2025.6.2.data/scripts/process_worker_pool.py,sha256=__gFeFQJpV5moRofj3WKQCnKp6gmzieXjzkmzVuTmX4,41123
467
+ parsl-2025.6.2.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
468
+ parsl-2025.6.2.dist-info/METADATA,sha256=6TKNQwxwu5QiBBEZaDWkTNGu0o2f2baDSnd_3BkDqsA,4054
469
+ parsl-2025.6.2.dist-info/WHEEL,sha256=tZoeGjtWxWRfdplE7E3d45VPlLNQnvbKiYnx7gwAy8A,92
470
+ parsl-2025.6.2.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
471
+ parsl-2025.6.2.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
472
+ parsl-2025.6.2.dist-info/RECORD,,