runnable 0.17.0__py3-none-any.whl → 0.18.0__py3-none-any.whl

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. extensions/README.md +0 -0
  2. extensions/__init__.py +0 -0
  3. extensions/catalog/README.md +0 -0
  4. extensions/catalog/file_system.py +253 -0
  5. extensions/catalog/pyproject.toml +14 -0
  6. extensions/job_executor/README.md +0 -0
  7. extensions/job_executor/__init__.py +160 -0
  8. extensions/job_executor/k8s.py +362 -0
  9. extensions/job_executor/k8s_job_spec.yaml +37 -0
  10. extensions/job_executor/local.py +61 -0
  11. extensions/job_executor/local_container.py +192 -0
  12. extensions/job_executor/pyproject.toml +16 -0
  13. extensions/nodes/README.md +0 -0
  14. extensions/nodes/nodes.py +954 -0
  15. extensions/nodes/pyproject.toml +15 -0
  16. extensions/pipeline_executor/README.md +0 -0
  17. extensions/pipeline_executor/__init__.py +644 -0
  18. extensions/pipeline_executor/argo.py +1307 -0
  19. extensions/pipeline_executor/argo_specification.yaml +51 -0
  20. extensions/pipeline_executor/local.py +62 -0
  21. extensions/pipeline_executor/local_container.py +363 -0
  22. extensions/pipeline_executor/mocked.py +161 -0
  23. extensions/pipeline_executor/pyproject.toml +16 -0
  24. extensions/pipeline_executor/retry.py +180 -0
  25. extensions/run_log_store/README.md +0 -0
  26. extensions/run_log_store/__init__.py +0 -0
  27. extensions/run_log_store/chunked_fs.py +113 -0
  28. extensions/run_log_store/db/implementation_FF.py +163 -0
  29. extensions/run_log_store/db/integration_FF.py +0 -0
  30. extensions/run_log_store/file_system.py +145 -0
  31. extensions/run_log_store/generic_chunked.py +599 -0
  32. extensions/run_log_store/pyproject.toml +15 -0
  33. extensions/secrets/README.md +0 -0
  34. extensions/secrets/dotenv.py +62 -0
  35. extensions/secrets/pyproject.toml +15 -0
  36. runnable/sdk.py +40 -99
  37. {runnable-0.17.0.dist-info → runnable-0.18.0.dist-info}/METADATA +1 -7
  38. runnable-0.18.0.dist-info/RECORD +58 -0
  39. runnable-0.17.0.dist-info/RECORD +0 -23
  40. {runnable-0.17.0.dist-info → runnable-0.18.0.dist-info}/WHEEL +0 -0
  41. {runnable-0.17.0.dist-info → runnable-0.18.0.dist-info}/entry_points.txt +0 -0
  42. {runnable-0.17.0.dist-info → runnable-0.18.0.dist-info}/licenses/LICENSE +0 -0
runnable/sdk.py CHANGED
@@ -84,7 +84,7 @@ class BaseTraversal(ABC, BaseModel):
84
84
  next_node: str = Field(default="", serialization_alias="next_node")
85
85
  terminate_with_success: bool = Field(default=False, exclude=True)
86
86
  terminate_with_failure: bool = Field(default=False, exclude=True)
87
- on_failure: str = Field(default="", alias="on_failure")
87
+ on_failure: Optional[Pipeline] = Field(default=None)
88
88
 
89
89
  model_config = ConfigDict(extra="forbid")
90
90
 
@@ -117,18 +117,6 @@ class BaseTraversal(ABC, BaseModel):
117
117
 
118
118
  return other
119
119
 
120
- def depends_on(self, node: StepType) -> Self:
121
- assert not isinstance(node, Success)
122
- assert not isinstance(node, Fail)
123
-
124
- if node.next_node:
125
- raise Exception(
126
- f"The {node} node already has a next node: {node.next_node}"
127
- )
128
-
129
- node.next_node = self.name
130
- return self
131
-
132
120
  @model_validator(mode="after")
