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
123 changes: 82 additions & 41 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,50 +1,91 @@
FROM quay.io/jupyter/minimal-notebook:2025-04-14
LABEL authors="Max Winokan"


# Ported from Max's Dockerfile to support local development

# combines conda and pip environments. There's a conflict somewhere
# which I haven't resolved (TODO) between something in conda and pip
# envs, so cannot use a single env

# this is what I'm stuck on. Project depends on numpy v1.23 (why?) and
# I can't go over 3.12
ARG PYTHON_VERSION=3.12


WORKDIR "/home/code/HIPPO"
# WORKDIR "/home/code/HIPPO/data"
# WORKDIR "/home/code/HIPPO/hippo"
# Dev environment for local HIPPO development.

USER 0
FROM python:3.13-slim

LABEL authors="Max Winokan"

# install package dependencies into different virtual env
# The container writes into bind-mounted host directories (./data), so it runs
# as a normal user whose id matches the host's rather than as root, otherwise
# files created in the mounts end up root-owned on the host. Override at build
# time if your host account is not 1000: --build-arg UID=$(id -u).
ARG UID=1000
ARG GID=1000

ENV VIRTUAL_ENV=/home/code/HIPPO/.venv \
PYTHONUNBUFFERED=1 \
UV_LINK_MODE=copy

# .venv first so `python`, `jupyter` etc. resolve to the environment.
# PYTHONPATH puts the project root on sys.path so `import hippo` works from a
# notebook in any subdirectory (the package itself is bind-mounted at runtime,
# see docker-compose.yaml: ./hippo:/home/code/HIPPO/hippo).
ENV PATH="${VIRTUAL_ENV}/bin:${PATH}" \
PYTHONPATH="/home/code/HIPPO"


RUN apt-get update \
&& apt-get install -y --no-install-recommends \
screen \
ca-certificates \
libexpat1 \
zlib1g \
libx11-6 \
libxext6 \
libxrender1 \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir uv

WORKDIR /home/code/HIPPO

# Only the lock and manifest are copied; the hippo package is bind-mounted at
# runtime. --no-install-project therefore stops uv_build failing with
# "Expected a Python module at: hippo/__init__.py". There are no
# [project.scripts], so nothing needs the project to be installed.
COPY uv.lock pyproject.toml ./
RUN pip install --upgrade pip && python -m pip install uv

# install all dependencies into active environment without updating lockfile
# RUN uv sync --frozen --active
RUN uv venv /home/code/HIPPO/.venv
RUN uv sync --frozen --python /home/code/HIPPO/.venv/bin/python
RUN uv venv "${VIRTUAL_ENV}" \
&& uv sync --frozen --no-install-project

# now add venv python to path so conda python can find it
ENV PATH="/home/code/HIPPO/.venv/bin:$PATH"
ENV PYTHONPATH="/home/code/HIPPO/.venv/lib/python${PYTHON_VERSION}/site-packages:$PYTHONPATH"

# NB `import syndirella` additionally requires PyRosetta to be installed, which
# it is not here -- xchem-fragmenstein substitutes a mock into
# sys.modules['pyrosetta'] that supports attribute access but not submodule
# imports, so the import dies with "'AttributeFilledMock' object is not
# iterable". PyRosetta is a large out-of-band download with its own licence
# terms, so it is deliberately left out; add it with
# python -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()"
# if the code that consumes syndirella is ported. This does not affect HIPPO,
# which does not import syndirella.

# patch rich
# Rewrites rich/jupyter.py in site-packages, so it must run while still root.
RUN python -c "import mrich; mrich.patch_rich_jupyter_margins()"


# notebooks
RUN chown ${NB_USER} "/home/code" && sudo apt update && sudo apt install screen -y

# force numpy update. since there's now a new venv, make sure conda is
# used (othewise numpy will be invisible)
RUN uv pip install --upgrade numpy==2.4.4
USER ${NB_USER}


