runnable 0.32.1__py3-none-any.whl → 0.32.3__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.
@@ -329,7 +329,7 @@ class GenericK8sJobExecutor(GenericJobExecutor):
329
329
 
330
330
  logger.info(f"Submitting job: {job.__dict__}")
331
331
  if self.mock:
332
- print(job.__dict__)
332
+ logger.info(job.__dict__)
333
333
  return
334
334
 
335
335
  try:
extensions/nodes/torch.py CHANGED
@@ -12,7 +12,7 @@ from pydantic import BaseModel, ConfigDict, Field, field_serializer
12
12
  from extensions.nodes.torch_config import EasyTorchConfig, TorchConfig
13
13
  from runnable import PythonJob, datastore, defaults
14
14
  from runnable.datastore import StepLog
15
- from runnable.nodes import DistributedNode
15
+ from runnable.nodes import ExecutableNode
16
16
  from runnable.tasks import PythonTaskType, create_task
17
17
  from runnable.utils import TypeMapVariable
18
18
 
@@ -21,11 +21,9 @@ logger = logging.getLogger(defaults.LOGGER_NAME)
21
21
  try:
22
22
  from torch.distributed.elastic.multiprocessing.api import DefaultLogsSpecs, Std
23
23
  from torch.distributed.launcher.api import LaunchConfig, elastic_launch
24
-
25
24
  except ImportError:
26
- raise ImportError("torch is not installed. Please install torch first.")
27
-
28
- print("torch is installed")
25
+ logger.exception("Torch is not installed. Please install torch first.")
26
+ raise Exception("Torch is not installed. Please install torch first.")
29
27
 
30
28
 
31
29
  def training_subprocess():
@@ -119,7 +117,7 @@ def delete_env_vars_with_prefix(prefix):
119
117
 
120
118
 
121
119
  # TODO: The design of this class is not final
122
- class TorchNode(DistributedNode, TorchConfig):
120
+ class TorchNode(ExecutableNode, TorchConfig):
123
121
  node_type: str = Field(default="torch", serialization_alias="type")
124
122
  executable: PythonTaskType = Field(exclude=True)
125
123
 
@@ -20,14 +20,10 @@ from pydantic import (
20
20
  from pydantic.alias_generators import to_camel
21
21
  from ruamel.yaml import YAML
22
22
 
23
- from extensions.nodes.nodes import (
24
- MapNode,
25
- ParallelNode,
26
- StubNode,
27
- SuccessNode,
28
- TaskNode,
29
- )
30
- from extensions.nodes.torch import TorchNode
23
+ from extensions.nodes.nodes import MapNode, ParallelNode, TaskNode
24
+
25
+ # TODO: Should be part of a wider refactor
26
+ # from extensions.nodes.torch import TorchNode
31
27
  from extensions.pipeline_executor import GenericPipelineExecutor
32
28
  from runnable import defaults, utils
33
29
  from runnable.defaults import TypeMapVariable
@@ -590,12 +586,7 @@ class ArgoExecutor(GenericPipelineExecutor):
590
586
  task_name: str,
591
587
  inputs: Optional[Inputs] = None,
592
588
  ) -> ContainerTemplate:
593
- assert (
594
- isinstance(node, TaskNode)
595
- or isinstance(node, StubNode)
596
- or isinstance(node, SuccessNode)
597
- or isinstance(node, TorchNode)
598
- )
589
+ assert node.node_type in ["task", "torch", "success", "stub", "fail"]
599
590
 
600
591
  node_override = None
601
592
  if hasattr(node, "overrides"):
@@ -658,7 +649,7 @@ class ArgoExecutor(GenericPipelineExecutor):
658
649
  def _set_env_vars_to_task(
659
650
  self, working_on: BaseNode, container_template: CoreContainerTemplate
660
651
  ):
661
- if not (isinstance(working_on, TaskNode) or isinstance(working_on, TorchNode)):
652
+ if working_on.node_type not in ["task", "torch"]:
662
653
  return
663
654
 
664
655
  global_envs: dict[str, str] = {}
