runnable 0.17.0__py3-none-any.whl → 0.18.0__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 (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,,