Skip to content

Developing MedUX Plugins

MedUX plugins are independent Python packages that extend MedUX via GDAPS. A plugin depends on medux, which transitively pulls in conjunto and gdaps.

TL;DR

curl -sSL https://gitlab.com/nerdocs/medux/medux/-/raw/main/scripts/bootstrap.sh | bash -s myplugin
cd medux-myplugin
source .venv/bin/activate    # mandatory for every session
medux migrate
medux runserver

The bootstrap script generates the plugin from the cookiecutter template, writes a [tool.uv.sources] entry that pins medux to its Git main branch, runs uv sync (which pulls medux + deps from Git), creates a dev .env, and initializes a git repo. No local sibling checkouts are cloned by default — pure plugin development does not need them.

Activate .venv — never prefix commands with uv run

Plugin development requires the plugin's .venv to be active. The medux CLI script lives in .venv/bin/medux and is only on your $PATH after source .venv/bin/activate.

Do not use uv run medux … or uv run pytest …uv run implicitly syncs the venv against uv.lock, which also removes any editable overlay installed by make dev-local. You would then have to re-run make dev-local.

uv is still used for dependency management (uv sync, uv add, uv lock) — just not for running tools.

Alpha phase: dependencies come from Git main

MedUX, Conjunto, and GDAPS have no PyPI releases yet. The plugin's committed pyproject.toml therefore resolves medux via [tool.uv.sources]:

[tool.uv.sources]
medux = { git = "https://gitlab.com/nerdocs/medux/medux.git", rev = "main" }

uv sync pulls the current main HEAD into the plugin's .venv. Conjunto and GDAPS are pulled in transitively by medux's own [tool.uv.sources]. Pure plugin development needs nothing else.

Once MedUX reaches a stable PyPI release, the Git-URL source can be dropped and uv sync will resolve from PyPI.

Optional: local editable overlay of medux / conjunto

If you are also hacking on medux or conjunto itself while building a plugin, clone them as siblings and use the editable overlay:

~/Projects/
├── medux/                # git checkout
├── conjunto/             # git checkout (optional)
└── medux-myplugin/       # this plugin

In the plugin directory, make dev-local first runs uv sync (which installs medux/conjunto from Git main) and then overlays the sibling checkouts as editable installs via uv pip install --python .venv/bin/python --no-deps -e ../medux ../conjunto. pyproject.toml and uv.lock are never touched — CI still resolves from Git main, exactly as committed. Re-run make dev-local after every uv sync (which wipes the overlay) or after re-activating the venv.

To enable the overlay immediately at bootstrap time, pass --local=medux (or --local=medux,conjunto) to bootstrap.sh; the script then asks whether to clone any missing siblings.

Prerequisites

  • uv (package manager / runner)
  • Git
  • For pure plugin development: internet access to clone the medux Git repository (HTTPS, no credentials required)
  • Only if you pass --local=...: an SSH key registered at gitlab.com for cloning sibling repositories

Bootstrap

Default: Git-main only, no sibling clones

curl -sSL https://gitlab.com/nerdocs/medux/medux/-/raw/main/scripts/bootstrap.sh | bash -s myplugin

This creates medux-myplugin/ with pyproject.toml pinned to medux's Git main branch, runs uv sync (which fetches medux + transitive deps from Git), writes a dev .env, and git inits the repo.

No siblings are cloned. You can start developing immediately:

cd medux-myplugin
source .venv/bin/activate
medux migrate
medux runserver

Custom sibling overlay

If you also hack on medux, conjunto, or gdaps while building the plugin, request a local overlay:

bash bootstrap.sh myplugin --local=medux,conjunto -y

The script confirms or clones the named sibling repositories, runs uv sync as usual, and then overlays the siblings as editable installs. make dev-local reapplies this overlay after any future uv sync.

What the script does

  1. Runs the gl:nerdocs/gdaps-plugin-cookiecutter template non-interactively (author taken from git config).
  2. Replaces the generated gdaps dependency with medux in [project.dependencies] and writes medux = { git = "https://gitlab.com/nerdocs/medux/medux.git", rev = "main" } in [tool.uv.sources]. Any extra --local packages get the same treatment. Committed state therefore always resolves from Git — no local paths leak into pyproject.toml.
  3. Optionally (only with --local): confirms or clones the requested sibling checkouts into the current directory.
  4. Runs uv sync, then (only with --local) overlays the editable sibling installs via uv pip install --python .venv/bin/python --no-deps -e ../<pkg> (same pattern as make dev-local).
  5. git init + installs a pre-push hook that runs pytest (only when the cookiecutter shipped a .pre-commit-config.yaml).

Editable sibling overlay

