Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions docs/content/creating-tesseracts/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,6 @@ After that is done, you will be able to use the `tesseract-runtime` command in y
This is the exact same command that is launched inside Tesseract containers to run their
various endpoints, and its syntax mirrors the one of `tesseract run`.

```{note}
When running without containerization, the directory containing `tesseract_api.py` is automatically added to `PYTHONPATH`, allowing you to import additional Python modules from that directory. This behavior matches what happens inside Tesseract containers, where the path containing `tesseract_api.py` is also added to `PYTHONPATH`.
```

For instance, to call the `apply` function, rather than first building a `helloworld` image and running this command:

```bash
Expand Down
62 changes: 0 additions & 62 deletions docs/content/examples/building-blocks/localdependency.md

This file was deleted.

96 changes: 96 additions & 0 deletions docs/content/examples/building-blocks/localpackage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Installing local Python modules into a Tesseract

## Context
Sometimes it might be necessary to bundle local Python modules into a Tesseract.

There are 2 ways to do this:

1. Make them a proper Python package with `pyproject.toml` and add the local path to the `tesseract_requirements.txt` file.
Both absolute and relative paths work, but in case they are relative paths, they should be
relative to the Tesseract's root folder (i.e., the one which contains the `tesseract_api.py` file).
2. Just put them as `.py` files next to `tesseract_api.py` and add them to `build_config.package_data` (see also [packagedata.md]) in `tesseract_config.yaml` to make sure they're being included in container builds.

## Example Tesseract

Here is an example Tesseract that highlights both ways to include dependencies.

```{literalinclude} ../../../../examples/localpackage/tesseract_api.py
:language: python
```

The custom package `helloworld` is shipped with a file `helloworld.py` and a `pyproject.toml` that ensures it can be built as a Python package.

```{literalinclude} ../../../../examples/localpackage/helloworld/helloworld.py
:language: python
```

```{literalinclude} ../../../../examples/localpackage/helloworld/pyproject.toml
:language: toml
```

Then, it can be added as a local dependency to `tesseract_requirements.txt` by passing a relative path:

```{literalinclude} ../../../../examples/localpackage/tesseract_requirements.txt
:language: text
```

The local module `goodbyeworld.py` is just a single Python file:

```{literalinclude} ../../../../examples/localpackage/goodbyeworld.py
:language: python
```

To ensure it gets added to built Tesseracts, we have to register it as `package_data`:

```{literalinclude} ../../../../examples/localpackage/tesseract_config.yaml
:language: yaml
```

Now we are ready to build the Tesseract. This Tesseract will accept a name like "Tessie" as an input and return a message:

```bash
$ tesseract build examples/localpackage
$ tesseract run localpackage apply '{"inputs": {"name": "Tessie"}}'
{"message":"Hello Tessie!\nGoodbye Tessie!"}
```

This confirms that both custom dependencies are available to the Tesseract container.

To run the Tesseract without containerization, we have to make sure that `helloworld` is installed into our dev environment:

```bash
$ pip install examples/localpackage/helloworld
$ TESSERACT_API_PATH=examples/localpackage/tesseract_api.py tesseract-runtime apply '{"inputs": {"name": "Tessie"}}'
{"message":"Hello Tessie!\nGoodbye Tessie!"}
```

## Advanced pattern: injecting private dependencies as local wheels

In case a Tesseract depends on private packages that are not accessible from the public Python package index (PyPI), injecting them as local files can be a useful way to side-step authentication issues.

This is especially powerful in conjunction with `pip download` (e.g. from a private PyPI registry) to obtain a pre-built wheel:

```bash
$ pip download cowsay==6.1
Collecting cowsay==6.1
Obtaining dependency information for cowsay==6.1 from https://files.pythonhosted.org/packages/f1/13/63c0a02c44024ee16f664e0b36eefeb22d54e93531630bd99e237986f534/cowsay-6.1-py3-none-any.whl.metadata
Downloading cowsay-6.1-py3-none-any.whl.metadata (5.6 kB)
Downloading cowsay-6.1-py3-none-any.whl (25 kB)
Saved ./cowsay-6.1-py3-none-any.whl
Successfully downloaded cowsay
```

We can then specify it as a local dependency in `tesseract_requirements.txt` by adding the following line:
```
./cowsay-6.1-py3-none-any.whl
```

Finally, let's build the Tesseract, and verify it works
```bash
$ tesseract build mytess
[i] Building image ...
[i] Built image sha256:7d024, ['mytess:latest']

