pipeline-toolkit 0.1.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.
- pipeline/__init__.py +24 -0
- pipeline/pipeline.py +317 -0
- pipeline/stack.py +132 -0
- pipeline/tap.py +43 -0
- pipeline_toolkit-0.1.0.dist-info/METADATA +477 -0
- pipeline_toolkit-0.1.0.dist-info/RECORD +9 -0
- pipeline_toolkit-0.1.0.dist-info/WHEEL +5 -0
- pipeline_toolkit-0.1.0.dist-info/licenses/LICENSE +21 -0
- pipeline_toolkit-0.1.0.dist-info/top_level.txt +1 -0
pipeline/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""A small functional pipeline toolkit for Python.
|
|
2
|
+
|
|
3
|
+
This package provides a simple asynchronous pipeline executor along with supporting container and utility components.
|
|
4
|
+
|
|
5
|
+
The main components are:
|
|
6
|
+
|
|
7
|
+
- :class:`Pipeline`: Execute callable steps sequentially in a worker thread.
|
|
8
|
+
- :class:`Stack`: A simple LIFO container for storing values and errors.
|
|
9
|
+
- :func:`tap`: Apply a side effect to a deep copy of a value while returning the original value unchanged.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from pipeline import Pipeline
|
|
13
|
+
>>> pipeline = Pipeline(
|
|
14
|
+
... (lambda value: value + 1,),
|
|
15
|
+
... (lambda value: value * 2,),
|
|
16
|
+
... )
|
|
17
|
+
>>> pipeline.run(5).wait()
|
|
18
|
+
>>> pipeline.results.get()
|
|
19
|
+
12
|
|
20
|
+
"""
|
|
21
|
+
from .pipeline import Pipeline
|
|
22
|
+
__version__ = "0.1.0"
|
|
23
|
+
__author__ = "Hoàng Long"
|
|
24
|
+
__all__ = ["__version__", "__author__", "Pipeline"]
|
pipeline/pipeline.py
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
from .stack import Stack
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
import threading
|
|
4
|
+
class Pipeline:
|
|
5
|
+
"""A simple asynchronous pipeline for executing callable steps sequentially.
|
|
6
|
+
|
|
7
|
+
Each step is represented by a tuple containing a callable and optional positional and keyword arguments:
|
|
8
|
+
|
|
9
|
+
(function,)
|
|
10
|
+
(function, args)
|
|
11
|
+
(function, kwargs)
|
|
12
|
+
(function, args, kwargs)
|
|
13
|
+
|
|
14
|
+
Where ``args`` must be a tuple or mapping, and ``kwargs`` must be a mapping.
|
|
15
|
+
The pipeline always passes the result of the previous step as the first positional argument to the next step.
|
|
16
|
+
The pipeline is snapshotted when execution starts, so modifications to the pipeline after ``run()`` has started do not affect the current execution.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
iterable: An iterable of pipeline steps. Defaults to an empty pipeline.
|
|
20
|
+
|
|
21
|
+
Attributes:
|
|
22
|
+
stop_event: Event used to request that the worker stop execution.
|
|
23
|
+
skip_event: Event used to request that the next step be skipped.
|
|
24
|
+
pipeline: List containing the configured pipeline steps.
|
|
25
|
+
thread: The worker thread for the current execution, or ``None`` when no execution is active.
|
|
26
|
+
step: The one-based index of the currently executing step, or ``0`` when the pipeline is not running.
|
|
27
|
+
results: Stack containing the initial value and results produced by executed steps.
|
|
28
|
+
errors: Stack containing exceptions raised by executed steps.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> def add(value, amount):
|
|
32
|
+
... return value + amount
|
|
33
|
+
|
|
34
|
+
>>> pipeline = Pipeline([
|
|
35
|
+
... (add, (5,)),
|
|
36
|
+
... (add, (10,)),
|
|
37
|
+
... ])
|
|
38
|
+
>>> pipeline.run(0)
|
|
39
|
+
>>> pipeline.wait()
|
|
40
|
+
>>> pipeline.results.get()
|
|
41
|
+
15
|
|
42
|
+
"""
|
|
43
|
+
def __init__(self, iterable=()):
|
|
44
|
+
"""Initialize a pipeline.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
iterable: An iterable containing pipeline steps.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
TypeError: If any item in ``iterable`` is not a valid step.
|
|
51
|
+
"""
|
|
52
|
+
iterable = tuple(iterable)
|
|
53
|
+
self.stop_event = threading.Event()
|
|
54
|
+
self.skip_event = threading.Event()
|
|
55
|
+
self.pipeline = []
|
|
56
|
+
self.thread = None
|
|
57
|
+
self.step = 0
|
|
58
|
+
self.results = Stack()
|
|
59
|
+
self.errors = Stack()
|
|
60
|
+
for step in iterable:
|
|
61
|
+
self._validate_step(step)
|
|
62
|
+
self.pipeline.append(step)
|
|
63
|
+
def _validate_step(self, step):
|
|
64
|
+
if not step or not isinstance(step, tuple):
|
|
65
|
+
raise TypeError("Invalid step format.")
|
|
66
|
+
if not callable(step[0]):
|
|
67
|
+
raise TypeError("Invalid step format.")
|
|
68
|
+
if len(step) > 3:
|
|
69
|
+
raise TypeError("Invalid step format.")
|
|
70
|
+
if len(step) >= 2 and not isinstance(step[1], (tuple, Mapping)):
|
|
71
|
+
raise TypeError("Invalid step format.")
|
|
72
|
+
if len(step) == 3 and not isinstance(step[1], tuple):
|
|
73
|
+
raise TypeError("Invalid step format.")
|
|
74
|
+
if len(step) == 3 and not isinstance(step[2], Mapping):
|
|
75
|
+
raise TypeError("Invalid step format.")
|
|
76
|
+
def _execute_step(self, step, default):
|
|
77
|
+
if len(step) == 1:
|
|
78
|
+
return step[0](default)
|
|
79
|
+
if isinstance(step[1], tuple):
|
|
80
|
+
return step[0](default, *step[1], **step[2]) if len(step) > 2 else step[0](default, *step[1])
|
|
81
|
+
return step[0](default, **step[1])
|
|
82
|
+
def run(self, default=None, delay=0, daemon=False, stop_on_error=True):
|
|
83
|
+
"""Run the pipeline asynchronously in a worker thread.
|
|
84
|
+
|
|
85
|
+
The supplied ``default`` value is stored as the initial result.
|
|
86
|
+
Each step receives the result of the preceding step as its first positional argument.
|
|
87
|
+
|
|
88
|
+
The pipeline is snapshotted when execution starts, so modifications to ``self.pipeline`` do not affect the current execution.
|
|
89
|
+
Execution stops when all steps have completed, ``stop()`` is called, or an exception is raised while ``stop_on_error`` is enabled.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
default: Initial value passed to the first step.
|
|
93
|
+
Defaults to ``None``.
|
|
94
|
+
delay: Delay in seconds between steps. Must be a non-negative integer or floating-point number.
|
|
95
|
+
Defaults to ``0``.
|
|
96
|
+
daemon: Whether the worker thread should be a daemon thread.
|
|
97
|
+
Defaults to ``False``.
|
|
98
|
+
stop_on_error: Whether execution should stop after the first exception.
|
|
99
|
+
Defaults to ``True``.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
This pipeline instance.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
TypeError: If ``delay`` is not an integer or floating-point number.
|
|
106
|
+
ValueError: If ``delay`` is negative.
|
|
107
|
+
RuntimeError: If the pipeline is already running.
|
|
108
|
+
"""
|
|
109
|
+
if not isinstance(delay, (int, float)):
|
|
110
|
+
raise TypeError("Delay is not int or float.")
|
|
111
|
+
if delay < 0:
|
|
112
|
+
raise ValueError("Delay cannot be negative.")
|
|
113
|
+
if self.running:
|
|
114
|
+
raise RuntimeError("Pipeline is already running.")
|
|
115
|
+
self.stop_event.clear()
|
|
116
|
+
self.skip_event.clear()
|
|
117
|
+
pipeline = tuple(self.pipeline)
|
|
118
|
+
self.step = 0
|
|
119
|
+
self.results.clear()
|
|
120
|
+
self.results.push(default)
|
|
121
|
+
self.errors.clear()
|
|
122
|
+
def worker():
|
|
123
|
+
for index, step in enumerate(pipeline):
|
|
124
|
+
if self.stop_event.is_set():
|
|
125
|
+
break
|
|
126
|
+
self.step = index + 1
|
|
127
|
+
if self.skip_event.is_set():
|
|
128
|
+
self.skip_event.clear()
|
|
129
|
+
continue
|
|
130
|
+
try:
|
|
131
|
+
self.results.push(self._execute_step(step, self.results.get()))
|
|
132
|
+
except Exception as error:
|
|
133
|
+
self.errors.push(error)
|
|
134
|
+
if stop_on_error:
|
|
135
|
+
break
|
|
136
|
+
if delay:
|
|
137
|
+
self.stop_event.wait(delay)
|
|
138
|
+
if not self.stop_event.is_set():
|
|
139
|
+
self.step = 0
|
|
140
|
+
self.thread = None
|
|
141
|
+
self.thread = threading.Thread(target=worker, daemon=daemon)
|
|
142
|
+
self.thread.start()
|
|
143
|
+
return self
|
|
144
|
+
def run_step(self, step, default):
|
|
145
|
+
"""Execute a single pipeline step synchronously.
|
|
146
|
+
|
|
147
|
+
This method does not create or modify the worker thread and does not store the result or exception in the pipeline's ``results`` or ``errors`` stacks.
|
|
148
|
+
Exceptions are allowed to propagate to the caller.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
step: One-based index of the pipeline step to execute.
|
|
152
|
+
default: Value passed to the selected step as its first argument.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The value returned by the selected step.
|
|
156
|
+
|
|
157
|
+
Raises:
|
|
158
|
+
TypeError: If ``step`` is not an integer.
|
|
159
|
+
ValueError: If ``step`` is less than ``1``.
|
|
160
|
+
IndexError: If ``step`` is outside the pipeline.
|
|
161
|
+
RuntimeError: If the pipeline is currently running.
|
|
162
|
+
"""
|
|
163
|
+
if not isinstance(step, int):
|
|
164
|
+
raise TypeError("Step is not int.")
|
|
165
|
+
if step < 1:
|
|
166
|
+
raise ValueError("Step < 1.")
|
|
167
|
+
if not 1 <= step <= len(self.pipeline):
|
|
168
|
+
raise IndexError("Step out of pipeline.")
|
|
169
|
+
if self.running:
|
|
170
|
+
raise RuntimeError("Pipeline is already running.")
|
|
171
|
+
step = self.pipeline[step - 1]
|
|
172
|
+
return self._execute_step(step, default)
|
|
173
|
+
def stop(self):
|
|
174
|
+
"""Request the running pipeline to stop and wait for termination.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The one-based index of the step at which execution was stopped, or ``0`` if the pipeline was not running.
|
|
178
|
+
"""
|
|
179
|
+
if self.thread is not None and self.running:
|
|
180
|
+
self.stop_event.set()
|
|
181
|
+
self.thread.join()
|
|
182
|
+
step = self.step
|
|
183
|
+
self.step = 0
|
|
184
|
+
return step
|
|
185
|
+
return 0
|
|
186
|
+
def skip(self):
|
|
187
|
+
"""Request the currently running pipeline to skip its next step.
|
|
188
|
+
|
|
189
|
+
If the worker has not yet checked the skip event, the next step that reaches the worker's skip check will be skipped.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
This pipeline instance.
|
|
193
|
+
"""
|
|
194
|
+
if self.thread is not None and self.running:
|
|
195
|
+
self.skip_event.set()
|
|
196
|
+
return self
|
|
197
|
+
def wait(self):
|
|
198
|
+
"""Wait until the currently running pipeline finishes.
|
|
199
|
+
|
|
200
|
+
This method blocks only when the pipeline is running.
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
This pipeline instance.
|
|
204
|
+
"""
|
|
205
|
+
if self.thread is not None and self.running:
|
|
206
|
+
self.thread.join()
|
|
207
|
+
return self
|
|
208
|
+
def rerun(self, *args, **kwargs):
|
|
209
|
+
"""Stop the current execution and start the pipeline again.
|
|
210
|
+
|
|
211
|
+
All arguments are passed directly to :meth:`run`.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
This pipeline instance.
|
|
215
|
+
"""
|
|
216
|
+
self.stop()
|
|
217
|
+
self.run(*args, **kwargs)
|
|
218
|
+
return self
|
|
219
|
+
@property
|
|
220
|
+
def running(self):
|
|
221
|
+
"""Whether the pipeline currently has a running worker thread."""
|
|
222
|
+
return self.thread is not None and self.thread.is_alive()
|
|
223
|
+
def add(self, step):
|
|
224
|
+
"""Append a validated step to the pipeline.
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
step: The step to append.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
TypeError: If ``step`` has an invalid format.
|
|
231
|
+
"""
|
|
232
|
+
self._validate_step(step)
|
|
233
|
+
self.pipeline.append(step)
|
|
234
|
+
def insert(self, index, step):
|
|
235
|
+
"""Insert a validated step at the specified index.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
index: One-based position at which to insert the step.
|
|
239
|
+
step: The step to insert.
|
|
240
|
+
|
|
241
|
+
Raises:
|
|
242
|
+
TypeError: If ``index`` is not an integer or ``step`` is invalid.
|
|
243
|
+
IndexError: If ``index`` is outside the pipeline.
|
|
244
|
+
"""
|
|
245
|
+
if not isinstance(index, int):
|
|
246
|
+
raise TypeError(f"{index} is not int.")
|
|
247
|
+
if not 1 < index <= len(self.pipeline) + 1:
|
|
248
|
+
raise IndexError("Index out of range.")
|
|
249
|
+
self._validate_step(step)
|
|
250
|
+
self.pipeline.insert(index - 1, step)
|
|
251
|
+
def pop(self, index):
|
|
252
|
+
"""Remove and return the step at the specified index.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
index: One-based index of the step to remove.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
The removed pipeline step.
|
|
259
|
+
|
|
260
|
+
Raises:
|
|
261
|
+
TypeError: If ``index`` is not an integer.
|
|
262
|
+
IndexError: If ``index`` is outside the pipeline.
|
|
263
|
+
"""
|
|
264
|
+
if not isinstance(index, int):
|
|
265
|
+
raise TypeError(f"{index} is not int.")
|
|
266
|
+
if not 1 <= index <= len(self.pipeline):
|
|
267
|
+
raise IndexError("Index out of range.")
|
|
268
|
+
return self.pipeline.pop(index - 1)
|
|
269
|
+
def clear(self):
|
|
270
|
+
"""Remove all steps from the pipeline."""
|
|
271
|
+
self.pipeline.clear()
|
|
272
|
+
def _format_step(self, step):
|
|
273
|
+
func = step[0]
|
|
274
|
+
if len(step) >= 2:
|
|
275
|
+
args = step[1]
|
|
276
|
+
else:
|
|
277
|
+
args = ()
|
|
278
|
+
if isinstance(args, tuple):
|
|
279
|
+
parts = [repr(arg) for arg in args]
|
|
280
|
+
if len(step) == 3:
|
|
281
|
+
parts.extend(f"{key}={value!r}" for key, value in step[2].items())
|
|
282
|
+
else:
|
|
283
|
+
parts = [f"{key}={value!r}" for key, value in args.items()]
|
|
284
|
+
return f"{func.__name__}({', '.join(parts)})"
|
|
285
|
+
def __str__(self):
|
|
286
|
+
"""Return a human-readable representation of the pipeline."""
|
|
287
|
+
return " | ".join(self._format_step(step) for step in self.pipeline)
|
|
288
|
+
def __repr__(self):
|
|
289
|
+
"""Return the developer-oriented representation of the pipeline."""
|
|
290
|
+
return f"{type(self).__name__}(total_steps={len(self.pipeline)}, current_step={self.step}, running={self.running})"
|
|
291
|
+
def __bool__(self):
|
|
292
|
+
"""Return whether the pipeline is currently running."""
|
|
293
|
+
return self.running
|
|
294
|
+
def __call__(self, *args, **kwargs):
|
|
295
|
+
"""Run the pipeline.
|
|
296
|
+
|
|
297
|
+
Arguments are passed directly to :meth:`run`.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
This pipeline instance.
|
|
301
|
+
"""
|
|
302
|
+
return self.run(*args, **kwargs)
|
|
303
|
+
def __contains__(self, item):
|
|
304
|
+
"""Return whether a callable exists as a pipeline step.
|
|
305
|
+
|
|
306
|
+
Identity comparison is used, so the callable must be the exact same object as the callable stored in the step.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
item: Callable to search for.
|
|
310
|
+
|
|
311
|
+
Returns:
|
|
312
|
+
``True`` if the callable is present, otherwise ``False``.
|
|
313
|
+
"""
|
|
314
|
+
return any(step[0] is item for step in self.pipeline)
|
|
315
|
+
def __len__(self):
|
|
316
|
+
"""Return the number of configured pipeline steps."""
|
|
317
|
+
return len(self.pipeline)
|
pipeline/stack.py
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
class StackOverflowError(Exception):
|
|
2
|
+
"""Raised when attempting to push an item onto a full stack."""
|
|
3
|
+
pass
|
|
4
|
+
class StackUnderflowError(Exception):
|
|
5
|
+
"""Raised when attempting to access or remove an item from an empty stack."""
|
|
6
|
+
pass
|
|
7
|
+
class Stack:
|
|
8
|
+
"""A simple LIFO stack container with optional maximum capacity.
|
|
9
|
+
|
|
10
|
+
The stack follows the Last-In, First-Out (LIFO) principle. The most recently pushed item is returned by :meth:`get` or removed by :meth:`pop`.
|
|
11
|
+
|
|
12
|
+
A maximum size can optionally be specified.
|
|
13
|
+
A ``maxsize`` of ``0`` means that the stack has no size limit.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
maxsize: Maximum number of items allowed in the stack.
|
|
17
|
+
Defaults to ``0``, meaning unlimited capacity.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
TypeError: If ``maxsize`` is not an integer.
|
|
21
|
+
ValueError: If ``maxsize`` is negative.
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
>>> stack = Stack(3)
|
|
25
|
+
>>> stack.push("first")
|
|
26
|
+
>>> stack.push("second")
|
|
27
|
+
>>> stack.get()
|
|
28
|
+
'second'
|
|
29
|
+
>>> stack.pop()
|
|
30
|
+
'second'
|
|
31
|
+
>>> len(stack)
|
|
32
|
+
1
|
|
33
|
+
"""
|
|
34
|
+
def __init__(self, maxsize=0):
|
|
35
|
+
"""Initialize an empty stack.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
maxsize: Maximum number of items allowed in the stack.
|
|
39
|
+
``0`` means unlimited capacity.
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
TypeError: If ``maxsize`` is not an integer.
|
|
43
|
+
ValueError: If ``maxsize`` is negative.
|
|
44
|
+
"""
|
|
45
|
+
if not isinstance(maxsize, int):
|
|
46
|
+
raise TypeError("maxsize is not int.")
|
|
47
|
+
if maxsize < 0:
|
|
48
|
+
raise ValueError("maxsize < 0.")
|
|
49
|
+
self._maxsize = maxsize
|
|
50
|
+
self._stack = []
|
|
51
|
+
def push(self, value):
|
|
52
|
+
"""Push an item onto the top of the stack.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
value: Value to push onto the stack.
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
StackOverflowError: If the stack has reached its maximum capacity.
|
|
59
|
+
"""
|
|
60
|
+
if self._maxsize and len(self._stack) >= self._maxsize:
|
|
61
|
+
raise StackOverflowError("stack overflow")
|
|
62
|
+
self._stack.append(value)
|
|
63
|
+
def get(self):
|
|
64
|
+
"""Return the item at the top of the stack without removing it.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The most recently pushed item.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
StackUnderflowError: If the stack is empty.
|
|
71
|
+
"""
|
|
72
|
+
if not self._stack:
|
|
73
|
+
raise StackUnderflowError("stack underflow")
|
|
74
|
+
return self._stack[-1]
|
|
75
|
+
def pop(self):
|
|
76
|
+
"""Remove and return the item at the top of the stack.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
The most recently pushed item.
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
StackUnderflowError: If the stack is empty.
|
|
83
|
+
"""
|
|
84
|
+
if not self._stack:
|
|
85
|
+
raise StackUnderflowError("stack underflow")
|
|
86
|
+
return self._stack.pop()
|
|
87
|
+
def clear(self):
|
|
88
|
+
"""Remove all items from the stack."""
|
|
89
|
+
self._stack.clear()
|
|
90
|
+
def empty(self):
|
|
91
|
+
"""Return whether the stack is empty.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
``True`` if the stack contains no items, otherwise ``False``.
|
|
95
|
+
"""
|
|
96
|
+
return len(self._stack) == 0
|
|
97
|
+
def full(self):
|
|
98
|
+
"""Return whether the stack has reached its maximum capacity.
|
|
99
|
+
|
|
100
|
+
An unlimited stack is never considered full.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
``True`` if the stack is full, otherwise ``False``.
|
|
104
|
+
"""
|
|
105
|
+
return bool(self._maxsize and len(self._stack) >= self._maxsize)
|
|
106
|
+
def __len__(self):
|
|
107
|
+
"""Return the number of items currently in the stack."""
|
|
108
|
+
return len(self._stack)
|
|
109
|
+
def __bool__(self):
|
|
110
|
+
"""Return whether the stack contains at least one item."""
|
|
111
|
+
return bool(self._stack)
|
|
112
|
+
def __contains__(self, value):
|
|
113
|
+
"""Return whether a value exists in the stack.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
value: Value to search for.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
``True`` if ``value`` is present, otherwise ``False``.
|
|
120
|
+
"""
|
|
121
|
+
return value in self._stack
|
|
122
|
+
def __iter__(self):
|
|
123
|
+
"""Iterate over the items in the stack from bottom to top.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
An iterator over the stack's items.
|
|
127
|
+
"""
|
|
128
|
+
for item in self._stack:
|
|
129
|
+
yield item
|
|
130
|
+
def __repr__(self):
|
|
131
|
+
"""Return the developer-oriented representation of the stack."""
|
|
132
|
+
return f"{type(self).__name__}({self._stack!r}, maxsize={self._maxsize})"
|
pipeline/tap.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import copy
|
|
2
|
+
def tap(value, function, *args, **kwargs):
|
|
3
|
+
"""Apply a function to a deep copy of a value and return the original value.
|
|
4
|
+
|
|
5
|
+
The function is called for its side effect.
|
|
6
|
+
A deep copy of ``value`` is passed to the function, so modifications made by the function do not affect the original value.
|
|
7
|
+
|
|
8
|
+
The return value of ``function`` is ignored.
|
|
9
|
+
This makes ``tap`` useful for inspecting, logging, debugging, or otherwise processing a value without interrupting a chain of operations.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
value: Value to pass to ``function`` and return unchanged.
|
|
13
|
+
function: Callable to invoke with a deep copy of ``value``.
|
|
14
|
+
*args: Additional positional arguments passed to ``function``.
|
|
15
|
+
**kwargs: Additional keyword arguments passed to ``function``.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The original ``value`` object.
|
|
19
|
+
|
|
20
|
+
Raises:
|
|
21
|
+
TypeError: If ``function`` is not callable.
|
|
22
|
+
Any exception raised by ``copy.deepcopy`` or ``function`` is propagated to the caller.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> data = {"items": [1, 2, 3]}
|
|
26
|
+
>>> tap(data, print)
|
|
27
|
+
{'items': [1, 2, 3]}
|
|
28
|
+
|
|
29
|
+
A function can modify its argument without modifying the original:
|
|
30
|
+
|
|
31
|
+
>>> def inspect(data):
|
|
32
|
+
... data["items"].append(4)
|
|
33
|
+
>>> original = {"items": [1, 2, 3]}
|
|
34
|
+
>>> result = tap(original, inspect)
|
|
35
|
+
>>> result is original
|
|
36
|
+
True
|
|
37
|
+
>>> original
|
|
38
|
+
{'items': [1, 2, 3]}
|
|
39
|
+
"""
|
|
40
|
+
if not callable(function):
|
|
41
|
+
raise TypeError("function is not callable.")
|
|
42
|
+
function(copy.deepcopy(value), *args, **kwargs)
|
|
43
|
+
return value
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pipeline-toolkit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small functional pipeline toolkit for Python.
|
|
5
|
+
Author-email: Hoàng Long <hoanglongcodes@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Hoang-Long2012/pipeline-toolkit
|
|
8
|
+
Project-URL: Repository, https://github.com/Hoang-Long2012/pipeline-toolkit
|
|
9
|
+
Project-URL: Issues, https://github.com/Hoang-Long2012/pipeline-toolkit/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Hoang-Long2012/pipeline-toolkit/blob/main/CHANGELOG.md
|
|
11
|
+
Keywords: pipeline,functional programming,functional pipeline,workflow,async,asynchronous,threading,utilities
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Requires-Python: >=3.8
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
License-File: LICENSE
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
# Pipeline Toolkit
|
|
32
|
+
|
|
33
|
+
A small functional pipeline toolkit for Python.
|
|
34
|
+
|
|
35
|
+
`pipeline-toolkit` provides a simple way to build sequential pipelines from ordinary Python callables. Each step receives the result of the previous step, while the pipeline runs asynchronously in a worker thread.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- Sequential functional pipeline execution
|
|
40
|
+
- Asynchronous execution using a worker thread
|
|
41
|
+
- Positional and keyword arguments for pipeline steps
|
|
42
|
+
- Stop, skip, wait, and rerun execution
|
|
43
|
+
- Manual synchronous step execution
|
|
44
|
+
- Result and error history using a stack
|
|
45
|
+
- Pipeline modification with `add()`, `insert()`, `pop()`, and `clear()`
|
|
46
|
+
- Small utility modules for functional workflows
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
Install from PyPI:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install pipeline-toolkit
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Or install directly from GitHub:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install git+https://github.com/Hoang-Long2012/pipeline-toolkit.git
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
A pipeline is created from an iterable of steps. Each step is a tuple whose first item is a callable.
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from pipeline import Pipeline
|
|
68
|
+
|
|
69
|
+
def add(value, amount):
|
|
70
|
+
return value + amount
|
|
71
|
+
|
|
72
|
+
def multiply(value, factor):
|
|
73
|
+
return value * factor
|
|
74
|
+
|
|
75
|
+
pipeline = Pipeline([
|
|
76
|
+
(add, (5,)),
|
|
77
|
+
(multiply, (2,)),
|
|
78
|
+
])
|
|
79
|
+
|
|
80
|
+
pipeline.run(10).wait()
|
|
81
|
+
|
|
82
|
+
print(pipeline.results.get())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The execution flow is:
|
|
86
|
+
|
|
87
|
+
```text
|
|
88
|
+
10
|
|
89
|
+
↓
|
|
90
|
+
add(10, 5)
|
|
91
|
+
↓
|
|
92
|
+
15
|
|
93
|
+
↓
|
|
94
|
+
multiply(15, 2)
|
|
95
|
+
↓
|
|
96
|
+
30
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The final result is `30`.
|
|
100
|
+
|
|
101
|
+
## Pipeline Steps
|
|
102
|
+
|
|
103
|
+
Each step can use one of four supported forms.
|
|
104
|
+
|
|
105
|
+
### Callable only
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
(function,)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The callable receives the previous result:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
pipeline = Pipeline([
|
|
115
|
+
(str.upper,),
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
pipeline.run("hello").wait()
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Positional arguments
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
(function, args)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
where `args` is a tuple:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
pipeline = Pipeline([
|
|
131
|
+
(add, (5,)),
|
|
132
|
+
(multiply, (2,)),
|
|
133
|
+
])
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A step such as:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
(add, (5,))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
is executed as:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
add(previous_result, 5)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Keyword arguments
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
(function, kwargs)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
where `kwargs` is a mapping:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
pipeline = Pipeline([
|
|
158
|
+
(pow, {"exp": 2}),
|
|
159
|
+
])
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
The step is executed as:
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
pow(previous_result, exp=2)
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Positional and keyword arguments
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
(function, args, kwargs)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
For example:
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
pipeline = Pipeline([
|
|
178
|
+
(my_function, (1, 2), {"option": True}),
|
|
179
|
+
])
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The callable receives the previous result followed by the supplied positional and keyword arguments.
|
|
183
|
+
|
|
184
|
+
## Execution
|
|
185
|
+
|
|
186
|
+
### `run()`
|
|
187
|
+
|
|
188
|
+
Start the pipeline asynchronously.
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
pipeline.run(default=None, delay=0, daemon=False, stop_on_error=True)
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The `default` value becomes the initial result and is passed to the first step.
|
|
195
|
+
|
|
196
|
+
`delay` specifies the delay between steps.
|
|
197
|
+
|
|
198
|
+
`daemon` controls whether the worker thread is a daemon thread.
|
|
199
|
+
|
|
200
|
+
`stop_on_error` controls whether execution stops after the first exception.
|
|
201
|
+
|
|
202
|
+
`run()` returns the pipeline instance, allowing calls such as:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
pipeline.run(10).wait()
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
The pipeline is snapshotted when execution starts. Changes made to `pipeline.pipeline` after `run()` begins do not affect the current execution.
|
|
209
|
+
|
|
210
|
+
### `wait()`
|
|
211
|
+
|
|
212
|
+
Wait for the current execution to finish.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
pipeline.wait()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
It returns the pipeline instance.
|
|
219
|
+
|
|
220
|
+
### `stop()`
|
|
221
|
+
|
|
222
|
+
Request the running pipeline to stop and wait for its worker thread to terminate.
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
step = pipeline.stop()
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The return value is the current one-based step index when execution is stopped, or `0` if the pipeline was not running.
|
|
229
|
+
|
|
230
|
+
### `skip()`
|
|
231
|
+
|
|
232
|
+
Request the worker to skip the next step that reaches its skip check.
|
|
233
|
+
|
|
234
|
+
```python
|
|
235
|
+
pipeline.skip()
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The method returns the pipeline instance.
|
|
239
|
+
|
|
240
|
+
### `rerun()`
|
|
241
|
+
|
|
242
|
+
Stop the current execution and start the pipeline again.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
pipeline.rerun(10)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Arguments are passed directly to `run()`.
|
|
249
|
+
|
|
250
|
+
## Manual Step Execution
|
|
251
|
+
|
|
252
|
+
`run_step()` executes one configured step synchronously.
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
result = pipeline.run_step(2, 10)
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
Unlike `run()`, this method:
|
|
259
|
+
|
|
260
|
+
- does not create a worker thread
|
|
261
|
+
- does not modify the worker thread or pipeline execution state
|
|
262
|
+
- does not store the result in `results`
|
|
263
|
+
- does not store exceptions in `errors`
|
|
264
|
+
- allows exceptions to propagate to the caller
|
|
265
|
+
|
|
266
|
+
This makes it useful when a single pipeline step needs to be executed manually.
|
|
267
|
+
|
|
268
|
+
## Results and Errors
|
|
269
|
+
|
|
270
|
+
The pipeline provides two `Stack` instances:
|
|
271
|
+
|
|
272
|
+
```python
|
|
273
|
+
pipeline.results
|
|
274
|
+
pipeline.errors
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
`results` contains the initial value and the results produced by executed steps.
|
|
278
|
+
|
|
279
|
+
For example:
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
pipeline.run(10).wait()
|
|
283
|
+
|
|
284
|
+
print(pipeline.results.get())
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
`errors` contains exceptions raised by pipeline steps.
|
|
288
|
+
|
|
289
|
+
When `stop_on_error=True`, execution stops after the first exception.
|
|
290
|
+
|
|
291
|
+
When `stop_on_error=False`, the exception is stored in `errors` and execution continues with the previous result.
|
|
292
|
+
|
|
293
|
+
## Managing Pipeline Steps
|
|
294
|
+
|
|
295
|
+
Pipeline steps can be modified before or between executions.
|
|
296
|
+
|
|
297
|
+
### `add()`
|
|
298
|
+
|
|
299
|
+
Append a step:
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
pipeline.add((str.upper,))
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### `insert()`
|
|
306
|
+
|
|
307
|
+
Insert a step at a one-based position:
|
|
308
|
+
|
|
309
|
+
```python
|
|
310
|
+
pipeline.insert(2, (str.strip,))
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### `pop()`
|
|
314
|
+
|
|
315
|
+
Remove and return a step:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
step = pipeline.pop(1)
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Pipeline indexes are one-based.
|
|
322
|
+
|
|
323
|
+
### `clear()`
|
|
324
|
+
|
|
325
|
+
Remove all configured steps:
|
|
326
|
+
|
|
327
|
+
```python
|
|
328
|
+
pipeline.clear()
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
## Pipeline State
|
|
332
|
+
|
|
333
|
+
The `running` property indicates whether the worker thread is currently running:
|
|
334
|
+
|
|
335
|
+
```python
|
|
336
|
+
if pipeline.running:
|
|
337
|
+
print("Pipeline is running")
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The `step` attribute contains the one-based index of the currently executing step. It is `0` when the pipeline is not running.
|
|
341
|
+
|
|
342
|
+
A `Pipeline` instance can also be used as a boolean:
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
if pipeline:
|
|
346
|
+
print("Pipeline is running")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Calling a pipeline instance is equivalent to calling `run()`:
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
pipeline(10)
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
is equivalent to:
|
|
356
|
+
|
|
357
|
+
```python
|
|
358
|
+
pipeline.run(10)
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
The length of a pipeline is the number of configured steps:
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
len(pipeline)
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
A callable can be checked with the `in` operator:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
if add in pipeline:
|
|
371
|
+
print("add is part of the pipeline")
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
Callable membership uses identity comparison.
|
|
375
|
+
|
|
376
|
+
## Utilities
|
|
377
|
+
|
|
378
|
+
### `Stack`
|
|
379
|
+
|
|
380
|
+
`Stack` is a simple LIFO stack container with optional capacity limits.
|
|
381
|
+
|
|
382
|
+
It supports common stack operations such as pushing, retrieving, peeking, and removing items, with dedicated exceptions for overflow and underflow conditions.
|
|
383
|
+
|
|
384
|
+
Import it directly from its submodule:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from pipeline.stack import Stack
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
`Stack` is also used internally by `Pipeline` for storing results and errors.
|
|
391
|
+
|
|
392
|
+
For example:
|
|
393
|
+
|
|
394
|
+
```python
|
|
395
|
+
from pipeline.stack import Stack
|
|
396
|
+
|
|
397
|
+
stack = Stack()
|
|
398
|
+
|
|
399
|
+
stack.push("first")
|
|
400
|
+
stack.push("second")
|
|
401
|
+
|
|
402
|
+
print(stack.get())
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
For detailed stack operations and behavior, see the `pipeline.stack` module.
|
|
406
|
+
|
|
407
|
+
### `tap`
|
|
408
|
+
|
|
409
|
+
`tap` is a small functional utility for performing a side effect while keeping the pipeline value available for subsequent processing.
|
|
410
|
+
|
|
411
|
+
`tap` performs a side effect on a deep copy of the current value and returns the original value unchanged.
|
|
412
|
+
|
|
413
|
+
Import it directly from its submodule:
|
|
414
|
+
|
|
415
|
+
```python
|
|
416
|
+
from pipeline.tap import tap
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
For example:
|
|
420
|
+
|
|
421
|
+
```python
|
|
422
|
+
from pipeline import Pipeline
|
|
423
|
+
from pipeline.tap import tap
|
|
424
|
+
|
|
425
|
+
def add(value, amount):
|
|
426
|
+
return value + amount
|
|
427
|
+
|
|
428
|
+
pipeline = Pipeline([
|
|
429
|
+
(add, (5,)),
|
|
430
|
+
(tap, (print,)),
|
|
431
|
+
(add, (10,)),
|
|
432
|
+
])
|
|
433
|
+
|
|
434
|
+
pipeline.run(10).wait()
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
Utilities are provided as separate submodules rather than being exported from the top-level `pipeline` package.
|
|
438
|
+
|
|
439
|
+
## API Overview
|
|
440
|
+
|
|
441
|
+
### `Pipeline`
|
|
442
|
+
|
|
443
|
+
| Member | Description |
|
|
444
|
+
| ------------ | ------------------------------------- |
|
|
445
|
+
| `run()` | Start asynchronous pipeline execution |
|
|
446
|
+
| `run_step()` | Execute one step synchronously |
|
|
447
|
+
| `stop()` | Stop the current execution |
|
|
448
|
+
| `skip()` | Request the next step to be skipped |
|
|
449
|
+
| `wait()` | Wait for the current execution |
|
|
450
|
+
| `rerun()` | Restart the pipeline |
|
|
451
|
+
| `add()` | Append a step |
|
|
452
|
+
| `insert()` | Insert a step |
|
|
453
|
+
| `pop()` | Remove and return a step |
|
|
454
|
+
| `clear()` | Remove all steps |
|
|
455
|
+
| `running` | Whether the worker is running |
|
|
456
|
+
| `step` | Current one-based step index |
|
|
457
|
+
| `results` | Stack of initial value and results |
|
|
458
|
+
| `errors` | Stack of raised exceptions |
|
|
459
|
+
|
|
460
|
+
## Requirements
|
|
461
|
+
|
|
462
|
+
* Python 3.8 or newer
|
|
463
|
+
|
|
464
|
+
## Changelog
|
|
465
|
+
|
|
466
|
+
See changelog from: [CHANGELOG.md](https://github.com/Hoang-Long2012/pipeline-toolkit/blob/main/CHANGELOG.md)
|
|
467
|
+
|
|
468
|
+
## License
|
|
469
|
+
|
|
470
|
+
This project is licensed under the MIT License. See [LICENSE](https://github.com/Hoang-Long2012/pipeline-toolkit/blob/main/LICENSE) for details.
|
|
471
|
+
|
|
472
|
+
## Contribution
|
|
473
|
+
|
|
474
|
+
If you'd like to contribute, feel free to submit a pull request.
|
|
475
|
+
If you'd like to report a bug or request a feature, please open an issue.
|
|
476
|
+
|
|
477
|
+
Copyright (C) 2026 Hoàng Long
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pipeline/__init__.py,sha256=OqDA6wIp3_I4rPtf9fnN9YVUWHd_AOTduG02-F3tRbk,827
|
|
2
|
+
pipeline/pipeline.py,sha256=HGkfBTn-KU28k--FAVoQ13fKQSFhXYztplyG-3chPHM,10714
|
|
3
|
+
pipeline/stack.py,sha256=1Ng_Tb4B2vQaU61OJMIiC2Kfl_qqIn4c-A6g_ERSzxA,3742
|
|
4
|
+
pipeline/tap.py,sha256=rmcQWGk6ulSieJTvN_KabfEvaSmN4NM7n8X_hDbeYxE,1492
|
|
5
|
+
pipeline_toolkit-0.1.0.dist-info/licenses/LICENSE,sha256=c1qSpYLFRd3y_-AO7xnys7ntD8rcvVy7_E5Ue5-QIBo,1068
|
|
6
|
+
pipeline_toolkit-0.1.0.dist-info/METADATA,sha256=XAmz4Jucy3Yhx6fagBvJ6YWVB_929qUME1u0ImY9Wyo,10704
|
|
7
|
+
pipeline_toolkit-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
pipeline_toolkit-0.1.0.dist-info/top_level.txt,sha256=Qdc1eKrvhKK_o9CPbdooOdDt7g3ZSXZDrNXHmUGl94Q,9
|
|
9
|
+
pipeline_toolkit-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hoàng Long
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pipeline
|