Every generated plugin ships with a Makefile implementing the same overlay pattern used by medux itself:

make dev-local   # uv sync + editable overlay of ../medux, ../conjunto
make dev-pypi    # plain uv sync — no overlay

make dev-local reinstalls medux / conjunto from sibling checkouts on top of a Git-synced venv without touching pyproject.toml or uv.lock. That means committing and pushing never triggers a dependency-source switch — the committed state is always the Git-main baseline that CI resolves via [tool.uv.sources].

Note

uv sync is authoritative and will wipe the overlay. Re-run make dev-local after every uv sync (or after make dev-pypi) to restore the editable sibling links.

MedUX itself offers the same overlay if you want to develop against a local ../conjunto or ../gdaps:

cd ~/Projekte/medux
make dev-local

Plugin anatomy

See Plugin Structure for the directory layout and the meaning of each file (apps.py, menus.py, scoped_settings.py, gdaps_hooks.py, ...).

The plugin must register itself via an entry point:

[project.entry-points."medux.plugins"]
myplugin = "medux.plugins.myplugin:apps.MyPluginConfig"

Its AppConfig must subclass MeduxPluginAppConfig:

from medux.common.api import MeduxPluginAppConfig

class MyPluginConfig(MeduxPluginAppConfig):
    name = "medux.plugins.myplugin"
    verbose_name = "My Plugin"

    class PluginMeta:
        pass

Testing

source .venv/bin/activate
pytest             # or: make test

Plugins use pytest-django. The generated tests/settings.py is a minimal Django settings module suitable for running the plugin in isolation. See medux-schedule for a full testing example.

Useful commands

All tool commands assume the venv is active (source .venv/bin/activate).

Command Description
make dev-local / make dev-pypi Editable-sibling overlay vs plain Git-synced venv
medux migrate Apply database migrations
medux runserver Start the development server
medux syncplugins Sync plugin metadata to the database
medux makemigrations Create migrations for model changes
medux shell Open a Django shell
pytest Run the test suite
ruff check . Run the linter

Switching between sibling and Git resolution

make dev-pypi    # plain uv sync — resolves medux/conjunto from Git main
make dev-local   # same, then overlays ../medux / ../conjunto as editable

The overlay has zero effect on tracked files, so CI always resolves from the [tool.uv.sources] Git URLs regardless of the local overlay state. The dev-pypi target is named for historical reasons — during the alpha phase it really fetches from Git main, not from PyPI.

Using an .env file

Drop a .env file into your plugin directory to configure the database, debug mode, or other settings. Without one, MedUX defaults to SQLite (db.sqlite3 in the current directory) with DEBUG=True.

Example for PostgreSQL:

DATABASE_ENGINE=django.db.backends.postgresql
DATABASE_NAME=medux_dev
DATABASE_USER=myuser
DATABASE_PASS=mypassword
DEBUG=True

Git hooks

The bootstrap script installs a pre-push hook that runs pytest before every git push, so broken tests never reach the remote. The included .pre-commit-config.yaml also runs Black, Ruff, and basic file checks on every commit.

If you set up a plugin manually, install the hooks yourself (venv active):

git init
pre-commit install --hook-type pre-push

Troubleshooting

Permission denied (publickey) when bootstrapping with --local=. Only --local=... clones via SSH. Register your SSH key at https://gitlab.com/-/user_settings/ssh_keys and ensure ssh -T git@gitlab.com works. The default (no --local) uses HTTPS Git fetches via uv sync and does not need an SSH key.

'./medux' exists but is not a git repository. Only occurs with --local=medux. You have a stray directory of that name. Remove or rename it, then rerun the script.

uv sync fails: cannot find medux or conjunto on PyPI. During alpha, these packages are not on PyPI yet. The committed pyproject.toml therefore must carry a [tool.uv.sources] entry pointing at the Git main branch (e.g. medux = { git = "https://gitlab.com/nerdocs/medux/medux.git", rev = "main" }). If that entry is missing, add it — otherwise uv sync has no way to resolve the package.

The editable overlay is gone after uv sync. That's expected — uv sync removes packages not in uv.lock. Re-run make dev-local to reapply the overlay.

Plugin not picked up by MedUX. With the venv active, run medux syncplugins. Verify the [project.entry-points."medux.plugins"] entry in pyproject.toml points at the correct AppConfig.

medux: command not found. You forgot source .venv/bin/activate. The medux script lives in .venv/bin/medux; it is only on your $PATH while the venv is active.

Reference implementation

medux-schedule is a real MedUX plugin and the canonical reference for the layout, pyproject.toml shape, [tool.uv.sources] pattern, and test setup.

Next steps