@@ -667,7 +658,7 @@ class ArgoExecutor(GenericPipelineExecutor):
667
658
  env_var = cast(EnvVar, env_var)
668
659
  global_envs[env_var.name] = env_var.value
669
660
 
670
- override_key = working_on.overrides.get(self.service_name, "")
661
+ override_key = working_on.overrides.get(self.service_name, "") # type: ignore
671
662
  node_override = self.overrides.get(override_key, None)
672
663
 
673
664
  # Update the global envs with the node overrides
@@ -878,6 +869,8 @@ class ArgoExecutor(GenericPipelineExecutor):
878
869
  self._templates.append(composite_template)
879
870
 
880
871
  case "torch":
872
+ from extensions.nodes.torch import TorchNode
873
+
881
874
  assert isinstance(working_on, TorchNode)
882
875
  # TODO: Need to add multi-node functionality
883
876
  # Check notes on the torch node
extensions/tasks/torch.py CHANGED
@@ -17,15 +17,22 @@ from runnable.datastore import StepAttempt
17
17
  from runnable.tasks import BaseTaskType
18
18
  from runnable.utils import get_module_and_attr_names
19
19
 
20
+ logger = logging.getLogger(defaults.LOGGER_NAME)
21
+
22
+ logger = logging.getLogger(defaults.LOGGER_NAME)
23
+
20
24
  try:
21
25
  from torch.distributed.elastic.multiprocessing.api import DefaultLogsSpecs, Std
22
26
  from torch.distributed.launcher.api import LaunchConfig, elastic_launch
23
27
 
24
- except ImportError:
25
- raise ImportError("torch is not installed. Please install torch first.")
28
+ except ImportError as e:
29
+ logger.exception("torch is not installed")
30
+ raise Exception("torch is not installed") from e
26
31
 
27
32
 
28
- logger = logging.getLogger(defaults.LOGGER_NAME)
33
+ def get_min_max_nodes(nnodes: str) -> tuple[int, int]:
34
+ min_nodes, max_nodes = (int(x) for x in nnodes.split(":"))
35
+ return min_nodes, max_nodes
29
36
 
30
37
 
31
38
  class TorchTaskType(BaseTaskType, TorchConfig):
@@ -60,7 +67,8 @@ class TorchTaskType(BaseTaskType, TorchConfig):
60
67
  exclude_none=True,
61
68
  )
62
69
  )
63
-
70
+ print("###", easy_torch_config)
71
+ print("###", easy_torch_config)
64
72
  launch_config = LaunchConfig(
65
73
  **easy_torch_config.model_dump(
66
74
  exclude_none=True,
@@ -77,7 +85,53 @@ class TorchTaskType(BaseTaskType, TorchConfig):
77
85
  ):
78
86
  assert map_variable is None, "map_variable is not supported for torch"
79
87
 
88
+ # The below should happen only if we are in the node that we want to execute
89
+ # For a single node, multi worker setup, this should be the entry point
90
+ # For a multi-node, we need to:
91
+ # - create a service config
92
+ # - Create a stateful set with number of nodes
93
+ # - Create a job to run the torch.distributed.launcher.api.elastic_launch on every node
94
+ # - the entry point to runnnable could be a way to trigger execution instead of scaling
95
+ is_execute = os.environ.get("RUNNABLE_TORCH_EXECUTE", "true") == "true"
96
+
97
+ _, max_nodes = get_min_max_nodes(self.nnodes)
98
+
99
+ if max_nodes > 1 and not is_execute:
100
+ executor = self._context.executor
101
+ executor.scale_up(self)
102
+ return StepAttempt(
103
+ status=defaults.SUCCESS,
104
+ start_time=str(datetime.now()),
105
+ end_time=str(datetime.now()),
106
+ attempt_number=1,
107
+ message="Triggered a scale up",
108
+ )
109
+
110
+ # The below should happen only if we are in the node that we want to execute
111
+ # For a single node, multi worker setup, this should be the entry point
112
+ # For a multi-node, we need to:
113
+ # - create a service config
114
+ # - Create a stateful set with number of nodes
115
+ # - Create a job to run the torch.distributed.launcher.api.elastic_launch on every node
116
+ # - the entry point to runnnable could be a way to trigger execution instead of scaling
117
+ is_execute = os.environ.get("RUNNABLE_TORCH_EXECUTE", "true") == "true"
118
+
119
+ _, max_nodes = get_min_max_nodes(self.nnodes)
120
+
121
+ if max_nodes > 1 and not is_execute:
122
+ executor = self._context.executor
123
+ executor.scale_up(self)
124
+ return StepAttempt(
125
+ status=defaults.SUCCESS,
126
+ start_time=str(datetime.now()),
127
+ end_time=str(datetime.now()),
128
+ attempt_number=1,
129
+ message="Triggered a scale up",
130
+ )
131
+
80
132
  launch_config = self._get_launch_config()
133
+ print("###****", launch_config)
134
+ print("###****", launch_config)
81
135
  logger.info(f"launch_config: {launch_config}")
82
136
 
83
137
  # ENV variables are shared with the subprocess, use that as communication
@@ -175,9 +229,6 @@ def training_subprocess():
175
229
  self._context.parameters_file or ""
176
230
  )
