Skip to content

Commit 31af12b

Browse files
authored
Merge pull request #51 from thecodecrate/chore/organize-as-spatie
update docs
2 parents e05149b + 7f76d9c commit 31af12b

File tree

14 files changed

+200
-183
lines changed

14 files changed

+200
-183
lines changed

CLAUDE.md

Lines changed: 87 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -4,153 +4,133 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
This is a Python implementation of the Pipeline pattern, distributed as `thecodecrate-pipeline`. The package allows composing sequential stages into reusable, immutable pipelines for processing data. It's inspired by the PHP League Pipeline package.
7+
This is `thecodecrate-pipeline`, a Python package that provides a pipeline pattern implementation for data processing. Pipelines allow chaining multiple stages together, where each stage transforms a payload before passing it to the next stage.
88

9-
## Development Commands
9+
**Key Concepts:**
1010

11-
### Environment Setup
12-
```bash
13-
# The project uses uv for dependency management
14-
# Dependencies are managed in pyproject.toml under [dependency-groups]
15-
uv sync
16-
```
11+
- **Pipeline**: Orchestrates the execution of multiple stages using a processor
12+
- **Stage**: A callable unit that transforms a payload (input → output)
13+
- **Processor**: Defines how stages are executed (e.g., chained, interruptible)
14+
- **PipelineFactory**: Creates pipeline instances with predefined stages and processors
1715

18-
### Testing
19-
```bash
20-
# Run all tests
21-
pytest
16+
## Architecture
2217

23-
# Run tests with coverage
24-
pytest --cov
18+
### Code Organization
2519

26-
# Run a specific test file
27-
pytest tests/test_pipeline.py
20+
The codebase follows a concern-based architecture with clear separation between contracts, concerns, and implementations:
2821

29-
# Run a specific test function
30-
pytest tests/test_pipeline.py::test_function_name
22+
```
23+
src/
24+
├── thecodecrate_pipeline/ # Public API
25+
│ └── __init__.py # Re-exports main classes
26+
└── _lib/ # Internal implementation
27+
├── contracts/ # Protocol definitions (interfaces)
28+
├── concerns/ # Mixins for shared behavior
29+
├── processors/ # Processor implementations
30+
├── support/ # Utility patterns (Clonable, ActAsFactory)
31+
└── types/ # Type definitions
3132
```
3233

33-
### Code Quality
34-
```bash
35-
# Format code with ruff
36-
ruff format .
34+
### Key Design Patterns
3735

38-
# Lint with ruff
39-
ruff check .
36+
**Concerns Pattern**: The Pipeline class inherits from multiple concern classes (BasePipeline, ProcessablePipeline, StageablePipeline), each providing specific functionality. This follows a composition-over-inheritance approach where each concern is responsible for a single aspect:
4037

41-
# Fix auto-fixable linting issues
42-
ruff check --fix .
38+
- `BasePipeline`: Core cloning and factory behavior
39+
- `ProcessablePipeline`: Handles processor management and execution (src/_lib/concerns/processable_pipeline.py)
40+
- `StageablePipeline`: Manages stage collection and instantiation (src/_lib/concerns/stageable_pipeline.py)
4341

44-
# Format with black (line length: 79)
45-
black .
42+
**Contracts (Protocols)**: All contracts are defined using Python's `Protocol` type for structural subtyping. Implementations explicitly declare they implement these protocols via inheritance.
4643

47-
# Type checking is configured with strict mode in pyrightconfig.json
48-
# Type check manually: pyright (if installed)
49-
```
44+
**Clonable Pattern**: Most classes inherit from `Clonable` (src/_lib/support/clonable/clonable.py), which provides immutable-style operations using deep copying. Methods like `pipe()`, `with_stages()`, and `with_processor()` return cloned instances rather than mutating the original.
5045

51-
### Documentation
52-
```bash
53-
# Build documentation locally
54-
mkdocs serve
46+
**ActAsFactory Pattern**: The `PipelineFactory` uses this pattern (src/_lib/support/act_as_factory/) to create pipeline instances with predefined configuration.
5547