# WORKDIR "/home/code/HIPPO"
# Import smoke test. Missing system shared libraries only show up when an
# extension is actually loaded, which otherwise means a clean build followed by
# a container that dies on the first import (this is how the libXrender and
# libexpat gaps above were found). Importing the heavy extension modules here
# turns that into a build failure instead. hippo itself is not importable at
# build time -- it is bind-mounted at runtime.
# `syndirella` is intentionally absent from this list -- see the note above: it
# is installed but not importable without PyRosetta.
RUN python -c "\
import molparse, openmm, gemmi, apsw, psycopg, sklearn, pandas, numpy, jupyterlab; \
from rdkit.Chem.Draw import rdMolDraw2D; \
from ta_auth_connector import get_auth_target_access; \
print('import smoke test OK')"

# -f so the build still works if the base image ever ships a group on that id.
RUN groupadd -f -g "${GID}" hippo \
&& useradd -u "${UID}" -g "${GID}" -m -s /bin/bash hippo \
&& chown -R hippo:hippo /home/code
USER hippo

EXPOSE 8888

# Serves on all interfaces so the published port reaches it; docker-compose maps
# 8888:8888. The login token is printed to the container log on startup.
CMD ["jupyter", "lab", \
"--ip=0.0.0.0", \
"--port=8888", \
"--no-browser", \
"--ServerApp.root_dir=/home/code/HIPPO"]
40 changes: 38 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,50 @@ Please see the [documentation](https://hippo-docs.winokan.com) to get started

## Installation

HIPPO is pip-installable, to install it, simply run
HIPPO requires **python 3.13**. Install it with an explicit version:

```bash
pip install xchem-hippo
pip install xchem-hippo==2.0.3
```

**Pin the version.** A bare `pip install xchem-hippo` is not safe here. If the
requested version cannot be installed — most often because the interpreter is
not 3.13 — pip does not report that. It quietly works backwards through older
releases until it finds one that fits, and lands on **1.0.5**, the last release
predating the 2.x rewrite. The install succeeds and nothing warns you, but the
package installed is a different codebase.

The 1.0.x releases cannot be yanked, as existing work depends on them, so this
fallback path stays open. Pinning closes it: with `==` there is only one
candidate, so pip has nothing to fall back to and reports the real error
instead.

To run the full suite of tests see README-dev.md.

### Syndirella

HIPPO reads and writes [Syndirella](https://github.com/xchem/syndirella)'s file
formats. `xchem-syndirella` is a normal dependency and is installed for you.

It was previously excluded, because the older `syndirella` distribution required
python `<3.11` and `numpy<2` and so could not be depended on at all.
`xchem-syndirella` 1.0.6 lifted both caps. It does still pin `numpy<=2.4`, which
is what holds numpy below the latest release in this project.

Note that `import syndirella` also requires **PyRosetta**, which is not
installed automatically: it is a large out-of-band download with its own licence
terms. Without it, `xchem-fragmenstein` substitutes a mock object for
`pyrosetta` that cannot satisfy submodule imports, and importing syndirella
fails with `'AttributeFilledMock' object is not iterable`. To install it:

```bash
python -c "import pyrosetta_installer; pyrosetta_installer.install_pyrosetta()"
```

This does not affect HIPPO itself, which does not yet import syndirella — the code
that did (`Pose.posebusters()`) has not been ported yet.


## More Information

<details>
Expand Down
10 changes: 5 additions & 5 deletions hippo/bootstrap.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import django
import mrich
from django.conf import settings
from ta_auth_connector import get_auth_target_access

# fix path
ROOT = Path(__file__).resolve().parent
Expand Down Expand Up @@ -78,12 +79,11 @@ def load_hippo(
mrich.bold('Creating HIPPO animal')
mrich.var('target_name', target_name, color='arg')

# TODO: disabled because of STFC downtime on 03-07-2026. re-enable when done
# tas_list = get_auth_target_access(username)
tas_list = get_auth_target_access(username)

# if target_access_string not in tas_list:
# mrich.error(f'User {username} does not have access to {target_access_string}')
# return
if target_access_string not in tas_list:
mrich.error(f'User {username} does not have access to {target_access_string}')
return

if db is None:
# populate from env
Expand Down
2 changes: 1 addition & 1 deletion hippo/designdb/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def deconstruct(self):
# plain-text RDKitMolField shim defined above.
MolField = RDKitMolField
else:
from django_rdkit.models import MolField
from .rdkit_fields import MolField


class BaseModel(models.Model):
Expand Down
Loading
Loading