insequence 5
A python library for function composition and execution orchestration.
Requires Python: >=3.10

# insequence

A small Python library for function composition and execution orchestration.

## Setup

Currently, Python 3.10 or higher is required. There are no further runtime requirements.

Install via `pip install insequence` or `uv pip install insequence`.

For full type-annotation support, install `insequence[typing]`. This will install `typing-extensions`, which are required for all supported Python versions. This dependency will no longer be needed in Python 3.13 once [PEP 742](https://peps.python.org/pep-0742/) is implemented.

## Outline

`insequence` facilitates building complex transformations (referred to as `Chain`s) by composing simpler transformations. Each transformation (referred to as an `Action`) takes a single argument and returns an object of the same type; `Action`s can be combined only if they operate on the same type.

`Action`s are generated by factories obtained by applying the `action` decorator to user-written functions. Generated `Action`s inherit the first parameter and return type from the decorated function; further parameters of that function become arguments to the `Action` factory (the process somewhat resembles _function currying_).

Modifiers allow altering a `Chain`'s execution without any explicit modification of the chain. Adding new modifiers is simple using the `controller` decorator.

## Example

To generate Fibonacci numbers, create two `Action` factories: one for initialization and one for adding the next number.

```python
>>> from insequence import action
>>>
>>> @action
... def init(numbers: list[int], first: int, second: int) -> list[int]:
...     numbers.extend((first, second))
...     return numbers
...
>>> @action
... def add_next(numbers: list[int]) -> list[int]:
...     numbers.append(numbers[-1] + numbers[-2])
...     return numbers
```

`Action`s are generated by calling the factory functions and can be composed using `+`. The resulting `Chain` is callable; upon execution, the `Action`s are applied in sequence where each `Action` receives the output of its predecessor as its own argument.

```python
>>> generate = init(0, 1) + add_next() + add_next() + add_next()
>>> generate([])
[0, 1, 1, 2, 3]
```

`Chain` execution can be stopped by raising `StopIteration` inside one of its `Action`s. The `repeat` modifier allows looping (with or without limit) within a `Chain`.

```python
>>> @action
... def stop_at(numbers: list[int], limit: int) -> list[int]:
...     if numbers[-1] > limit:
...         raise StopIteration()
...     return numbers
...
>>> add_number = add_next() + stop_at(limit=1000)
>>>
>>> from insequence import modifiers
>>> m = modifiers[list[int]]
>>> generate = init(0, 1) + m.repeat()(add_number)
>>> generate([])
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597]
```

`Chain` modifiers can themselves be composed. Adding some printing at the end of each iteration of the loop can be done without redefining the `add_number`.

```python
>>> import time
>>>
>>> @action
... def show(numbers: list[int], sec: float) -> list[int]:
...     print(numbers[-1], end="\r")
...     time.sleep(sec)
...     return numbers
...
>>> generate = init(0, 1) + (m.repeat() + m.append(show(0.1)))(add_number)
>>> generate([])
```

## What's next?

The package repository contains an `examples` folder with more meaningful examples.