56-
# Documentation is built with mkdocs-material and auto-generates API docs
57-
# from docstrings using mkdocstrings-python
58-
```
48+
### Type System
5949

60-
### Version Management
61-
```bash
62-
# Bump version (uses bumpver)
63-
bumpver update --patch # 1.26.0 -> 1.26.1
64-
bumpver update --minor # 1.26.0 -> 1.27.0
65-
bumpver update --major # 1.26.0 -> 2.0.0
50+
The codebase is fully typed using generic types `T_in` and `T_out` for input/output payloads. All classes are generic over these types to ensure type safety through the pipeline chain.
6651

67-
# Note: bumpver automatically commits and tags, but does NOT push
68-
```
52+
## Development Commands
6953

70-
## Architecture
54+
### Environment Setup
7155

72-
### Core Concepts
56+
```bash
57+
# Install uv package manager if not available
58+
uv python install 3.13
59+
uv sync --all-extras --dev
60+
```
7361

74-
The codebase implements a pipeline pattern with three main abstractions:
62+
### Testing
7563

76-
1. **Stage**: A callable unit that transforms input to output (`StageInterface[T_in, T_out]`)
77-
2. **Pipeline**: An immutable chain of stages (`PipelineInterface[T_in, T_out]`)
78-
3. **Processor**: Controls how stages are executed (`ProcessorInterface[T_in, T_out]`)
64+
```bash
65+
# Run all tests with coverage
66+
uv run pytest tests --cov
7967

80-
### Directory Structure
68+
# Run a specific test file
69+
uv run pytest tests/test_pipeline.py
8170

82-
```
83-
src/
84-
├── _lib/ # Internal implementation
85-
│ ├── pipeline/ # Core pipeline implementation
86-
│ │ ├── pipeline.py # Main Pipeline class
87-
│ │ ├── pipeline_factory.py # Factory for building pipelines
88-
│ │ ├── processor.py # Base Processor class
89-
│ │ ├── stage.py # Base Stage class
90-
│ │ ├── processors/ # Built-in processors
91-
│ │ │ ├── chained_processor.py
92-
│ │ │ └── ...
93-
│ │ └── traits/ # Mixins (Clonable, ActAsFactory)
94-
│ └── processors/ # Additional processor implementations
95-
│ ├── chained_processor/ # Processor that chains stages
96-
│ └── interruptible_processor/ # Processor with interruption support
97-
└── thecodecrate_pipeline/ # Public API package
98-
├── __init__.py # Re-exports from _lib
99-
├── processors/ # Public processor exports
100-
└── types/ # Public type exports
71+
# Run a specific test
72+
uv run pytest tests/test_pipeline.py::test_lambda_stages -v
10173
```
10274

103-
### Key Design Patterns
75+
### Linting & Formatting
76+
77+
```bash
78+
# Check linting
79+
uvx ruff check .
10480

105-
**Immutability**: Pipelines use copy-on-write semantics. The `pipe()` method creates a new pipeline instance with the added stage, preserving the original pipeline.
81+
# Fix linting issues automatically
82+
uvx ruff check --fix .
10683

107-
**Traits System**: The codebase uses a trait-like pattern with mixins:
108-
- `Clonable`: Provides shallow cloning capability
109-
- `ActAsFactory`: Enables objects to act as factories for creating instances
84+
# Format code
85+
uvx ruff format .
86+
```
11087

111-
**Interface Segregation**: Each core concept has an interface (`*Interface`) and implementation, enabling custom implementations while maintaining type safety.
88+
### Version Bumping
11289

113-
**Async-First**: All processing is async (`async def process(...)`). The processor handles both sync and async callables transparently using `inspect.isawaitable()`.
90+
```bash
91+
# Bump version (patch/minor/major)
92+
uv run bumpver update --patch
93+
uv run bumpver update --minor
94+
uv run bumpver update --major
95+
```
11496

115-
### Type System
97+
Version is managed by bumpver and automatically updates:
11698

117-
The codebase uses generic type variables for type safety:
118-
- `T_in`: Input type to a stage or pipeline
119-
- `T_out`: Output type from a stage or pipeline (defaults to `T_in`)
99+
- `pyproject.toml`
100+
- `src/thecodecrate_pipeline/__init__.py`
120101

121-
Stages can transform types:
122-
```python
123-
Pipeline[int, str] # Takes int, returns str
124-
StageInterface[int, int] # Takes int, returns int
125-
```
102+
### Documentation
126103

