Custom Python package¶
About this page
This documents a stack generated from soliplex-template. A generated
project ships its own copy of this page without this note.
Every entry under src/ is a self-contained project directory: a
pyproject.toml, an src/ package inside it, and its own tests. This
project's own is src/myproject/:
src/myproject/ is the project directory; src/myproject/src/myproject/ is
the importable package inside it. <other>/ is whatever else you clone —
same shape, no special handling.
Work in it the way you would work in any checkout:
cd src/myproject
uv sync # create/refresh the dev environment (installs pytest)
uv run pytest # run the project's tests
uv pip install -e . # or a plain editable install into another environment
The stack root has its own pyproject.toml, but it is a tooling environment
(soliplex-cli, the docs site) rather than a library — the importable code all
lives under src/.
How it reaches the backend¶
The Soliplex backend bind-mounts ./src (read-only) into its container as
/app/src and puts this project's package directory on PYTHONPATH as
/app/src/myproject/src (see docker-compose.yml), so anything you define
here is importable by dotted name from the Soliplex config under
backend/environment/ — no image rebuild needed to edit your code. The
backend watches it too: its serve command carries
--reload-dirs=/app/src/myproject/src, so saving a change to your code
restarts the server.
Because only the package directories named in PYTHONPATH are import roots,
/app/src itself is not one: a repo you clone next to this project is
visible to the container but contributes nothing to the import namespace
until you add its own src/ to PYTHONPATH.
What ships wired up¶
Two examples are referenced from the config so you can see the pattern:
- a tool,
myproject.tools.greeting, referenced frombackend/environment/rooms/custom/room_config.yaml; - a FastAPI router,
myproject.views.router, registered viaapp_router_operationsinbackend/environment/installation.yaml.
Dotted names into this package can equally be used in the installation.yaml
meta: section (tool/agent/skill config classes, MCP wrappers, secret sources)
— see the commented myproject.* examples there.
Making it your own¶
Add modules under src/myproject/src/myproject/, reference them by dotted name
from the config, and add tests under src/myproject/tests/unit/. Delete the
demonstration custom room once you have your own.
Adding a second project¶
src/ is where you clone any other repo you want to work on alongside this
one — the soliplex backend itself, a library you depend on, a tool you are
patching.
1. Clone it¶
The soliplex-template skill does the clone and the wiring below in one step,
which is the recommended route:
uv run <skill>/scripts/src_projects.py clone <url>
uv run <skill>/scripts/src_projects.py list # what is there, what is on the path
By hand it is just:
Nothing to do for git either way: .gitignore ignores everything under src/
except this project's own directory, so the clone stays part of its own repo
and never shows up in this stack's git status. That is a rule, not a list — a
third and fourth clone need no further edits.
2. Work on it¶
Each project directory carries its own environment, so treat it as the standalone checkout it is:
Its .venv is its own; nothing is shared with this project's, or with the
stack root's.
3. Make it importable by the backend (optional)¶
Only needed if the backend should run that checkout's code. The ./src
bind mount already carries it into the container at /app/src/<other>; what
it lacks is a place on the import path.
src_projects.py clone does this automatically, and src_projects.py add
<other> does it for a checkout you cloned by hand. Either way it also checks
the checkout's declared dependencies against the backend image and tells you
whether a rebuild is needed — see What PYTHONPATH does and does not do
below for why that matters.
By hand, append its package directory to the backend's PYTHONPATH in
docker-compose.yml:
and, so edits to it restart the server, a matching --reload-dirs line to
the backend's serve command, after this project's own:
The trailing /src is right for a checkout that uses a src/ layout (most
modern projects, including this one). For a flat-layout repo — packages
sitting at the repo root — use /app/src/<other> instead. Point at the
directory that contains the importable package, never at the package
itself, and never at /app/src: making the mount point an import root is
what this layout exists to avoid.
Then restart the backend. This is a compose change, so the --reload=config
flag does not pick it up:
What PYTHONPATH does and does not do¶
- It shadows installed distributions.
PYTHONPATHis searched before site-packages, so asoliplexcheckout listed there wins over thesoliplexinstalled in the image. That is what makes a backend dev-mode checkout work — and what makes an accidental name collision confusing. - Every project on it is watched. The backend's serve command carries one
--reload-dirsperPYTHONPATHentry, whichsrc_projects.pykeeps in step, so editing any of them restarts the server: asoliplexcheckout is no special case.src_projects.py listshows which are watched. - It does not install anything. The checkout's own dependencies are not
resolved: only its source becomes importable. If it imports a third-party
package the backend image does not already have, the import fails at
runtime — and
docker compose up -d backendwill not fix it, because the dependency has to be baked into the image. Add it tobackend/requirements.txt, thendocker compose build backend; see Backend image & dependencies.src_projects.pychecks the project's direct requirements for you, reporting each as satisfied, MISSING, or CONFLICT: installed, at a version the project rejects.requirements.txtcannot fix a conflict, since the image's own versions are frozen as constraints; adapt the project to the image's versions, pick a soliplex image tag that has versions it accepts, or customizebackend/Dockerfile. For what lies deeper than the direct requirements, see Checking it in the backend's environment. - The mount is read-only, and the container runs as the
PUID:PGIDfrom.env. Edit the checkout on the host; the backend only reads it.
Checking it in the backend's environment¶
uv run pytest in a project directory runs against that project's own
uv.lock, which is not what the backend has: the backend imports the
project's code alongside the soliplex image's own versions. When
src_projects.py adds a project, it checks the direct requirements in its
pyproject.toml against the image, but not the requirements of those
requirements.
To check the code itself against the image, import it in the dev-mode
soliplex-dev service, which runs the backend's image with the very same
PYTHONPATH (--no-deps: the check needs no database):
docker compose run --rm --no-deps soliplex-dev /app/.venv/bin/python -c "import myproject.tools, myproject.views"
For another project on the path, import its own package instead. A silent
exit means everything its modules import is there; an ImportError, or a
version error raised on import, names what the image lacks or has at the
wrong version, however deep in the tree. It does not run the project's
tests, so code that imports cleanly but needs a newer API still fails only
when it runs — which is what the tests are for.
Running its tests there¶
soliplex-dev is built with every src/ project's dev dependency group
(pytest and the like) installed on top of the backend's image, with the
image's own versions frozen, so a project's tests run there against the very
versions the backend imports it with:
docker compose run --rm --no-deps -w /app/src/myproject soliplex-dev /app/.venv/bin/python -m pytest
For another project, pass its own directory to -w. The dev groups are
listed in soliplex-dev/requirements-dev.txt, which src_projects.py
regenerates whenever it adds or clones a project. Every project directory
under src/ contributes, whether or not it is on PYTHONPATH, so taking one
off the path (src_projects.py remove) leaves the file as it is. After
changing a project's dev group yourself, or deleting a project's directory
from src/, regenerate the file and rebuild:
Commit the regenerated file: a change to it is a change to what the tests
run against. A dev requirement that needs a different version of something
the backend image has fails that build, as one in backend/requirements.txt
does; adapt the project's dev group to the image's versions.