177
231
  os.environ["RUNNABLE_TORCH_RUN_ID"] = self._context.run_id
178
- os.environ["RUNNABLE_TORCH_COPY_CONTENTS_TO"] = (
179
- self._context.catalog_handler.compute_data_folder
180
- )
181
232
  os.environ["RUNNABLE_TORCH_TORCH_LOGS"] = self.log_dir or ""
182
233
 
183
234
  """
@@ -43,7 +43,7 @@ class TorchConfig(BaseModel):
43
43
  # and sent at the creation of the LaunchConfig
44
44
 
45
45
  # This section is about the communication between nodes/processes
46
- rdzv_backend: str | None = Field(default="static")
46
+ rdzv_backend: str | None = Field(default="")
47
47
  rdzv_endpoint: str | None = Field(default="")
48
48
  rdzv_configs: dict[str, Any] = Field(default_factory=dict)
49
49
  rdzv_timeout: int | None = Field(default=None)
runnable/executor.py CHANGED
@@ -153,6 +153,14 @@ class BaseJobExecutor(BaseExecutor):
153
153
  """
154
154
  ...
155
155
 
156
+ # @abstractmethod
157
+ # def scale_up(self, job: BaseTaskType):
158
+ # """
159
+ # Scale up the job to run on max_nodes
160
+ # This has to also call the entry point
161
+ # """
162
+ # ...
163
+
156
164
 
157
165
  # TODO: Consolidate execute_node, trigger_node_execution, _execute_node
158
166
  class BasePipelineExecutor(BaseExecutor):
runnable/nodes.py CHANGED
@@ -36,8 +36,8 @@ class BaseNode(ABC, BaseModel):
36
36
  name: str
37
37
  internal_name: str = Field(exclude=True)
38
38
  internal_branch_name: str = Field(default="", exclude=True)
39
+
39
40
  is_composite: bool = Field(default=False, exclude=True)
40
- is_distributed: bool = Field(default=False, exclude=True)
41
41
 
42
42
  @property
43
43
  def _context(self):
@@ -483,41 +483,6 @@ class CompositeNode(TraversalNode):
483
483
  )
484
484
 
485
485
 
486
- class DistributedNode(TraversalNode):
487
- """
488
- Use this node for distributed execution of tasks.
489
- eg: torch distributed, horovod, etc.
490
- """
491
-
492
- is_distributed: bool = True
493
- catalog: Optional[CatalogStructure] = Field(default=None)
494
- max_attempts: int = Field(default=1, ge=1)
495
-
496
- def _get_catalog_settings(self) -> Dict[str, Any]:
497
- """
498
- If the node defines a catalog settings, return it or None
499
-
500
- Returns:
501
- dict: catalog settings defined as per the node or None
502
- """
503
- if self.catalog:
504
- return self.catalog.model_dump()
505
- return {}
506
-
507
- def _get_max_attempts(self) -> int:
508
- return self.max_attempts
509
-
510
- def _get_branch_by_name(self, branch_name: str):
511
- raise exceptions.NodeMethodCallError(
512
- "This is an distributed node and does not have branches"
513
- )
514
-
515
- def execute_as_graph(self, map_variable: TypeMapVariable = None):
516
- raise exceptions.NodeMethodCallError(
517
- "This is an executable node and does not have a graph"
518
- )
519
-
520
-
521
486
  class TerminalNode(BaseNode):