127-
### Processing Flow
104+
```bash
105+
# Build documentation locally
106+
mkdocs serve
107+
108+
# Build static site
109+
mkdocs build
110+
```
128111

129-
1. A Pipeline is created with stages (either via `.pipe()` or declaratively)
130-
2. When `.process(payload)` is called, the pipeline:
131-
- Instantiates stages if needed (converts classes to instances)
132-
- Delegates to the processor's `.process()` method
133-
- The processor iterates through stages, passing output to next stage
134-
3. Processors can customize execution (e.g., ChainedProcessor for error handling)
112+
## Important Implementation Details
135113

136-
### Stream Processing
114+
### Async Processing
137115

138-
Pipelines support processing `AsyncIterator` streams, allowing real-time data transformation where each stage can yield results consumed immediately by the next stage.
116+
All pipeline processing is async. The `Pipeline.process()` method and all stage `__call__` methods are async. The base `Processor._call()` method (src/_lib/processor.py:37-52) handles both sync and async callables automatically using `inspect.isawaitable()`.
139117

140-
### PipelineFactory
118+
### Stage Instantiation
141119

142-
Because pipelines are immutable, `PipelineFactory` provides mutable stage collection during composition. It builds the final immutable pipeline via `.build()`.
120+
Stages can be provided as either classes or instances. The `StageablePipeline` concern automatically instantiates stage classes when needed (src/_lib/concerns/stageable_pipeline.py:67-72).
143121

144-
## Testing Notes
122+
### Processor Types
145123

146-
- Test files use stub classes in `tests/stubs/` for consistent test fixtures
147-
- Tests are async-aware (configured via `pytest.ini` with `pytest-asyncio`)
148-
- Mock stages implement `StageInterface` for type safety
124+
- `ChainedProcessor`: Default processor that executes stages sequentially (src/_lib/processors/chained_processor/)
125+
- `InterruptibleProcessor`: Allows stages to interrupt the pipeline flow (src/_lib/processors/interruptible_processor/)
149126

150-
## Python Version
127+
### Callable Invocation
151128

152-
Requires Python 3.13+ (specified in pyproject.toml).
129+
The `Pipeline` class is callable and delegates to `process()` (src/_lib/concerns/processable_pipeline.py:52-62). The first parameter is positional-only to match the callable signature.
153130

154-
## Package Distribution
131+
## Testing Guidelines
155132

156-
Built with `hatchling`. The wheel includes both `thecodecrate_pipeline` (public API) and `_api` packages (internal). The public package re-exports symbols from `_lib`.
133+
- All async tests must be marked with `@pytest.mark.asyncio`
134+
- Test stubs are located in `tests/stubs/`
135+
- Tests cover: lambda stages, class-based stages, pipeline-as-stage, custom processors, and factory patterns
136+
- The pytest configuration sets `asyncio_default_fixture_loop_scope = function`

README.md

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ Pipelines are implemented as immutable stage chains. When you pipe a new stage,
4949
Operations in a pipeline, stages, can be anything that satisfies the `Callable` type hint. So functions and anything that's callable is acceptable.
5050

5151
```python
52-
pipeline = Pipeline().pipe(lambda payload: payload * 10)
52+
pipeline = (
53+
Pipeline()
54+
.pipe(lambda payload: payload * 10)
55+
)
5356

5457
# Returns 100
5558
await pipeline.process(10)
@@ -110,7 +113,10 @@ The `T_out` type variable is optional and defaults to `T_in`. Similarly, `T_in`
110113
```python
111114
from typing import Any
112115

113-
pipeline = Pipeline[int]().pipe(lambda payload: payload * 2)
116+
pipeline = (
117+
Pipeline[int]()
118+
.pipe(lambda payload: payload * 2)
119+
)
114120

115121
# Returns 20
116122
await pipeline.process(10)
@@ -119,7 +125,10 @@ await pipeline.process(10)
119125
You can also handle varying types between stages:
120126

