parsl 2024.10.21__py3-none-any.whl → 2024.10.28__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/channels/base.py +0 -11
  2. parsl/channels/errors.py +0 -17
  3. parsl/channels/local/local.py +3 -16
  4. parsl/channels/ssh/ssh.py +0 -11
  5. parsl/dataflow/dflow.py +1 -1
  6. parsl/executors/high_throughput/interchange.py +8 -5
  7. parsl/executors/high_throughput/process_worker_pool.py +0 -8
  8. parsl/monitoring/db_manager.py +1 -1
  9. parsl/monitoring/monitoring.py +5 -5
  10. parsl/monitoring/radios.py +2 -2
  11. parsl/monitoring/router.py +4 -7
  12. parsl/monitoring/types.py +3 -6
  13. parsl/providers/base.py +0 -16
  14. parsl/tests/{integration/test_channels → test_channels}/test_local_channel.py +4 -8
  15. parsl/tests/test_scaling/test_worker_interchange_bad_messages_3262.py +92 -0
  16. parsl/tests/test_serialization/test_3495_deserialize_managerlost.py +1 -1
  17. parsl/version.py +1 -1
  18. {parsl-2024.10.21.data → parsl-2024.10.28.data}/scripts/interchange.py +8 -5
  19. {parsl-2024.10.21.data → parsl-2024.10.28.data}/scripts/process_worker_pool.py +0 -8
  20. {parsl-2024.10.21.dist-info → parsl-2024.10.28.dist-info}/METADATA +2 -2
  21. {parsl-2024.10.21.dist-info → parsl-2024.10.28.dist-info}/RECORD +27 -27
  22. parsl/tests/integration/test_channels/test_channels.py +0 -17
  23. {parsl-2024.10.21.data → parsl-2024.10.28.data}/scripts/exec_parsl_function.py +0 -0
  24. {parsl-2024.10.21.data → parsl-2024.10.28.data}/scripts/parsl_coprocess.py +0 -0
  25. {parsl-2024.10.21.dist-info → parsl-2024.10.28.dist-info}/LICENSE +0 -0
  26. {parsl-2024.10.21.dist-info → parsl-2024.10.28.dist-info}/WHEEL +0 -0
  27. {parsl-2024.10.21.dist-info → parsl-2024.10.28.dist-info}/entry_points.txt +0 -0
  28. {parsl-2024.10.21.dist-info → parsl-2024.10.28.dist-info}/top_level.txt +0 -0
parsl/channels/base.py CHANGED
@@ -120,14 +120,3 @@ class Channel(metaclass=ABCMeta):
120
120
  Path of directory to check.
121
121
  """
122
122
  pass
123
-
124
- @abstractmethod
125
- def abspath(self, path: str) -> str:
126
- """Return the absolute path.
127
-
128
- Parameters
129
- ----------
130
- path : str
131
- Path for which the absolute path will be returned.
132
- """
133
- pass
parsl/channels/errors.py CHANGED
@@ -1,7 +1,5 @@
1
1
  ''' Exceptions raise by Apps.
2
2
  '''
3
- from typing import Optional
4
-
5
3
  from parsl.errors import ParslError
6
4
 
7
5
 
@@ -60,21 +58,6 @@ class BadPermsScriptPath(ChannelError):
60
58
  super().__init__("User does not have permissions to access the script_dir", e, hostname)
61
59
 
62
60
 
63
- class FileExists(ChannelError):
64
- ''' Push or pull of file over channel fails since a file of the name already
65
- exists on the destination.
66
-
67
- Contains:
68
- reason(string)
69
- e (paramiko exception object)
70
- hostname (string)
71
- '''
72
-
73
- def __init__(self, e: Exception, hostname: str, filename: Optional[str] = None) -> None:
74
- super().__init__("File name collision in channel transport phase: {}".format(filename),
75
- e, hostname)
76
-
77
-
78
61
  class AuthException(ChannelError):
79
62
  ''' An error raised during execution of an app.
80
63
  What this exception contains depends entirely on context
@@ -37,19 +37,16 @@ class LocalChannel(Channel, RepresentationMixin):
37
37
 
38
38
  Args:
39
39
  - cmd (string) : Commandline string to execute
40
- - walltime (int) : walltime in seconds, this is not really used now.
40
+ - walltime (int) : walltime in seconds
41
41
 
42
42
  Kwargs:
43
43
  - envs (dict) : Dictionary of env variables. This will be used
44
44
  to override the envs set at channel initialization.
45
45
 
46
46
  Returns:
47
- - retcode : Return code from the execution, -1 on fail
47
+ - retcode : Return code from the execution
48
48
  - stdout : stdout string
49
49
  - stderr : stderr string
50
-
51
- Raises:
52
- None.
53
50
  '''
54
51
  current_env = copy.deepcopy(self._envs)
55
52
  current_env.update(envs)
@@ -145,16 +142,6 @@ class LocalChannel(Channel, RepresentationMixin):
145
142
 
146
143
  return os.makedirs(path, mode, exist_ok)
147
144
 
148
- def abspath(self, path):
149
- """Return the absolute path.
150
-
151
- Parameters
152
- ----------
153
- path : str
154
- Path for which the absolute path will be returned.
155
- """
156
- return os.path.abspath(path)
157
-
158
145
  @property
159
146
  def script_dir(self):
160
147
  return self._script_dir
@@ -162,5 +149,5 @@ class LocalChannel(Channel, RepresentationMixin):
162
149
  @script_dir.setter
163
150
  def script_dir(self, value):
164
151
  if value is not None:
165
- value = self.abspath(value)
152
+ value = os.path.abspath(value)
166
153
  self._script_dir = value
parsl/channels/ssh/ssh.py CHANGED
@@ -214,7 +214,6 @@ class DeprecatedSSHChannel(Channel, RepresentationMixin):
214
214
  - str: Local path to file
215
215
 
216
216
  Raises:
217
- - FileExists : Name collision at local directory.
218
217
  - FileCopyException : FileCopy failed.
219
218
  '''
220
219
 
@@ -287,16 +286,6 @@ class DeprecatedSSHChannel(Channel, RepresentationMixin):
287
286
  self.execute_wait('mkdir -p {}'.format(path))
288
287
  self._valid_sftp_client().chmod(path, mode)
289
288
 
290
- def abspath(self, path):
291
- """Return the absolute path on the remote side.
292
-
293
- Parameters
294
- ----------
295
- path : str
296
- Path for which the absolute path will be returned.
297
- """
298
- return self._valid_sftp_client().normalize(path)
299
-
300
289
  @property
301
290
  def script_dir(self):
302
291
  return self._script_dir
parsl/dataflow/dflow.py CHANGED
@@ -987,7 +987,7 @@ class DataFlowKernel:
987
987
  - app_kwargs (dict) : Rest of the kwargs to the fn passed as dict.
988
988
 
989
989
  Returns:
990
- (AppFuture) [DataFutures,]
990
+ AppFuture
991
991
 
992
992
  """
