composing-apps

Composing apps

!!! abstract “”

Compose multiple apps into a single pipeline using the `+` operator, see what happens when types don't match, and observe how `NotCompleted` propagates through a pipeline without raising exceptions.

Why compose?

Consider an app that performs a molecular evolutionary analysis (fit_model) and another that extracts statistics from the result (extract_stats). You could apply them sequentially:

python { notest } fitted = fit_model(alignment) stats = extract_stats(fitted)

Composability simplifies this into a single callable:

python { notest } app = fit_model + extract_stats stats = app(alignment)

You can have many more apps in a composed function than just two.

A worked example

We compose three apps: a loader, a processor, and a writer.

```python { notest } from cogent3 import get_app from scinexus import open_data_store

out_dstore = open_data_store(path_to_dir, suffix=“fa”, mode=“w”)

loader = get_app(“load_aligned”, format_name=“fasta”, moltype=“dna”) cpos3 = get_app(“take_codon_positions”, 3) writer = get_app(“write_seqs”, out_dstore, format_name=“fasta”)


### Using apps sequentially

```python { notest }
data = loader("data/primate_brca1.fasta")
just3rd = cpos3(data)
m = writer(just3rd)

Composing into a single pipeline

python { notest } process = loader + cpos3 + writer m = process("data/primate_brca1.fasta")

The result is identical, but the composed form is more concise and enables batch processing via apply_to().

Composability rules

App type ordering

Loaders and writers are special cases. If included, a loader must always be first:

python { notest } app = a_loader + a_generic

If included, a writer must always be last:

python { notest } app = a_generic + a_writer

Changing the order for either will raise a TypeError.

Type compatibility

Apps define the type of input they accept and the type of output they produce. For two apps to be composed, the output type of the app on the left must overlap with the input type of the app on the right. If they don’t match, a TypeError is raised.

NotCompleted propagation

If any step in a composed pipeline returns a NotCompleted, subsequent steps are skipped and the NotCompleted is returned as the final result.

???+ example “Condition not satisfied”

```python { linenums="1" notest }
from cogent3 import get_app

reader = get_app("load_aligned", format_name="fasta")
select_seqs = get_app("take_named_seqs", "Mouse", "Human")
app = reader + select_seqs

result = app("data/primate_brca1.fasta")
print(result)  # (1)!

# NotCompleted(type=FAIL, origin=take_named_seqs, source="primate_brca1",
# message="named seq(s) {'Mouse'} not in ('FlyingLem', 'TreeShrew', 'Galago',
# 'HowlerMon', 'Rhesus', 'Orangutan', 'Gorilla', 'Chimpanzee', 'Human')")
```

1. A successful load but a failed selection — the `NotCompleted` from `select_seqs` is returned 

???+ example “Caught an exception”

```python { linenums="1" notest }
from cogent3 import get_app

reader = get_app("load_aligned", format_name="fasta")
select_seqs = get_app("take_named_seqs", "Mouse", "Human")
app = reader + select_seqs

result = app("primate_brca1.fasta")
print(result)  # (1)!

# NotCompleted(type=ERROR, origin=load_aligned, source="primate_brca1",
# message="Traceback (most recent call last):   File
# "/Users/gavin/repos/SciNexus/src/scinexus/composable.py", line 545, in __call__
# result = self.main(val, *args, **kwargs)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   File
# "/Users/gavin/repos/SciNexus/.venv/lib/python3.12/site-
# packages/cogent3/app/io.py", line 334, in main     return _load_seqs(path,
# cogent3.make_aligned_seqs, self._parser, self.moltype)
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^   File
# "/Users/gavin/repos/SciNexus/.venv/lib/python3.12/site- [...]
```

1. An error during load — `select_seqs` is never called.