522
487
  def _get_on_failure_node(self) -> str:
523
488
  return ""
runnable/sdk.py CHANGED
@@ -5,7 +5,7 @@ import os
5
5
  import re
6
6
  from abc import ABC, abstractmethod
7
7
  from pathlib import Path
8
- from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
8
+ from typing import Any, Callable, Dict, List, Optional, Union
9
9
 
10
10
  from pydantic import (
11
11
  BaseModel,
@@ -34,7 +34,7 @@ from extensions.nodes.nodes import (
34
34
  SuccessNode,
35
35
  TaskNode,
36
36
  )
37
- from extensions.nodes.torch_config import TorchConfig
37
+ from extensions.tasks.torch_config import TorchConfig
38
38
  from runnable import console, defaults, entrypoints, exceptions, graph, utils
39
39
  from runnable.executor import BaseJobExecutor, BasePipelineExecutor
40
40
  from runnable.nodes import TraversalNode
@@ -46,8 +46,6 @@ logger = logging.getLogger(defaults.LOGGER_NAME)
46
46
  StepType = Union[
47
47
  "Stub", "PythonTask", "NotebookTask", "ShellTask", "Parallel", "Map", "TorchTask"
48
48
  ]
49
- if TYPE_CHECKING:
50
- pass
51
49
 
52
50
 
53
51
  def pickled(name: str) -> TaskReturns:
@@ -192,6 +190,8 @@ class BaseTask(BaseTraversal):
192
190
 
193
191
 
194
192
  class TorchTask(BaseTask, TorchConfig):
193
+ # The user will not know the rnnz variables for multi node
194
+ # They should be overridden in the environment
195
195
  function: Callable = Field(exclude=True)
196
196
 
197
197
  @field_validator("returns", mode="before")
runnable/tasks.py CHANGED
@@ -760,7 +760,6 @@ def create_task(kwargs_for_init) -> BaseTaskType:
760
760
  """
761
761
  # The dictionary cannot be modified
762
762
 
763
- print(kwargs_for_init)
764
763
  kwargs = kwargs_for_init.copy()
765
764
  command_type = kwargs.pop("command_type", defaults.COMMAND_TYPE)
766
765
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runnable
3
- Version: 0.32.1
3
+ Version: 0.32.3
4
4
  Summary: Add your description here
5
5
  Author-email: "Vammi, Vijay" <vijay.vammi@astrazeneca.com>
6
6
  License-File: LICENSE
@@ -8,7 +8,7 @@ extensions/catalog/pyproject.toml,sha256=lLNxY6v04c8I5QK_zKw_E6sJTArSJRA_V-79kta
8
8
  extensions/catalog/s3.py,sha256=Sw5t8_kVRprn3uGGJCiHn7M9zw1CLaCOFj6YErtfG0o,287
9
9
  extensions/job_executor/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  extensions/job_executor/__init__.py,sha256=VeLuYCcShCIYT0TNtAXfUF9tOk4ZHoLzdTEvbsz0spM,5870
11
- extensions/job_executor/k8s.py,sha256=0V7BL7ERmonVMgCsO-J57cxH__v8KomwukMwepH3qgs,16388
11
+ extensions/job_executor/k8s.py,sha256=Jl0s3YryISx-SJIhDhyNskzlUlhy4ynBHEc9DfAXjAY,16394
12
12
  extensions/job_executor/k8s_job_spec.yaml,sha256=7aFpxHdO_p6Hkc3YxusUOuAQTD1Myu0yTPX9DrhxbOg,1158
13
13
  extensions/job_executor/local.py,sha256=3ZbCFXBvbLlMp10JTmQJJrjBKG2keHI6SH8hEvmHDkA,2230
14
14
  extensions/job_executor/local_container.py,sha256=1JcLJ0zrNSNHdubrSO9miN54iwvPLHqKMZ08aOC8WWo,6886
@@ -16,11 +16,11 @@ extensions/job_executor/pyproject.toml,sha256=UIEgiCYHTXcRWSByNMFuKJFKgxTBpQqTqy
16
16
  extensions/nodes/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  extensions/nodes/nodes.py,sha256=s9ub1dqy4qHjRQG6YElCdL7rCOTYNs9RUIrStZ6tEB4,28256
18
18
  extensions/nodes/pyproject.toml,sha256=YTu-ETN3JNFSkMzzWeOwn4m-O2nbRH-PmiPBALDCUw4,278
19
- extensions/nodes/torch.py,sha256=h3x5931ePBNckeSXM3JFjSoUnxmIWvDyEpn1AI9TKaU,9347
19
+ extensions/nodes/torch.py,sha256=64DTjdPNSJ8vfMwUN9h9Ly5g9qj-Bga7LSGrfCAO0BY,9389
20
20
  extensions/nodes/torch_config.py,sha256=tO3sG2_fj8a6FmPZZllwKVx3WaRr4QmQYcACseg8YXM,2839
21
21
  extensions/pipeline_executor/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
22
  extensions/pipeline_executor/__init__.py,sha256=wfigTL2T9OHrmE8b2Ydmb8h6hr-oF--Yc2FectC7WaY,24623
23
- extensions/pipeline_executor/argo.py,sha256=AEGSWVZulBL6EsvbVCaeBeTl2m_t5ymc6RFpMKhivis,37946
23
+ extensions/pipeline_executor/argo.py,sha256=lHM3TM_UnQc4I1ghkuYdeBLpyr4pBLg-Ubnaf55Zw54,37878
24
24
  extensions/pipeline_executor/local.py,sha256=6oWUJ6b6NvIkpeQJBoCT1hbfX4_6WCB4HzMgHZ4ik1A,1887
25
25
  extensions/pipeline_executor/local_container.py,sha256=3kZ2QCsrq_YjH9dcAz8v05knKShQ_JtbIU-IA_-G538,12724
26
26
  extensions/pipeline_executor/mocked.py,sha256=0sMmypuvstBIv9uQg-WAcPrF3oOFpeEXNi6N8Nzdnl0,5680
@@ -40,8 +40,8 @@ extensions/run_log_store/db/integration_FF.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeR
40
40
  extensions/secrets/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
41
  extensions/secrets/dotenv.py,sha256=nADHXI6KJ_LUYOIe5EbtYH-21OBebSNVr0Pjb1GlZ7w,1573
42
42
  extensions/secrets/pyproject.toml,sha256=mLJNImNcBlbLKHh-0ugVWT9V83R4RibyyYDtBCSqVF4,282
43
- extensions/tasks/torch.py,sha256=R0J_Q6SRAW2Ii0XQbXaaBWTah8TYs4P_48j2M1bIXeA,7983
44
- extensions/tasks/torch_config.py,sha256=tO3sG2_fj8a6FmPZZllwKVx3WaRr4QmQYcACseg8YXM,2839
43
+ extensions/tasks/torch.py,sha256=oeXRkmuttFIAuBwH7-h4SOVXMDOZXX5mvqI2aFrR3Vo,10283
44
+ extensions/tasks/torch_config.py,sha256=UjfMitT-TXASRDGR30I2vDRnyk7JQnR-5CsOVidjpSY,2833
45
45
  runnable/__init__.py,sha256=3ZKuvGEkY_zHVQlJtarXd4jkjICxjgnw-bbKN_5SiJI,691
46
46
  runnable/catalog.py,sha256=4msQxLhLKlsDDrHFnGauPYe-Or-q9g8_RYCn_4dpxaU,4466
47
47
  runnable/cli.py,sha256=3BiKSj95h2Drn__YlchMPZ5rBMafuRb2OGIsVpbsO5Y,8788
@@ -50,18 +50,18 @@ runnable/datastore.py,sha256=ZobM1aVkgeUJ2fZYt63IFDsoNzObwc93hdByegS5YKQ,32396
50
50
  runnable/defaults.py,sha256=3o9IVGryyCE6PoQTOoaIaHHTbJGEzmdXMcwzOhwAYoI,3518
51
51
  runnable/entrypoints.py,sha256=1xCbWVUQLGmg5gkWnAVWFLAUf6j4avP9azX_vuGQUMY,18985
52
52
  runnable/exceptions.py,sha256=LFbp0-Qxg2PAMLEVt7w2whhBxSG-5pzUEv5qN-Rc4_c,3003
53
- runnable/executor.py,sha256=UOsYJ3NkTGw4FTR0iePX7AOJzY7vODhZ62aqrwVMO1c,15143
53
+ runnable/executor.py,sha256=Jr9yJtSH7CzjXJLWx3VWIUAQblstuGqzpFtajv7d39M,15348
54
54
  runnable/graph.py,sha256=poQz5zcvq89ju_u5sYlunQLPbHnXTaUmjcvstPwvT4U,16536
55
55
  runnable/names.py,sha256=vn92Kv9ANROYSZX6Z4z1v_WA3WiEdIYmG6KEStBFZug,8134
56
- runnable/nodes.py,sha256=d1eLttMAcV7CTwTEqOuNwZqItANoLUkXJ73Xp-srlyI,17811
56
+ runnable/nodes.py,sha256=QGHMznriEz4AcmntHICBZKrDT6zbc7WD1sV0MgwK10c,16691
57
57
  runnable/parameters.py,sha256=u77CdqqDAbVdzNeBFPNUfGnWPy9-SpBVmwEJ56xmDm8,5289
58
58
  runnable/pickler.py,sha256=ydJ_eti_U1F4l-YacFp7BWm6g5vTn04UXye25S1HVok,2684
59
- runnable/sdk.py,sha256=J1PyiHQD2v_0JaqHjY7xSaXwCUMi_mCNr70TsC-SFZU,35012
59
+ runnable/sdk.py,sha256=hwsEGCCFSijm0DZwDJGHmV8jdMuSU_3Pf-vYoomWYHw,35084
60
60
  runnable/secrets.py,sha256=4L_dBFxTgr8r_hHUD6RlZEtqaOHDRsFG5PXO5wlvMI0,2324
61
- runnable/tasks.py,sha256=_A0pcTyOGQL-72AicOxracsrwfs2Vg0r4mQyxz3k6Iw,29016
61
+ runnable/tasks.py,sha256=ABRhgiTY8F62pNlqJmVTDjwJwuzp8DqciUEOq1fpt1U,28989
62
62
  runnable/utils.py,sha256=hBr7oGwGL2VgfITlQCTz-a1iwvvf7Mfl-HY8UdENZac,19929
63
- runnable-0.32.1.dist-info/METADATA,sha256=07vRwa5svN-dopa4Pxh8fQcSM8jiCIO7s0o1V0IVtsE,10168
64
- runnable-0.32.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
65
- runnable-0.32.1.dist-info/entry_points.txt,sha256=uWHbbOSj0jlG54tFHw377xKkfVbjWvb_1Y9L_LgjJ0Q,1925
66
- runnable-0.32.1.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
67
- runnable-0.32.1.dist-info/RECORD,,
63
+ runnable-0.32.3.dist-info/METADATA,sha256=l0jxi_VKPXblTa_Kd-fnqKophmB2e8x1Dj1HDbJV570,10168
64
+ runnable-0.32.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
65
+ runnable-0.32.3.dist-info/entry_points.txt,sha256=uWHbbOSj0jlG54tFHw377xKkfVbjWvb_1Y9L_LgjJ0Q,1925
66
+ runnable-0.32.3.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
67
+ runnable-0.32.3.dist-info/RECORD,,