993
993
 
@@ -66,7 +66,7 @@ class Interchange:
66
66
  If specified the interchange will only listen on this address for connections from workers
67
67
  else, it binds to all addresses.
68
68
 
69
- client_ports : triple(int, int, int)
69
+ client_ports : tuple(int, int, int)
70
70
  The ports at which the client can be reached
71
71
 
72
72
  worker_ports : tuple(int, int)
@@ -104,7 +104,6 @@ class Interchange:
104
104
  os.makedirs(self.logdir, exist_ok=True)
105
105
 
106
106
  start_file_logger("{}/interchange.log".format(self.logdir), level=logging_level)
107
- logger.propagate = False
108
107
  logger.debug("Initializing Interchange process")
109
108
 
110
109
  self.client_address = client_address
@@ -437,9 +436,13 @@ class Interchange:
437
436
  logger.info(f"Manager {manager_id!r} has compatible Parsl version {msg['parsl_v']}")
438
437
  logger.info(f"Manager {manager_id!r} has compatible Python version {msg['python_v'].rsplit('.', 1)[0]}")
439
438
  elif msg['type'] == 'heartbeat':
440
- self._ready_managers[manager_id]['last_heartbeat'] = time.time()
441
- logger.debug("Manager %r sent heartbeat via tasks connection", manager_id)
442
- self.task_outgoing.send_multipart([manager_id, b'', PKL_HEARTBEAT_CODE])
439
+ manager = self._ready_managers.get(manager_id)
440
+ if manager:
441
+ manager['last_heartbeat'] = time.time()
442
+ logger.debug("Manager %r sent heartbeat via tasks connection", manager_id)
443
+ self.task_outgoing.send_multipart([manager_id, b'', PKL_HEARTBEAT_CODE])
444
+ else:
445
+ logger.warning("Received heartbeat via tasks connection for not-registered manager %r", manager_id)
443
446
  elif msg['type'] == 'drain':
444
447
  self._ready_managers[manager_id]['draining'] = True
445
448
  logger.debug("Manager %r requested drain", manager_id)
@@ -650,14 +650,6 @@ def worker(
650
650
  debug: bool,
651
651
  mpi_launcher: str,
652
652
  ):
653
- """
654
-
655
- Put request token into queue
656
- Get task from task_queue
657
- Pop request from queue
658
- Put result into result_queue
659
- """
660
-
661
653
  # override the global logger inherited from the __main__ process (which
662
654
  # usually logs to manager.log) with one specific to this worker.
663
655
  global logger
@@ -556,7 +556,7 @@ class DatabaseManager:
556
556
  logger.debug("Checking STOP conditions: kill event: %s, queue has entries: %s",
557
557
  kill_event.is_set(), logs_queue.qsize() != 0)
558
558
  try:
559
- x, addr = logs_queue.get(timeout=0.1)
559
+ x = logs_queue.get(timeout=0.1)
560
560
  except queue.Empty:
561
561
  continue
562
562
  else:
@@ -16,7 +16,7 @@ from parsl.monitoring.errors import MonitoringHubStartError
16
16
  from parsl.monitoring.message_type import MessageType
17
17
  from parsl.monitoring.radios import MultiprocessingQueueRadioSender
18
18
  from parsl.monitoring.router import router_starter
19
- from parsl.monitoring.types import AddressedMonitoringMessage
19
+ from parsl.monitoring.types import TaggedMonitoringMessage
20
20
  from parsl.multiprocessing import ForkProcess, SizedQueue
21
21
  from parsl.process_loggers import wrap_with_logs
22
22
  from parsl.serialize import deserialize
@@ -138,7 +138,7 @@ class MonitoringHub(RepresentationMixin):
138
138
  self.exception_q: Queue[Tuple[str, str]]
139
139
  self.exception_q = SizedQueue(maxsize=10)
140
140
 
141
- self.resource_msgs: Queue[Union[AddressedMonitoringMessage, Tuple[Literal["STOP"], Literal[0]]]]
141
+ self.resource_msgs: Queue[Union[TaggedMonitoringMessage, Literal["STOP"]]]
142
142
  self.resource_msgs = SizedQueue()
143
143
 
144
144
  self.router_exit_event: ms.Event
@@ -237,7 +237,7 @@ class MonitoringHub(RepresentationMixin):
237
237
  logger.debug("Finished waiting for router termination")
238
238
  if len(exception_msgs) == 0:
239
239
  logger.debug("Sending STOP to DBM")
240
- self.resource_msgs.put(("STOP", 0))
240
+ self.resource_msgs.put("STOP")
241
241
  else:
242
242
  logger.debug("Not sending STOP to DBM, because there were DBM exceptions")