133
121
  def validate_terminations(self) -> Self:
134
122
  if self.terminate_with_failure and self.terminate_with_success:
@@ -175,7 +163,6 @@ class BaseTask(BaseTraversal):
175
163
  if isinstance(x, str):
176
164
  task_returns.append(TaskReturns(name=x, kind="json"))
177
165
  continue
178
-
179
166
  # Its already task returns
180
167
  task_returns.append(x)
181
168
 
@@ -188,6 +175,9 @@ class BaseTask(BaseTraversal):
188
175
  "A node not being terminated must have a user defined next node"
189
176
  )
190
177
 
178
+ if self.on_failure:
179
+ self.on_failure = self.on_failure.steps[0].name # type: ignore
180
+
191
181
  return TaskNode.parse_from_config(
192
182
  self.model_dump(exclude_none=True, by_alias=True)
193
183
  )
@@ -605,8 +595,6 @@ class Pipeline(BaseModel):
605
595
  The order of steps is important as it determines the order of execution.
606
596
  Any on failure behavior should the first step in ```on_failure``` pipelines.
607
597
 
608
-
609
-
610
598
  on_failure (List[List[Pipeline], optional): A list of Pipelines to execute in case of failure.
611
599
 
612
600
  For example, for the below pipeline:
@@ -624,7 +612,7 @@ class Pipeline(BaseModel):
624
612
 
625
613
  """
626
614
 
627
- steps: List[Union[StepType, List["Pipeline"]]]
615
+ steps: List[StepType]
628
616
  name: str = ""
629
617
  description: str = ""
630
618
 
@@ -637,114 +625,67 @@ class Pipeline(BaseModel):
637
625
  _dag: graph.Graph = PrivateAttr()
638
626
  model_config = ConfigDict(extra="forbid")
639
627
 
640
- def _validate_path(self, path: List[StepType], failure_path: bool = False) -> None:
641
- # TODO: Drastically simplify this
642
- # Check if one and only one step terminates with success
643
- # Check no more than one step terminates with failure
644
-
645
- reached_success = False
646
- reached_failure = False
647
-
648
- for step in path:
649
- if step.terminate_with_success:
650
- if reached_success:
651
- raise Exception(
652
- "A pipeline cannot have more than one step that terminates with success"
653
- )
654
- reached_success = True
655
- continue
656
- if step.terminate_with_failure:
657
- if reached_failure:
658
- raise Exception(
659
- "A pipeline cannot have more than one step that terminates with failure"
660
- )
661
- reached_failure = True
662
-
663
- if not reached_success and not reached_failure:
664
- raise Exception(
665
- "A pipeline must have at least one step that terminates with success"
666
- )
667
-
668
- def _construct_path(self, path: List[StepType]) -> None:
669
- prev_step = path[0]
670
-
671
- for step in path:
672
- if step == prev_step:
673
- continue
674
-
675
- if prev_step.terminate_with_success or prev_step.terminate_with_failure:
676
- raise Exception(
677
- f"A step that terminates with success/failure cannot have a next step: {prev_step}"
678
- )
679
-
680
- if prev_step.next_node and prev_step.next_node not in ["success", "fail"]:
681
- raise Exception(f"Step already has a next node: {prev_step} ")
682
-
683
- prev_step.next_node = step.name
684
- prev_step = step
685
-
686
628
  def model_post_init(self, __context: Any) -> None:
687
629
  """
688
630
  The sequence of steps can either be:
689
- [step1, step2,..., stepN, [step11, step12,..., step1N], [step21, step22,...,]]
631
+ [step1, step2,..., stepN]
690
632
  indicates:
691
633
  - step1 > step2 > ... > stepN
692
634
  - We expect terminate with success or fail to be explicitly stated on a step.
693
635
  - If it is stated, the step cannot have a next step defined apart from "success" and "fail".
694
-
695
- The inner list of steps is only to accommodate on-failure behaviors.
696
- - For sake of simplicity, lets assume that it has the same behavior as the happy pipeline.
697
- - A task which was already seen should not be part of this.
698
- - There should be at least one step which terminates with success
699
-
700
636
  Any definition of pipeline should have one node that terminates with success.
701
637
  """
702
- # TODO: Bug with repeat names
703
- # TODO: https://github.com/AstraZeneca/runnable/issues/156
638
+ # The last step of the pipeline is defaulted to be a success step
639
+ # unless it is explicitly stated to terminate with failure.
640
+ terminal_step: StepType = self.steps[-1]
641
+ if not terminal_step.terminate_with_failure:
642
+ terminal_step.terminate_with_success = True
704
643
 
705
- success_path: List[StepType] = []
706
- on_failure_paths: List[List[StepType]] = []
644
+ # assert that there is only one termination node with success or failure
645
+ # Assert that there are no duplicate step names
646
+ observed: Dict[str, str] = {}
647
+ count_termination: int = 0
707
648
 
708
649
  for step in self.steps:
709
650
  if isinstance(
710
651
  step, (Stub, PythonTask, NotebookTask, ShellTask, Parallel, Map)
711
652
  ):
712
- success_path.append(step)
713
- continue
714
- # on_failure_paths.append(step)
715
-
716
- if not success_path:
717
- raise Exception("There should be some success path")
718
-
719
- # Check all paths are valid and construct the path
720
- paths = [success_path] + on_failure_paths
721
- failure_path = False
722
- for path in paths:
723
- self._validate_path(path, failure_path)
724
- self._construct_path(path)
725
-
726
- failure_path = True
653
+ if step.terminate_with_success or step.terminate_with_failure:
654
+ count_termination += 1
655
+ if step.name in observed:
656
+ raise Exception(
657
+ f"Step names should be unique. Found duplicate: {step.name}"
658
+ )
659
+ observed[step.name] = step.name
727
660
 
728
- all_steps: List[StepType] = []
661
+ if count_termination > 1:
662
+ raise AssertionError(
663
+ "A pipeline can only have one termination node with success or failure"
664
+ )
729
665
 
730
- for path in paths:
731
- for step in path:
732
- all_steps.append(step)
666
+ # link the steps by assigning the next_node name to be that name of the node
667
+ # immediately after it.
668
+ for i in range(len(self.steps) - 1):
669
+ self.steps[i] >> self.steps[i + 1]
733
670
 
734
- seen = set()
735
- unique = [x for x in all_steps if not (x in seen or seen.add(x))] # type: ignore
671
+ # Add any on_failure pipelines to the steps
672
+ gathered_on_failure: List[StepType] = []
673
+ for step in self.steps:
674
+ if step.on_failure:
675
+ gathered_on_failure.extend(step.on_failure.steps)
736
676
 
737
677
  self._dag = graph.Graph(
738
- start_at=all_steps[0].name,
678
+ start_at=self.steps[0].name,
739
679
  description=self.description,
740
680
  internal_branch_name=self.internal_branch_name,
741
681
  )
742
682
 
743
- for step in unique:
683
+ self.steps.extend(gathered_on_failure)
684
+
685
+ for step in self.steps:
744
686
  self._dag.add_node(step.create_node())
745
687
 
746
- if self.add_terminal_nodes:
747
- self._dag.add_terminal_nodes()
688
+ self._dag.add_terminal_nodes()
748
689
 
749
690
  self._dag.check_graph()
750
691
 
@@ -1,23 +1,17 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: runnable
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: Add your description here
5
5
  Author-email: "Vammi, Vijay" <vijay.vammi@astrazeneca.com>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.10
8
- Requires-Dist: catalog
9
8
  Requires-Dist: click-plugins>=1.1.1
10
9
  Requires-Dist: click<=8.1.3
11
10
  Requires-Dist: dill>=0.3.9
12
- Requires-Dist: job-executor
13
- Requires-Dist: nodes
14
- Requires-Dist: pipeline-executor
15
11
  Requires-Dist: pydantic>=2.10.3
16
12
  Requires-Dist: python-dotenv>=1.0.1
17
13
  Requires-Dist: rich>=13.9.4
18
14
  Requires-Dist: ruamel-yaml>=0.18.6
19
- Requires-Dist: run-log-store
20
- Requires-Dist: secrets
21
15
  Requires-Dist: setuptools>=75.6.0
22
16
  Requires-Dist: stevedore>=5.4.0
23
17
  Requires-Dist: typer>=0.15.1
@@ -0,0 +1,58 @@
1
+ extensions/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ extensions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ extensions/catalog/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ extensions/catalog/file_system.py,sha256=VZEUx4X-GDSM8rJ_2kiCOyw1eek3roN0CiSB8wdUcOA,9307
5
+ extensions/catalog/pyproject.toml,sha256=lLNxY6v04c8I5QK_zKw_E6sJTArSJRA_V-79ktaA3Hk,279
6
+ extensions/job_executor/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ extensions/job_executor/__init__.py,sha256=HINaPjBWz04Ni7GqhuDLi0lS0-gYzq52HcOioYueYJE,5513
8
+ extensions/job_executor/k8s.py,sha256=BHICgJ_TT0JoGKPfX5GH_DpflKtIcSHp8I9i1ycSRZo,11516
9
+ extensions/job_executor/k8s_job_spec.yaml,sha256=7aFpxHdO_p6Hkc3YxusUOuAQTD1Myu0yTPX9DrhxbOg,1158
10
+ extensions/job_executor/local.py,sha256=8ebu4TKo6FnFiUflil6fmE7Pk8eSoe1fHNwX8YwI1BQ,1865
11
+ extensions/job_executor/local_container.py,sha256=7G2ARgoPwXbpNPgd5UDxWZqU2ABPIAk7bkNQkC4cNBQ,6585
12
+ extensions/job_executor/pyproject.toml,sha256=UIEgiCYHTXcRWSByNMFuKJFKgxTBpQqTqyUecIsb_Vc,286
13
+ extensions/nodes/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ extensions/nodes/nodes.py,sha256=ib68QE737ihGLIVp3V2wea13u7lmMZdRvK80bgUkRtA,34645
15
+ extensions/nodes/pyproject.toml,sha256=YTu-ETN3JNFSkMzzWeOwn4m-O2nbRH-PmiPBALDCUw4,278
16
+ extensions/pipeline_executor/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ extensions/pipeline_executor/__init__.py,sha256=YnKILiy-SxfnG3rYUoinjh1lfkuAF5QXpPePtn6VxBY,25174
18
+ extensions/pipeline_executor/argo.py,sha256=ClfuU_Of_2f5mvqVgY1QQwwJwXHB0LbzwNArG1x2Axc,44666
19
+ extensions/pipeline_executor/argo_specification.yaml,sha256=wXQcm2gOQYqy-IOQIhucohS32ZrHKCfGA5zZ0RraPYc,1276
20
+ extensions/pipeline_executor/local.py,sha256=H8s6AdML_9_f-vdGG_6k0y9FbLqAqvA1S_7xMNyARzY,1946
21
+ extensions/pipeline_executor/local_container.py,sha256=hqxLkxBOzk8P_PYnnZw5Ve9K0ztoyedv4kUVLkz8vVY,13967
22
+ extensions/pipeline_executor/mocked.py,sha256=SuObJ6Myt7p8duW8sylIp1cYIAnFutsJW1avWaOUY3c,5798
23
+ extensions/pipeline_executor/pyproject.toml,sha256=ykTX7srR10PBYb8LsIwEj8vIPPIEZQ5V_R7VYbZ-ido,291
24
+ extensions/pipeline_executor/retry.py,sha256=KGenhWrLLmOQgzMvqloXHDRJyoNs91t05rRW8aLW6FA,6969
25
+ extensions/run_log_store/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
+ extensions/run_log_store/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ extensions/run_log_store/chunked_fs.py,sha256=ElftNIwBmA2U2QAVGxruhcqepV312M2C9-GWVtiFaMM,3331
28
+ extensions/run_log_store/file_system.py,sha256=SANQ3aFjQeUaq8euvdpwju-8uci9UxdiEDupXtLYppQ,4303
29
+ extensions/run_log_store/generic_chunked.py,sha256=BX0j6S1Fwma3wuitHelUYm69FqXGToh10Zk2kamw6ZY,20253
30
+ extensions/run_log_store/pyproject.toml,sha256=YnmXsFvFG9uv_c0spLYBsNI_1sbktqxtHsOuClyvZ3g,288
31
+ extensions/run_log_store/db/implementation_FF.py,sha256=euTnh0xzNF0e_DyfHQ4W-kG1AwTr8u7OuO3_cZkR5bM,5237
32
+ extensions/run_log_store/db/integration_FF.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
+ extensions/secrets/README.md,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
+ extensions/secrets/dotenv.py,sha256=FbYYd_pVuJuVuIDIvXbzKuSSQ9GPq7xJXTDbJMTQbhM,1583
35
+ extensions/secrets/pyproject.toml,sha256=mLJNImNcBlbLKHh-0ugVWT9V83R4RibyyYDtBCSqVF4,282
36
+ runnable/__init__.py,sha256=KqpLDTD1CfdEj2aDyEkSn2KW-_83qyrRrrWLc5lZVM4,624
37
+ runnable/catalog.py,sha256=MiEmb-18liAKmgeMdDF41VVn0ZEAVLP8hR33oacQ1zs,4930
38
+ runnable/cli.py,sha256=01zmzOdynEmLI4vWDtSHQ6y1od_Jlc8G1RF69fi2L8g,8446
39
+ runnable/context.py,sha256=by5uepmuCP0dmM9BmsliXihSes5QEFejwAsmekcqylE,1388
40
+ runnable/datastore.py,sha256=9y5enzn6AXLHLdwvgkdjGPrBkVlrcjfbaAHsst-lJzg,32466
41
+ runnable/defaults.py,sha256=3o9IVGryyCE6PoQTOoaIaHHTbJGEzmdXMcwzOhwAYoI,3518
42
+ runnable/entrypoints.py,sha256=67gPBiIIS4Kd9g6LdoGCraRJPda8K1i7Lp7XcD2iY5k,18913
43
+ runnable/exceptions.py,sha256=LFbp0-Qxg2PAMLEVt7w2whhBxSG-5pzUEv5qN-Rc4_c,3003
44
+ runnable/executor.py,sha256=Rafu9EECrNq1LBkJmS6KYCekchP5ufrR04mHWG-JzqQ,15543
45
+ runnable/graph.py,sha256=jVjikRLR-so3b2ufmNKpEQ_Ny68qN4bcGDAdXBRKiCY,16574
46
+ runnable/names.py,sha256=vn92Kv9ANROYSZX6Z4z1v_WA3WiEdIYmG6KEStBFZug,8134
47
+ runnable/nodes.py,sha256=YU9u7r1ESzui1uVtJ1dgwdv1ozyJnF2k-MCFieT8CLI,17519
48
+ runnable/parameters.py,sha256=g_bJurLjuppFDiDpfFqy6BRF36o_EY0OC5APl7HJFok,5450
49
+ runnable/pickler.py,sha256=ydJ_eti_U1F4l-YacFp7BWm6g5vTn04UXye25S1HVok,2684
50
+ runnable/sdk.py,sha256=xN5F4XX8r5wCN131kgN2xG7MkNm0bSGJ3Ukw8prHYJ8,31444
51
+ runnable/secrets.py,sha256=PXcEJw-4WPzeWRLfsatcPPyr1zkqgHzdRWRcS9vvpvM,2354
52
+ runnable/tasks.py,sha256=JnIIYQf3YUidHXIN6hiUIfDnegc7_rJMNXuHW4WS9ig,29378
53
+ runnable/utils.py,sha256=wqyN7lMW56cBqyE59iDE6_i2HXPkvEUCQ-66UQnIwTA,19993
54
+ runnable-0.18.0.dist-info/METADATA,sha256=lBeCXTFGeVWpbxnOj2pZY6hTSNyB2Q5OWx5kVAF252s,9945
55
+ runnable-0.18.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
56
+ runnable-0.18.0.dist-info/entry_points.txt,sha256=I92DYldRrCb9HCsoum8GjC2UsQrWpuw2kawXTZpkIz4,1559
57
+ runnable-0.18.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
58
+ runnable-0.18.0.dist-info/RECORD,,
@@ -1,23 +0,0 @@
1
- runnable/__init__.py,sha256=KqpLDTD1CfdEj2aDyEkSn2KW-_83qyrRrrWLc5lZVM4,624
2
- runnable/catalog.py,sha256=MiEmb-18liAKmgeMdDF41VVn0ZEAVLP8hR33oacQ1zs,4930
3
- runnable/cli.py,sha256=01zmzOdynEmLI4vWDtSHQ6y1od_Jlc8G1RF69fi2L8g,8446
4
- runnable/context.py,sha256=by5uepmuCP0dmM9BmsliXihSes5QEFejwAsmekcqylE,1388
5
- runnable/datastore.py,sha256=9y5enzn6AXLHLdwvgkdjGPrBkVlrcjfbaAHsst-lJzg,32466
6
- runnable/defaults.py,sha256=3o9IVGryyCE6PoQTOoaIaHHTbJGEzmdXMcwzOhwAYoI,3518
7
- runnable/entrypoints.py,sha256=67gPBiIIS4Kd9g6LdoGCraRJPda8K1i7Lp7XcD2iY5k,18913
8
- runnable/exceptions.py,sha256=LFbp0-Qxg2PAMLEVt7w2whhBxSG-5pzUEv5qN-Rc4_c,3003
9
- runnable/executor.py,sha256=Rafu9EECrNq1LBkJmS6KYCekchP5ufrR04mHWG-JzqQ,15543
10
- runnable/graph.py,sha256=jVjikRLR-so3b2ufmNKpEQ_Ny68qN4bcGDAdXBRKiCY,16574
11
- runnable/names.py,sha256=vn92Kv9ANROYSZX6Z4z1v_WA3WiEdIYmG6KEStBFZug,8134
12
- runnable/nodes.py,sha256=YU9u7r1ESzui1uVtJ1dgwdv1ozyJnF2k-MCFieT8CLI,17519
13
- runnable/parameters.py,sha256=g_bJurLjuppFDiDpfFqy6BRF36o_EY0OC5APl7HJFok,5450
14
- runnable/pickler.py,sha256=ydJ_eti_U1F4l-YacFp7BWm6g5vTn04UXye25S1HVok,2684
15
- runnable/sdk.py,sha256=hwdk2dLmJsOTs2GnOlayw8WfliyeZFpA6Tcnp3tgblg,33370
16
- runnable/secrets.py,sha256=PXcEJw-4WPzeWRLfsatcPPyr1zkqgHzdRWRcS9vvpvM,2354
17
- runnable/tasks.py,sha256=JnIIYQf3YUidHXIN6hiUIfDnegc7_rJMNXuHW4WS9ig,29378
18
- runnable/utils.py,sha256=wqyN7lMW56cBqyE59iDE6_i2HXPkvEUCQ-66UQnIwTA,19993
19
- runnable-0.17.0.dist-info/METADATA,sha256=aPu6b9A_JP90oLpCFcnSJStDaizL5-sXbahPvjWU5Wo,10102
20
- runnable-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
- runnable-0.17.0.dist-info/entry_points.txt,sha256=I92DYldRrCb9HCsoum8GjC2UsQrWpuw2kawXTZpkIz4,1559
22
- runnable-0.17.0.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
23
- runnable-0.17.0.dist-info/RECORD,,