121127
```python
122-
pipeline = Pipeline[int, str]().pipe(lambda payload: f"Number: {payload}")
128+
pipeline = (
129+
Pipeline[int, str]()
130+
.pipe(lambda payload: f"Number: {payload}")
131+
)
123132

124133
# Returns "Number: 10"
125134
await pipeline.process(10)
@@ -149,7 +158,12 @@ class MyCustomProcessor(Processor[T_in, T_out]):
149158
And use it in your pipeline:
150159

151160
```python
152-
pipeline = Pipeline[int, int](processor=MyCustomProcessor()).pipe(lambda x: x * 2)
161+
pipeline = (
162+
Pipeline[int, int](
163+
processor=MyCustomProcessor(),
164+
)
165+
.pipe(lambda x: x * 2)
166+
)
153167
```
154168

155169
## Declarative Stages
@@ -158,10 +172,10 @@ Instead of using `pipe` to add stages at runtime, you can define stages declarat
158172

159173
```python
160174
class MyPipeline(Pipeline[int, int]):
161-
stages = [
162-
TimesTwoStage(),
163-
TimesThreeStage(),
164-
]
175+
stages = (
176+
TimesTwoStage, # class
177+
TimesThreeStage(), # object
178+
)
165179

166180
# Process the payload through the pipeline with the declared stages
167181
result = await MyPipeline().process(5)
@@ -174,11 +188,11 @@ In this example, `MyPipeline` declares its stages directly in the class definiti
174188

175189
## Declarative Processor
176190

177-
You can also specify the processor in a declarative way by setting the `processor_class` attribute in your pipeline class.
191+
You can also specify the processor in a declarative way by setting the `processor` attribute in your pipeline class.
178192

179193
```python
180194
class MyPipeline(Pipeline[T_in, T_out]):
181-
processor_class = MyCustomProcessor
195+
processor = MyCustomProcessor
182196
```
183197

184198
This allows you to customize the processing behavior of your pipeline while keeping the definition clean and declarative.
@@ -227,13 +241,19 @@ This allows you to process data in a streaming fashion, where each stage can yie
227241

228242
Because pipelines themselves are immutable, pipeline factory is introduced to facilitate distributed composition of a pipeline.
229243

230-
The `PipelineFactory[InputType, OutputType]` collects stages and allows you to create a pipeline at any given time.
244+
The `PipelineFactory[T_in, T_out]` collects stages and allows you to create a pipeline at any given time.
231245

232246
```python
233-
pipeline_factory = PipelineFactory().with_stages([LogicalStage(), AddOneStage()])
247+
pipeline_factory = (
248+
PipelineFactory()
249+
.with_stages([
250+
LogicalStage, # class
251+
AddOneStage(), # object
252+
])
253+
)
234254

235255
# Additional stages can be added later
236-
pipeline_factory.add_stage(LastStage()).with_processor(MyCustomProcessor())
256+
pipeline_factory.add_stage(LastStage())
237257

238258
# Build the pipeline
239259
pipeline = pipeline_factory.build()
@@ -244,7 +264,10 @@ pipeline = pipeline_factory.build()
244264
This package is completely transparent when dealing with exceptions. In no case will this package catch an exception or silence an error. Exceptions should be dealt with on a per-case basis, either inside a _stage_ or at the time the pipeline processes a payload.
245265

246266
```python
247-
pipeline = Pipeline().pipe(lambda payload: payload / 0)
267+
pipeline = (
268+
Pipeline()
269+
.pipe(lambda payload: payload / 0)
270+
)
248271

249272
try:
250273
await pipeline.process(10)

docs/api/core.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
::: thecodecrate_pipeline
44
options:
55
inherited_members: true
6+
filters:
7+
- "!Interface$"

jupyter/01-playground.ipynb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
")\n",
4242
"\n",
4343
"pipeline = (\n",
44-
" (Pipeline[int, str]())\n",
44+
" Pipeline[int, str]()\n",
4545
" .pipe(lambda x: x + 1)\n",
4646
" .pipe(lambda x: x + 1)\n",
4747
" .pipe(lambda x: f\"result is {x}\")\n",
@@ -260,7 +260,7 @@
260260
},
261261
{
262262
"cell_type": "code",
263-
"execution_count": 8,
263+
"execution_count": 7,
264264
"metadata": {},
265265
"outputs": [
266266
{
@@ -269,7 +269,7 @@
269269
"12"
270270
]
271271
},
272-
"execution_count": 8,
272+
"execution_count": 7,
273273
"metadata": {},
274274
"output_type": "execute_result"
275275
}

0 commit comments

Comments
 (0)