243
243
  logger.debug("Waiting for DB termination")
@@ -261,7 +261,7 @@ class MonitoringHub(RepresentationMixin):
261
261
 
262
262
 
263
263
  @wrap_with_logs
264
- def filesystem_receiver(logdir: str, q: "queue.Queue[AddressedMonitoringMessage]", run_dir: str) -> None:
264
+ def filesystem_receiver(logdir: str, q: "queue.Queue[TaggedMonitoringMessage]", run_dir: str) -> None:
265
265
  logger = set_file_logger("{}/monitoring_filesystem_radio.log".format(logdir),
266
266
  name="monitoring_filesystem_radio",
267
267
  level=logging.INFO)
@@ -288,7 +288,7 @@ def filesystem_receiver(logdir: str, q: "queue.Queue[AddressedMonitoringMessage]
288
288
  message = deserialize(f.read())
289
289
  logger.debug(f"Message received is: {message}")
290
290
  assert isinstance(message, tuple)
291
- q.put(cast(AddressedMonitoringMessage, message))
291
+ q.put(cast(TaggedMonitoringMessage, message))
292
292
  os.remove(full_path_filename)
293
293
  except Exception:
294
294
  logger.exception(f"Exception processing {filename} - probably will be retried next iteration")
@@ -58,7 +58,7 @@ class FilesystemRadioSender(MonitoringRadioSender):
58
58
 
59
59
  tmp_filename = f"{self.tmp_path}/{unique_id}"
60
60
  new_filename = f"{self.new_path}/{unique_id}"
61
- buffer = (message, "NA")
61
+ buffer = message
62
62
 
63
63
  # this will write the message out then atomically
64
64
  # move it into new/, so that a partially written
@@ -187,7 +187,7 @@ class MultiprocessingQueueRadioSender(MonitoringRadioSender):
187
187
  self.queue = queue
188
188
 
189
189
  def send(self, message: object) -> None:
190
- self.queue.put((message, 0))
190
+ self.queue.put(message)
191
191
 
192
192
 
193
193
  class ZMQRadioSender(MonitoringRadioSender):
@@ -14,7 +14,7 @@ import typeguard
14
14
  import zmq
15
15
 
16
16
  from parsl.log_utils import set_file_logger
17
- from parsl.monitoring.types import AddressedMonitoringMessage, TaggedMonitoringMessage
17
+ from parsl.monitoring.types import TaggedMonitoringMessage
18
18
  from parsl.process_loggers import wrap_with_logs
19
19
  from parsl.utils import setproctitle
20
20
 
@@ -125,7 +125,7 @@ class MonitoringRouter:
125
125
  data, addr = self.udp_sock.recvfrom(2048)
126
126
  resource_msg = pickle.loads(data)
127
127
  self.logger.debug("Got UDP Message from {}: {}".format(addr, resource_msg))
128
- self.resource_msgs.put((resource_msg, addr))
128
+ self.resource_msgs.put(resource_msg)
129
129
  except socket.timeout:
130
130
  pass
131
131
 
@@ -136,7 +136,7 @@ class MonitoringRouter:
136
136
  data, addr = self.udp_sock.recvfrom(2048)
137
137
  msg = pickle.loads(data)
138
138
  self.logger.debug("Got UDP Message from {}: {}".format(addr, msg))
139
- self.resource_msgs.put((msg, addr))
139
+ self.resource_msgs.put(msg)
140
140
  last_msg_received_time = time.time()
141
141
  except socket.timeout:
142
142
  pass
@@ -160,10 +160,7 @@ class MonitoringRouter:
160
160
  assert len(msg) >= 1, "ZMQ Receiver expects tuples of length at least 1, got {}".format(msg)
161
161
  assert len(msg) == 2, "ZMQ Receiver expects message tuples of exactly length 2, got {}".format(msg)
162
162
 
163
- msg_0: AddressedMonitoringMessage
164
- msg_0 = (msg, 0)
165
-
166
- self.resource_msgs.put(msg_0)
163
+ self.resource_msgs.put(msg)
167
164
  except zmq.Again:
168
165
  pass
169
166
  except Exception:
parsl/monitoring/types.py CHANGED
@@ -1,14 +1,11 @@
1
- from typing import Any, Dict, Tuple, Union
1
+ from typing import Any, Dict, Tuple
2
2
 
3
3
  from typing_extensions import TypeAlias
4
4
 
5
5
  from parsl.monitoring.message_type import MessageType
6
6
 
7
- # A basic parsl monitoring message is wrapped by up to two wrappers:
8
- # The basic monitoring message dictionary can first be tagged, giving
9
- # a TaggedMonitoringMessage, and then that can be further tagged with
10
- # an often unused sender address, giving an AddressedMonitoringMessage.
7
+ # A MonitoringMessage dictionary can be tagged, giving a
8
+ # TaggedMonitoringMessage.
11
9
 
12
10
  MonitoringMessage: TypeAlias = Dict[str, Any]
13
11
  TaggedMonitoringMessage: TypeAlias = Tuple[MessageType, MonitoringMessage]
14
- AddressedMonitoringMessage: TypeAlias = Tuple[TaggedMonitoringMessage, Union[str, int]]
parsl/providers/base.py CHANGED
@@ -2,7 +2,6 @@ import logging
2
2
  from abc import ABCMeta, abstractmethod, abstractproperty
3
3
  from typing import Any, Dict, List, Optional
4
4
 
5
- from parsl.channels.base import Channel
6
5
  from parsl.jobs.states import JobStatus
7
6
 
8
7
  logger = logging.getLogger(__name__)
@@ -154,18 +153,3 @@ class ExecutionProvider(metaclass=ABCMeta):
154
153
  :return: the number of seconds to wait between calls to status()
155
154
  """
156
155
  pass
157
-
158
-
159
- class Channeled():
160
- """A marker type to indicate that parsl should manage a Channel for this provider"""
161
- def __init__(self) -> None:
162
- self.channel: Channel
163
- pass
164
-
165
-
166
- class MultiChanneled():
167
- """A marker type to indicate that parsl should manage multiple Channels for this provider"""
168
-
169
- def __init__(self) -> None:
170
- self.channels: List[Channel]
171
- pass
@@ -1,6 +1,9 @@
1
+ import pytest
2
+
1
3
  from parsl.channels.local.local import LocalChannel
2
4
 
3
5
 
6
+ @pytest.mark.local
4
7
  def test_env():
5
8
  ''' Regression testing for issue #27
6
9
  '''
@@ -15,9 +18,8 @@ def test_env():
15
18
  x = [s for s in stdout if s.startswith("HOME=")]
16
19
  assert x, "HOME not found"
17
20
 
18
- print("RC:{} \nSTDOUT:{} \nSTDERR:{}".format(rc, stdout, stderr))
19
-
20
21
 
22
+ @pytest.mark.local
21
23
  def test_env_mod():
22
24
  ''' Testing for env update at execute time.
23
25
  '''
@@ -34,9 +36,3 @@ def test_env_mod():
34
36
 
35
37
  x = [s for s in stdout if s.startswith("TEST_ENV=fooo")]
36
38
  assert x, "User set env missing"
37
-
38
-
39
- if __name__ == "__main__":
40
-
41
- test_env()
42
- test_env_mod()
@@ -0,0 +1,92 @@
1
+ import os
2
+ import signal
3
+ import time
4
+
5
+ import pytest
6
+ import zmq
7
+
8
+ import parsl
9
+ from parsl.channels import LocalChannel
10
+ from parsl.config import Config
11
+ from parsl.executors import HighThroughputExecutor
12
+ from parsl.launchers import SimpleLauncher
13
+ from parsl.providers import LocalProvider
14
+
15
+ T_s = 1
16
+
17
+
18
+ def fresh_config():
19
+ htex = HighThroughputExecutor(
20
+ heartbeat_period=1 * T_s,
21
+ heartbeat_threshold=3 * T_s,
22
+ label="htex_local",
23
+ worker_debug=True,
24
+ cores_per_worker=1,
25
+ encrypted=False,
26
+ provider=LocalProvider(
27
+ channel=LocalChannel(),
28
+ init_blocks=0,
29
+ min_blocks=0,
30
+ max_blocks=0,
31
+ launcher=SimpleLauncher(),
32
+ ),
33
+ )
34
+ c = Config(
35
+ executors=[htex],
36
+ strategy='none',
37
+ strategy_period=0.5,
38
+ )
39
+ return c, htex
40
+
41
+
42
+ @parsl.python_app
43
+ def app():
44
+ return 7
45
+
46
+
47
+ @pytest.mark.local
48
+ @pytest.mark.parametrize("msg",
49
+ (b'FuzzyByte\rSTREAM', # not JSON
50
+ b'{}', # missing fields
51
+ b'{"type":"heartbeat"}', # regression test #3262
52
+ )
53
+ )
54
+ def test_bad_messages(try_assert, msg):
55
+ """This tests that the interchange is resilient to a few different bad
56
+ messages: malformed messages caused by implementation errors, and
57
+ heartbeat messages from managers that are not registered.
58
+
59
+ The heartbeat test is a regression test for issues #3262, #3632
60
+ """
61
+
62
+ c, htex = fresh_config()
63
+
64
+ with parsl.load(c):
65
+
66
+ # send a bad message into the interchange on the task_outgoing worker
67
+ # channel, and then check that the interchange is still alive enough
68
+ # that we can scale out a block and run a task.
69
+
70
+ (task_port, result_port) = htex.command_client.run("WORKER_PORTS")
71
+
72
+ context = zmq.Context()
73
+ channel_timeout = 10000 # in milliseconds
74
+ task_channel = context.socket(zmq.DEALER)
75
+ task_channel.setsockopt(zmq.LINGER, 0)
76
+ task_channel.setsockopt(zmq.IDENTITY, b'testid')
77
+
78
+ task_channel.set_hwm(0)
79
+ task_channel.setsockopt(zmq.SNDTIMEO, channel_timeout)
80
+ task_channel.connect(f"tcp://localhost:{task_port}")
81
+
82
+ task_channel.send(msg)
83
+
84
+ # If the interchange exits, it's likely that this test will hang rather
85
+ # than raise an error, because the interchange interaction code
86
+ # assumes the interchange is always there.
87
+ # In the case of issue #3262, an exception message goes to stderr, and
88
+ # no error goes to the interchange log file.
89
+ htex.scale_out_facade(1)
90
+ try_assert(lambda: len(htex.connected_managers()) == 1, timeout_ms=10000)
91
+
92
+ assert app().result() == 7
@@ -32,7 +32,7 @@ def test_manager_lost_system_failure(tmpd_cwd):
32
32
  cores_per_worker=1,
33
33
  worker_logdir_root=str(tmpd_cwd),
34
34
  heartbeat_period=1,
35
- heartbeat_threshold=1,
35
+ heartbeat_threshold=3,
36
36
  )
37
37
  c = Config(executors=[hte], strategy='simple', strategy_period=0.1)
38
38
 
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.10.21'
6
+ VERSION = '2024.10.28'
@@ -66,7 +66,7 @@ class Interchange:
66
66
  If specified the interchange will only listen on this address for connections from workers
67
67
  else, it binds to all addresses.
68
68
 
69
- client_ports : triple(int, int, int)
69
+ client_ports : tuple(int, int, int)
70
70
  The ports at which the client can be reached
71
71
 
72
72
  worker_ports : tuple(int, int)
@@ -104,7 +104,6 @@ class Interchange:
104
104
  os.makedirs(self.logdir, exist_ok=True)
105
105
 
106
106
  start_file_logger("{}/interchange.log".format(self.logdir), level=logging_level)
107
- logger.propagate = False
108
107
  logger.debug("Initializing Interchange process")
109
108
 
110
109
  self.client_address = client_address
@@ -437,9 +436,13 @@ class Interchange:
437
436
  logger.info(f"Manager {manager_id!r} has compatible Parsl version {msg['parsl_v']}")
438
437
  logger.info(f"Manager {manager_id!r} has compatible Python version {msg['python_v'].rsplit('.', 1)[0]}")
439
438
  elif msg['type'] == 'heartbeat':
440
- self._ready_managers[manager_id]['last_heartbeat'] = time.time()
441
- logger.debug("Manager %r sent heartbeat via tasks connection", manager_id)
442
- self.task_outgoing.send_multipart([manager_id, b'', PKL_HEARTBEAT_CODE])
439
+ manager = self._ready_managers.get(manager_id)
440
+ if manager:
441
+ manager['last_heartbeat'] = time.time()
442
+ logger.debug("Manager %r sent heartbeat via tasks connection", manager_id)
443
+ self.task_outgoing.send_multipart([manager_id, b'', PKL_HEARTBEAT_CODE])
444
+ else:
445
+ logger.warning("Received heartbeat via tasks connection for not-registered manager %r", manager_id)
443
446
  elif msg['type'] == 'drain':
444
447
  self._ready_managers[manager_id]['draining'] = True
445
448
  logger.debug("Manager %r requested drain", manager_id)
@@ -650,14 +650,6 @@ def worker(
650
650
  debug: bool,
651
651
  mpi_launcher: str,
652
652
  ):
653
- """
654
-
655
- Put request token into queue
656
- Get task from task_queue
657
- Pop request from queue
658
- Put result into result_queue
659
- """
660
-
661
653
  # override the global logger inherited from the __main__ process (which
662
654
  # usually logs to manager.log) with one specific to this worker.
663
655
  global logger
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: parsl
3
- Version: 2024.10.21
3
+ Version: 2024.10.28
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.10.21.tar.gz
6
+ Download-URL: https://github.com/Parsl/parsl/archive/2024.10.28.tar.gz
7
7
  Author: The Parsl Team
8
8
  Author-email: parsl@googlegroups.com
9
9
  License: Apache 2.0
@@ -8,7 +8,7 @@ parsl/multiprocessing.py,sha256=MyaEcEq-Qf860u7V98u-PZrPNdtzOZL_NW6EhIJnmfQ,1937
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=rMLKeadEsQ9jGwm4ogqiLIXPS3zOAyfznQJXVkJSY8E,13107
11
- parsl/version.py,sha256=0V6_ogkULPZVJXRQqKVT9TwsP2SpvX2cDNjSb1ouhPk,131
11
+ parsl/version.py,sha256=_aB3gX1QHuC8JylX_fsIWJhUjbr-OtBk6u88PVGFMFQ,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
@@ -18,14 +18,14 @@ parsl/app/python.py,sha256=0hrz2BppVOwwNfh5hnoP70Yv56gSRkIoT-fP9XNb4v4,2331
18
18
  parsl/benchmark/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
19
  parsl/benchmark/perf.py,sha256=kKXefDozWXSJKSNA7qdfUgEoacA2-R9kSZcI2YvZ5uE,3096
20
20
  parsl/channels/__init__.py,sha256=OEZcuNBOxUwmzrHMZOuPvkw4kUxrbJDA99crDk61O90,131
21
- parsl/channels/base.py,sha256=bS43-Qv4VSxa83V6fJ54lNBL_eHCu-Ce7-aoy1C9vCc,4193
22
- parsl/channels/errors.py,sha256=Dp0FhtHpygn0IjX8nGurx-WrTJm9aw-Jjz3SSUT-jCc,3283
21
+ parsl/channels/base.py,sha256=eJQQHBE_N0sAl41LH2UE6KBJomxjWw_B3ilQRYgpxHc,3948
22
+ parsl/channels/errors.py,sha256=x0ppJXY4Um0XIwO0oJ3x0biFaTxxv5Z4sP-FkowR72U,2782
23
23
  parsl/channels/local/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- parsl/channels/local/local.py,sha256=xqH4HnipUN95NgvyB1r33SiqgQKkARgRKmg0_HnumUk,5311
24
+ parsl/channels/local/local.py,sha256=kAyO7byrjBX2heuhcG_Nr_DbZemdvKHw8GsH9XEUQ4A,5004
25
25
  parsl/channels/oauth_ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
26
  parsl/channels/oauth_ssh/oauth_ssh.py,sha256=6pj3LQAX89p5Lc8NL1Llq2_noi8GS8BItCuRtDp-iCA,3823
27
27
  parsl/channels/ssh/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
28
- parsl/channels/ssh/ssh.py,sha256=3PfE3qYQOCr-BZrCseGiMKYFUILFPmW_CgvV63CWI4M,10494
28
+ parsl/channels/ssh/ssh.py,sha256=y21at_99Cjo2YNC110bf5dbNsOvAsUA-843LyOPkJH8,10156
29
29
  parsl/channels/ssh_il/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
30
  parsl/channels/ssh_il/ssh_il.py,sha256=acOXJyqCmgC2nl7zrO_uEu3GpJZMN2l-Af5XfmNMLRs,2783
31
31
  parsl/concurrent/__init__.py,sha256=TvIVceJYaJAsxedNBF3Vdo9lEQNHH_j3uxJv0zUjP7w,3288
@@ -62,7 +62,7 @@ parsl/data_provider/staging.py,sha256=ZDZuuFg38pjUStegKPcvPsfGp3iMeReMzfU6DSwtJj
62
62
  parsl/data_provider/zip.py,sha256=S4kVuH9lxAegRURYbvIUR7EYYBOccyslaqyCrVWUBhw,4497
63
63
  parsl/dataflow/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
64
64
  parsl/dataflow/dependency_resolvers.py,sha256=Om8Dgh7a0ZwgXAc6TlhxLSzvxXHDlNNV1aBNiD3JTNY,3325
65
- parsl/dataflow/dflow.py,sha256=2RV4MmQ3y6iwOT7aJaeWMsVPJ6tFT03V0YAcUbxogpk,68250
65
+ parsl/dataflow/dflow.py,sha256=g7il86TRBoulACyCZcDBkbFvFc7k1dV9cxoeb2wWngY,68230
66
66
  parsl/dataflow/errors.py,sha256=9SxVhIJY_53FQx8x4OU8UA8nd7lvUbDllH7KfMXpYaY,2177
67
67
  parsl/dataflow/futures.py,sha256=08LuP-HFiHBIZmeKCjlsazw_WpQ5fwevrU2_WbidkYw,6080
68
68
  parsl/dataflow/memoization.py,sha256=l9uw1Bu50GucBF70M5relpGKFkE4dIM9T3R1KrxW0v0,9583
@@ -81,7 +81,7 @@ parsl/executors/flux/flux_instance_manager.py,sha256=5T3Rp7ZM-mlT0Pf0Gxgs5_YmnaP
81
81
  parsl/executors/high_throughput/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
82
  parsl/executors/high_throughput/errors.py,sha256=Sak8e8UpiEcXefUjMHbhyXc4Rn7kJtOoh7L8wreBQdk,1638
83
83
  parsl/executors/high_throughput/executor.py,sha256=_dff5USFQq7V89kEXEWd2OqgYJQfq9i1b2e8FYA-zow,37511
84
- parsl/executors/high_throughput/interchange.py,sha256=elt48I-3WI4Wf5s7_3ECTw_fqqLPBDA2IzOiC4vqB14,29925
84
+ parsl/executors/high_throughput/interchange.py,sha256=DQjIb7SpBAfHUFr5kY9f_9lJhH_ZX0CktoFu7yyTXBQ,30111
85
85
  parsl/executors/high_throughput/manager_record.py,sha256=yn3L8TUJFkgm2lX1x0SeS9mkvJowC0s2VIMCFiU7ThM,455
86
86
  parsl/executors/high_throughput/manager_selector.py,sha256=UKcUE6v0tO7PDMTThpKSKxVpOpOUilxDL7UbNgpZCxo,2116
87
87
  parsl/executors/high_throughput/monitoring_info.py,sha256=HC0drp6nlXQpAop5PTUKNjdXMgtZVvrBL0JzZJebPP4,298
@@ -89,7 +89,7 @@ parsl/executors/high_throughput/mpi_executor.py,sha256=khvGz56A8zU8XAY-R4TtqqiJB
89
89
  parsl/executors/high_throughput/mpi_prefix_composer.py,sha256=DmpKugANNa1bdYlqQBLHkrFc15fJpefPPhW9hkAlh1s,4308
90
90
  parsl/executors/high_throughput/mpi_resource_management.py,sha256=LFBbJ3BnzTcY_v-jNu30uoIB2Enk4cleN4ygY3dncjY,8194
91
91
  parsl/executors/high_throughput/probe.py,sha256=TNpGTXb4_DEeg_h-LHu4zEKi1-hffboxvKcZUl2OZGk,2751
92
- parsl/executors/high_throughput/process_worker_pool.py,sha256=ndV6uJBd7ErVRZdL9Iy1362m9y3k36zMSe8w3CM6eBg,43074
92
+ parsl/executors/high_throughput/process_worker_pool.py,sha256=wpfKhA1hqbzfSnRfKn1WPFE_ZRd0LF07dBaK0YDXamg,42934
93
93
  parsl/executors/high_throughput/zmq_pipes.py,sha256=tAjQB3aNVMuTXziN3dbJWre46YpXgliD55qMBbhYTLU,8581
94
94
  parsl/executors/radical/__init__.py,sha256=CKbtV2numw5QvgIBq1htMUrt9TqDCIC2zifyf2svTNU,186
95
95
  parsl/executors/radical/executor.py,sha256=426cMt6d8uJFZ_7Ub1kCslaND4OKtBX5WZdz-0RXjMk,22554
@@ -121,14 +121,14 @@ parsl/launchers/base.py,sha256=CblcvPTJiu-MNLWaRtFe29SZQ0BpTOlaY8CGcHdlHIE,538
121
121
  parsl/launchers/errors.py,sha256=8YMV_CHpBNVa4eXkGE4x5DaFQlZkDCRCHmBktYcY6TA,467
122
122
  parsl/launchers/launchers.py,sha256=cQsNsHuCOL_nQTjPXf0--YsgsDoMoJ77bO1Wt4ncLjs,15134
123
123
  parsl/monitoring/__init__.py,sha256=0ywNz6i0lM1xo_7_BIxhETDGeVd2C_0wwD7qgeaMR4c,83
124
- parsl/monitoring/db_manager.py,sha256=l7Qiub4JsR6QUzTYUAJ9sVytZOvba2QMBdFH3cGbNIo,33336
124
+ parsl/monitoring/db_manager.py,sha256=G795Nme9di2AWT7zqFNNyOn8ZJd5i1I2hA6iDSorZD4,33330
125
125
  parsl/monitoring/errors.py,sha256=D6jpYzEzp0d6FmVKGqhvjAxr4ztZfJX2s-aXemH9bBU,148
126
126
  parsl/monitoring/message_type.py,sha256=Khn88afNxcOIciKiCK4GLnn90I5BlRTiOL3zK-P07yQ,401
127
- parsl/monitoring/monitoring.py,sha256=q_U2zpcd_hy0cxdWNXF_qhNBe1SQDipStvD1LdcWhlo,13098
128
- parsl/monitoring/radios.py,sha256=cHdpBOW1ITYvFnOgYjziuZOauq8p7mlSBOvcbIP78mg,6437
127
+ parsl/monitoring/monitoring.py,sha256=9P9IcXFd6m9YVPZlc1cTUlrvdIt2MibCvE84rtNsHWw,13062
128
+ parsl/monitoring/radios.py,sha256=mK7DfpeLbXnbXybLI_yS6clDNDmCCrZVAMVNaUI9DQA,6424
129
129
  parsl/monitoring/remote.py,sha256=avIWMvejN0LeIXpt_RCXJxGLbsXhapUab2rS5Tmjca4,13739
130
- parsl/monitoring/router.py,sha256=8zWTaYIXWsgpMranTTEPhTPqQSmT2ePK8JJmfW8K34s,9256
131
- parsl/monitoring/types.py,sha256=_WGizCTgQVOkJ2dvNfsvHpYBj21Ky3bJsmyIskIx10I,631
130
+ parsl/monitoring/router.py,sha256=5WrJ7YT2SV3T9BHCI8P0KqHm-4Y6NDgZkwmEcISmzGU,9110
131
+ parsl/monitoring/types.py,sha256=oOCrzv-ab-_rv4pb8o58Sdb8G_RGp1aZriRbdf9zBEk,339
132
132
  parsl/monitoring/queries/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
133
133
  parsl/monitoring/queries/pandas.py,sha256=0Z2r0rjTKCemf0eaDkF1irvVHn5g7KC5SYETvQPRxwU,2232
134
134
  parsl/monitoring/visualization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -153,7 +153,7 @@ parsl/monitoring/visualization/templates/task.html,sha256=omDwp7zFXHVtuGsUCXcB7x
153
153
  parsl/monitoring/visualization/templates/workflow.html,sha256=QCSHAPHK_2C3gNcZ3NmChLFG6xuchZEjT_iLQ3wwXmk,1871
154
154
  parsl/monitoring/visualization/templates/workflows_summary.html,sha256=7brKKNsxcT4z-l10BKJlgTxQtGL033ZS5jEDdSmsPEE,891
155
155
  parsl/providers/__init__.py,sha256=fvmVlu4aHw796K-fuUqxCHdK8KhrQviMARSmUQl1XXs,1077
156
- parsl/providers/base.py,sha256=u8oGlAaDfh15EgOJNJF1aZUy0Ou-UW6UY0b7ZI7Ecjo,5702
156
+ parsl/providers/base.py,sha256=Kj8adE9SgIFwvbd61qvwEdtuI3gfkqYVJEbI4bGsaZQ,5271
157
157
  parsl/providers/cluster_provider.py,sha256=o75wJHHyZkecjEBhGGBCMUQ1JlsecAhAKxX_Qd2pyg8,4668
158
158
  parsl/providers/errors.py,sha256=_CbCmpguzcA81SC5dPLkDZs1AShzacGKttNhuzNBeiQ,2270
159
159
  parsl/providers/ad_hoc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -251,8 +251,6 @@ parsl/tests/integration/latency.py,sha256=kWYkXsbnVnpwS6rHsdm7a1FsUOJWHhuXDsRPlA
251
251
  parsl/tests/integration/test_parsl_load_default_config.py,sha256=wecDvbmblTgE3gErejGNLqQNaBigDyUpiqry9eoVUcw,395
252
252
  parsl/tests/integration/test_apps/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
253
253
  parsl/tests/integration/test_channels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
254
- parsl/tests/integration/test_channels/test_channels.py,sha256=Nv_1ljrJ5Miqe4U5q9XPBqc0YZbJC90TIsH0p3203Gs,323
255
- parsl/tests/integration/test_channels/test_local_channel.py,sha256=_j9z4LqdfawEQRlae6EHpMtrhMPMapbIlJwoHEibAuE,1009
256
254
  parsl/tests/integration/test_stress/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
257
255
  parsl/tests/integration/test_stress/test_python_simple.py,sha256=QZMhi6E0OmMsKi3QkHJZdNpALSrWshrLcKsstLANUWE,1007
258
256
  parsl/tests/integration/test_stress/test_python_threads.py,sha256=-4dW-g69cu6uhSvk5HiH0fI6ceckQNqUXZGvNK6QGq4,897
@@ -303,6 +301,7 @@ parsl/tests/test_bash_apps/test_stdout.py,sha256=hrzHXLt308qH2Gg_r0-qy5nFBNXI56v
303
301
  parsl/tests/test_channels/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
304
302
  parsl/tests/test_channels/test_dfk_close.py,sha256=n7IF3Ud_vejg0VNRnvEgxCLmwMvPVvLbXvJdw-Mz_lw,628
305
303
  parsl/tests/test_channels/test_large_output.py,sha256=PGeNSW_sN5mR7KF1hVL2CPfktydYxo4oNz1wVQ-ENN0,595
304
+ parsl/tests/test_channels/test_local_channel.py,sha256=0TYjJRXBdeWbB60tCRQVZ4zZSXS4405eLzEIbcAWXy8,927
306
305
  parsl/tests/test_checkpointing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
307
306
  parsl/tests/test_checkpointing/test_periodic.py,sha256=nfMgrG7sZ8rkMu6iOHS6lp_iTU4IsOyQLQ2Gur_FMmE,1509
308
307
  parsl/tests/test_checkpointing/test_python_checkpoint_1.py,sha256=k7_Zy4CV9OQt4ORYFCdyX53c4B0YPiwEIi35LhvLB2w,746
@@ -425,9 +424,10 @@ parsl/tests/test_scaling/test_scale_down.py,sha256=u8TbbVM2PXgy4Zg7bAkh0C-KQuF1k
425
424
  parsl/tests/test_scaling/test_scale_down_htex_auto_scale.py,sha256=PKfH18sA11oNhf2Ub8BJjxbxdIfzEeL1Lk2E0VhLJrs,4605
426
425
  parsl/tests/test_scaling/test_scale_down_htex_unregistered.py,sha256=buNGB2wKG9omLf4d1R07zaxl1slVPuWtH8e1z4Hj70I,2038
427
426
  parsl/tests/test_scaling/test_shutdown_scalein.py,sha256=Jzi0OH7UE6qvQ4ZpsfHu8lySpkMDgorn2elAzMNE6wI,2397
427
+ parsl/tests/test_scaling/test_worker_interchange_bad_messages_3262.py,sha256=U4ZBx5HyjPmYZ2_Ti_gc0ZiChdDEC5BML5FnbpemmY4,2871
428
428
  parsl/tests/test_serialization/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
429
429
  parsl/tests/test_serialization/test_2555_caching_deserializer.py,sha256=jEXJvbriaLVI7frV5t-iJRKYyeQ7a9_-t3X9lhhBWQo,767
430
- parsl/tests/test_serialization/test_3495_deserialize_managerlost.py,sha256=23phMEstEWWQUHxlyZh4Ta1DC94by8f_-OYCcykyNu8,1144
430
+ parsl/tests/test_serialization/test_3495_deserialize_managerlost.py,sha256=GoMtK6BmARicawzYR2eQj5jUSL9RZ_tHV3g19BdQuQ8,1144
431
431
  parsl/tests/test_serialization/test_basic.py,sha256=4_1Rkq5tNl9EC0nfneF8kHTws7I0E6ovE_0DE97BEfU,544
432
432
  parsl/tests/test_serialization/test_htex_code_cache.py,sha256=dd0XwlNDn6Lgj6-nHHjYWzl1FnhFLY_8Buxj77dyZ28,1840
433
433
  parsl/tests/test_serialization/test_pack_resource_spec.py,sha256=-Vtyh8KyezZw8e7M2Z4m3LawY1Au4U-H3KRmVKXSut0,641
@@ -466,13 +466,13 @@ parsl/usage_tracking/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hS
466
466
  parsl/usage_tracking/api.py,sha256=iaCY58Dc5J4UM7_dJzEEs871P1p1HdxBMtNGyVdzc9g,1821
467
467
  parsl/usage_tracking/levels.py,sha256=xbfzYEsd55KiZJ-mzNgPebvOH4rRHum04hROzEf41tU,291
468
468
  parsl/usage_tracking/usage.py,sha256=tcoZ2OUjsQVakG8Uu9_HFuEdzpSHyt4JarSRcLGnSMw,8918
469
- parsl-2024.10.21.data/scripts/exec_parsl_function.py,sha256=RUkJ4JSJAjr7YyRZ58zhMdg8cR5dVV9odUl3AuzNf3k,7802
470
- parsl-2024.10.21.data/scripts/interchange.py,sha256=FcEEmcuMcuFBB_aNOLzaYr5w3Yw9zKJxhtKbIUPVfhI,29912
471
- parsl-2024.10.21.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
472
- parsl-2024.10.21.data/scripts/process_worker_pool.py,sha256=4K9vxwFHsz8QURwfq3VvnjEls7rYBxi2q0Gyy1cce5E,43060
473
- parsl-2024.10.21.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
474
- parsl-2024.10.21.dist-info/METADATA,sha256=8hfXCgoISytZwjc6AefQ1vMFLcHWbaOAmBHOJNmg_Ds,4072
475
- parsl-2024.10.21.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
476
- parsl-2024.10.21.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
477
- parsl-2024.10.21.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
478
- parsl-2024.10.21.dist-info/RECORD,,
469
+ parsl-2024.10.28.data/scripts/exec_parsl_function.py,sha256=RUkJ4JSJAjr7YyRZ58zhMdg8cR5dVV9odUl3AuzNf3k,7802
470
+ parsl-2024.10.28.data/scripts/interchange.py,sha256=6jsxpVgtruFtE_0nMHAZYVF1gvoALBCkprEbUb_YQgg,30098
471
+ parsl-2024.10.28.data/scripts/parsl_coprocess.py,sha256=zrVjEqQvFOHxsLufPi00xzMONagjVwLZbavPM7bbjK4,5722
472
+ parsl-2024.10.28.data/scripts/process_worker_pool.py,sha256=Qed0dgUa6375UgWm5h196V0FBdeTdW6iowG9RYDNG9Y,42920
473
+ parsl-2024.10.28.dist-info/LICENSE,sha256=tAkwu8-AdEyGxGoSvJ2gVmQdcicWw3j1ZZueVV74M-E,11357
474
+ parsl-2024.10.28.dist-info/METADATA,sha256=QrGvIyy6IrEP8lsBKtOKsOKAvgL3GeIyDUw29t81mAc,4072
475
+ parsl-2024.10.28.dist-info/WHEEL,sha256=eOLhNAGa2EW3wWl_TU484h7q1UNgy0JXjjoqKoxAAQc,92
476
+ parsl-2024.10.28.dist-info/entry_points.txt,sha256=XqnsWDYoEcLbsMcpnYGKLEnSBmaIe1YoM5YsBdJG2tI,176
477
+ parsl-2024.10.28.dist-info/top_level.txt,sha256=PIheYoUFQtF2icLsgOykgU-Cjuwr2Oi6On2jo5RYgRM,6
478
+ parsl-2024.10.28.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- from parsl.channels.local.local import LocalChannel
2
-
3
-
4
- def test_local():
5
-
6
- channel = LocalChannel(None, None)
7
-
8
- ec, out, err = channel.execute_wait('echo "pwd: $PWD"', 2)
9
-
10
- assert ec == 0, "Channel execute failed"
11
- print("Stdout: ", out)
12
- print("Stderr: ", err)
13
-
14
-
15
- if __name__ == "__main__":
16
-
17
- test_local()