summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYuval Adam <_@yuv.al>2025-03-21 21:48:14 +0100
committerYuval Adam <_@yuv.al>2025-03-21 21:48:14 +0100
commitd8846814f319fb792fbb1d7f48c41f911fce609c (patch)
treed11ee35b65e9abf0b6410278c7bbc7bc36c1d6e1
parent1f1d235a69c84e06eacc69b9a6041b79ddc51c2d (diff)
Add Pipeline
-rw-r--r--rivulet/pipeline.py63
-rw-r--r--rivulet/tests/test_pipeline.py98
2 files changed, 161 insertions, 0 deletions
diff --git a/rivulet/pipeline.py b/rivulet/pipeline.py
new file mode 100644
index 0000000..226a645
--- /dev/null
+++ b/rivulet/pipeline.py
@@ -0,0 +1,63 @@
+from typing import AsyncGenerator, TypeVar, Callable, List, Generic, Any
+
+T = TypeVar('T')
+U = TypeVar('U')
+V = TypeVar('V')
+
+class Pipeline:
+ """
+ A flexible pipeline that chains multiple async generators.
+
+ Each step is a function that takes an async generator and returns a new async generator.
+ """
+
+ def __init__(self, source: AsyncGenerator[Any, None]):
+ """Initialize the pipeline with a source async generator."""
+ self.source = source
+ self.steps: List[Callable[[AsyncGenerator[Any, None]], AsyncGenerator[Any, None]]] = []
+
+ def add_step(self, transform: Callable[[AsyncGenerator[Any, None]], AsyncGenerator[Any, None]]):
+ """
+ Add a transformation step to the pipeline.
+
+ Args:
+ transform: A function that takes an async generator and returns an async generator
+
+ Returns:
+ The pipeline instance for method chaining
+ """
+ self.steps.append(transform)
+ return self
+
+ def __aiter__(self):
+ """Make the pipeline itself an async generator."""
+ return self._execute()
+
+ async def _execute(self):
+ """
+ Execute the pipeline by chaining all generators together.
+
+ Yields:
+ Values from the final step of the pipeline
+ """
+ current_gen = self.source
+
+ # Apply each transformation step
+ for step in self.steps:
+ current_gen = step(current_gen)
+
+ # Yield all items from the final generator
+ async for item in current_gen:
+ yield item
+
+ async def collect(self):
+ """
+ Collect all results from the pipeline into a list.
+
+ Returns:
+ A list containing all output items from the pipeline
+ """
+ results = []
+ async for item in self:
+ results.append(item)
+ return results \ No newline at end of file
diff --git a/rivulet/tests/test_pipeline.py b/rivulet/tests/test_pipeline.py
new file mode 100644
index 0000000..33462bd
--- /dev/null
+++ b/rivulet/tests/test_pipeline.py
@@ -0,0 +1,98 @@
+import pytest
+
+from ..pipeline import Pipeline
+
+
+@pytest.mark.asyncio
+async def test_pipeline_basic_transformations():
+ """Test pipeline with simple transform functions that process values"""
+
+ async def source():
+ for i in range(3):
+ yield i
+
+ async def double(gen):
+ async for value in gen:
+ yield value * 2
+
+ async def as_string(gen):
+ async for value in gen:
+ yield str(value)
+
+ # Create and execute pipeline
+ pipeline = Pipeline(source())
+ pipeline.add_step(double)
+ pipeline.add_step(as_string)
+
+ # Collect results
+ results = await pipeline.collect()
+
+ # Verify results
+ assert results == ["0", "2", "4"]
+
+
+@pytest.mark.asyncio
+async def test_pipeline_with_expanding_transformations():
+ """Test pipeline with a transformation that outputs multiple values per input"""
+
+ async def source():
+ yield 1
+ yield 2
+
+ async def duplicate(gen):
+ async for value in gen:
+ yield value
+ yield value
+
+ async def multiply_by_ten(gen):
+ async for value in gen:
+ yield value * 10
+
+ # Test pipeline with expanding transformation first
+ pipeline1 = Pipeline(source())
+ pipeline1.add_step(duplicate)
+ pipeline1.add_step(multiply_by_ten)
+
+ results1 = await pipeline1.collect()
+ assert results1 == [10, 10, 20, 20]
+
+ # Test pipeline with expanding transformation last
+ pipeline2 = Pipeline(source())
+ pipeline2.add_step(multiply_by_ten)
+ pipeline2.add_step(duplicate)
+
+ results2 = await pipeline2.collect()
+ assert results2 == [10, 10, 20, 20]
+
+
+@pytest.mark.asyncio
+async def test_empty_pipeline():
+ """Test pipeline with no transformations"""
+
+ async def source():
+ yield "test"
+
+ pipeline = Pipeline(source())
+ results = await pipeline.collect()
+
+ assert results == ["test"]
+
+
+@pytest.mark.asyncio
+async def test_pipeline_with_filtering():
+ """Test pipeline with a transformation that filters values"""
+
+ async def source():
+ for i in range(5):
+ yield i
+
+ async def even_only(gen):
+ async for value in gen:
+ if value % 2 == 0:
+ yield value
+
+ pipeline = Pipeline(source())
+ pipeline.add_step(even_only)
+
+ results = await pipeline.collect()
+ assert results == [0, 2, 4]