$ tesseract run mytess apply '{"inputs": {"message": "Hello, World!"}}'
{"out":" _____________\n| Hello, World! |\n =============\n \\\n \\\n ^__^\n (oo)\\_______\n (__)\\ )\\/\\\n ||----w |\n || ||"}
```
4 changes: 2 additions & 2 deletions docs/content/examples/example_gallery.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Building Blocks
# Building Blocks Example Gallery

```{toctree}
:name: example-gallery
Expand All @@ -11,7 +11,7 @@ building-blocks/vectoradd.md
building-blocks/univariate.md
building-blocks/packagedata.md
building-blocks/arm64.md
building-blocks/localdependency.md
building-blocks/localpackage.md
building-blocks/dataloader.md
building-blocks/filereference.md
```
Expand Down
6 changes: 6 additions & 0 deletions examples/localpackage/goodbyeworld.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Copyright 2025 Pasteur Labs. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0


def ungreet(name):
return f"Goodbye {name}!"
8 changes: 5 additions & 3 deletions examples/localpackage/tesseract_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Copyright 2025 Pasteur Labs. All Rights Reserved.
# SPDX-License-Identifier: Apache-2.0

from goodbyeworld import ungreet
from helloworld import greet
from pydantic import BaseModel, Field

Expand All @@ -10,9 +11,10 @@ class InputSchema(BaseModel):


class OutputSchema(BaseModel):
greeting: str = Field(description="A greeting!")
message: str = Field(description="A message!")


def apply(inputs: InputSchema) -> OutputSchema:
"""Greet a person whose name is given as input."""
return OutputSchema(greeting=greet(inputs.name))
"""Greets and ungreets a person whose name is given as input."""
message = f"{greet(inputs.name)}\n{ungreet(inputs.name)}"
return OutputSchema(message=message)
4 changes: 4 additions & 0 deletions examples/localpackage/tesseract_config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
name: "localpackage"
version: "0.0.1"
description: ""

build_config:
package_data:
- ["goodbyeworld.py", "goodbyeworld.py"]
2 changes: 1 addition & 1 deletion examples/localpackage/tesseract_requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
./package
./helloworld
14 changes: 13 additions & 1 deletion tesseract_core/runtime/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import importlib.util
import os
import sys
from collections.abc import Callable, Generator
from contextlib import contextmanager
from io import TextIOBase
Expand Down Expand Up @@ -49,22 +50,33 @@ def redirect_fd(


def load_module_from_path(path: Union[Path, str]) -> ModuleType:
"""Load a module from a file path."""
"""Load a module from a file path.

Temporarily puts the module's parent folder on PYTHONPATH to ensure local imports work as expected.
"""
path = Path(path)

if not path.is_file():
raise ImportError(f"Could not load module from {path} (is not a file)")

module_name = path.stem
module_dir = path.parent.resolve()

spec = importlib.util.spec_from_file_location(module_name, path)
if spec is None:
raise ImportError(f"Could not load module from {path}")

module = importlib.util.module_from_spec(spec)

try:
old_path = sys.path
sys.path = [str(module_dir), *old_path]
spec.loader.exec_module(module)
except Exception as exc:
raise ImportError(f"Could not load module from {path}") from exc
finally:
sys.path = old_path

return module


Expand Down
1 change: 0 additions & 1 deletion tesseract_core/sdk/templates/Dockerfile.base
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,6 @@ COPY "{{ tesseract_source_directory }}/tesseract_api.py" ${TESSERACT_API_PATH}
RUN chmod 444 ${TESSERACT_API_PATH}

ENV PATH="/python-env/bin:$PATH"
ENV PYTHONPATH="/tesseract:$PYTHONPATH"

{% if config.build_config.package_data %}
# Copy package data to image
Expand Down
2 changes: 1 addition & 1 deletion tests/endtoend_tests/test_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class Config:
SampleRequest(
endpoint="apply",
payload={"inputs": {"name": "Ozzy"}},
output_contains_pattern="Hello Ozzy!",
output_contains_pattern="Hello Ozzy!\\nGoodbye Ozzy!",
),
],
),
Expand Down
19 changes: 19 additions & 0 deletions tests/runtime_tests/test_runtime_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,3 +569,22 @@ def test_check(cli, cli_runner, dummy_tesseract_package):
f"{schema_name} is not a subclass of pydantic.BaseModel"
in result.exception.args[0]
)


def test_local_module(cli, cli_runner, dummy_tesseract_package):
"""Ensure that a .py file next to tesseract_api.py can be imported."""
from tesseract_core.runtime.config import update_config

tesseract_api_file = Path(dummy_tesseract_package) / "tesseract_api.py"
with open(tesseract_api_file, "a") as f:
f.write("\nimport foo")

foo_file = Path(dummy_tesseract_package) / "foo.py"
with open(foo_file, "w") as f:
f.write("print('hey!')")

update_config(api_path=tesseract_api_file)
result = cli_runner.invoke(cli, ["check"], catch_exceptions=True)
assert result.exit_code == 0, result.stderr
assert "check successful" in result.stdout
assert "hey!" in result.stdout
Loading