Skip to content

Commit

Permalink
Merge pull request #185 from openradx/replace-invoke-with-typer
Browse files Browse the repository at this point in the history
Replace invoke with typer
  • Loading branch information
medihack authored Feb 14, 2025
2 parents 4d8dd0d + ae82d7e commit 37f2874
Show file tree
Hide file tree
Showing 21 changed files with 189 additions and 118 deletions.
6 changes: 0 additions & 6 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ FROM mcr.microsoft.com/devcontainers/python:3.13
USER root

# Install system dependencies
# - bash-completion for shell completions of invoke
# - gettext for Django translations
# - postgresql-common for the apt.postgresql.org.sh script
# - postgresql-client-17 for a current version of psql
Expand All @@ -19,10 +18,5 @@ RUN sudo apt-get update \

USER vscode

# pipx is already installed in the base devcontainers Python image
RUN pipx install invoke \
&& invoke --print-completion-script=bash >> ~/.bash_completion \
pipx uninstall invoke

RUN pipx install poetry \
&& poetry completions bash >> ~/.bash_completion
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
},
// https://github.com/orgs/community/discussions/50403
// "initializeCommand": "docker system prune --all --force",
"postCreateCommand": "poetry install && poetry run invoke init-workspace",
"postCreateCommand": "poetry install && poetry run ./cli.py init-workspace",
"customizations": {
"vscode": {
"extensions": [
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
- name: Install dependencies
run: poetry install
- name: Configure environment
run: poetry run invoke init-workspace
run: poetry run ./cli.py init-workspace
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and cache Docker images
Expand All @@ -41,14 +41,14 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Start Docker containers
run: poetry run invoke compose-up --no-build
run: poetry run ./cli.py compose-up --no-build
- name: Run linting
# https://github.com/actions/runner/issues/241#issuecomment-745902718
shell: 'script -q -e -c "bash {0}"'
run: poetry run invoke lint
run: poetry run ./cli.py lint
- name: Run tests
shell: 'script -q -e -c "bash {0}"'
run: poetry run invoke test --cov
run: poetry run ./cli.py test --cov
- name: Stop Docker containers
if: ${{ always() }}
run: poetry run invoke compose-down
run: poetry run ./cli.py compose-down
6 changes: 1 addition & 5 deletions .gitpod.Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ FROM gitpod/workspace-python-3.12
USER root

# Install system dependencies
# - bash-completion for shell completions of invoke
# - gettext for Django translations
# - postgresql-common for the apt.postgresql.org.sh script
# - postgresql-client-17 for a current version of psql
Expand Down Expand Up @@ -33,10 +32,7 @@ RUN mkdir $NVM_DIR \
&& nvm use default

RUN python3 -m pip install --user pipx \
&& python3 -m pipx ensurepath \
&& python3 -m pipx install invoke \
&& invoke --print-completion-script=bash >> $HOME/.bash_completion \
&& pipx uninstall invoke
&& python3 -m pipx ensurepath

# Poetry is already installed in the base Gitpod Python image,
# but we need to upgrade it
Expand Down
2 changes: 1 addition & 1 deletion .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ tasks:
- name: setup
init: |
poetry install
poetry run invoke init-workspace
poetry run ./cli.py init-workspace
ports:
- port: 8000
Expand Down
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ git clone https://github.com/openradx/adit.git
cd adit
poetry install
poetry shell
invoke compose-up
poetry run ./cli.py compose-up
```

The development server of the example project will be started on <http://localhost:8000>

If a library dependency is changed, the containers need to be rebuilt (e.g. by running
`invoke compose-down && invoke compose-up`).
`poetry run ./cli.py compose-down && poetry run ./cli.py compose-up`).
6 changes: 0 additions & 6 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,6 @@
- Can be work around by wrapping another zip file in an encrypted zip file <https://unix.stackexchange.com/a/290088/469228>
- Rewrite dicom_connector to use asyncio (wrap all pynetdicom calls in asyncio.to_thread)
- I don't think that this will gain any performance improvements, so maybe not worth it
- Evaluate other task runners
- <https://www.pyinvoke.org/> # used currently
- <https://github.com/taskipy/taskipy>
- <https://github.com/nat-n/poethepoet>
- <https://just.systems/>
- <https://taskfile.dev/>
- Make a job urgent retrospectively (maybe only staff members can do this)
- A current workaround is to cancel the job, change urgency with Django Admin and then resume the job
- Try to bring channels_liver_server in official pytest_django release
Expand Down
7 changes: 0 additions & 7 deletions adit/core/management/commands/hard_reset_migrations.py

This file was deleted.

13 changes: 10 additions & 3 deletions adit/core/management/commands/populate_orthancs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from pathlib import Path

from django.conf import settings
from django.core.management.base import BaseCommand

Expand All @@ -11,8 +13,12 @@ def add_arguments(self, parser):
parser.add_argument("--reset", action="store_true")

def handle(self, *args, **options):
orthanc1 = OrthancRestHandler(host=settings.ORTHANC1_HOST, port=settings.ORTHANC1_HTTP_PORT)
orthanc2 = OrthancRestHandler(host=settings.ORTHANC2_HOST, port=settings.ORTHANC2_HTTP_PORT)
orthanc1_host: str = settings.ORTHANC1_HOST
orthanc1_http_port: int = settings.ORTHANC1_HTTP_PORT
orthanc2_host: str = settings.ORTHANC2_HOST
orthanc2_http_port: int = settings.ORTHANC2_HTTP_PORT
orthanc1 = OrthancRestHandler(host=orthanc1_host, port=orthanc1_http_port)
orthanc2 = OrthancRestHandler(host=orthanc2_host, port=orthanc2_http_port)

if options["reset"]:
self.stdout.write("Resetting Orthancs...", ending="")
Expand All @@ -27,6 +33,7 @@ def handle(self, *args, **options):

self.stdout.write("Populating Orthanc1 with example DICOMs...", ending="")
self.stdout.flush()
dicoms_folder = settings.BASE_DIR / "samples" / "dicoms"
base_path: Path = settings.BASE_PATH
dicoms_folder = base_path / "samples" / "dicoms"
orthanc1.upload(dicoms_folder)
self.stdout.write("Done")
2 changes: 1 addition & 1 deletion adit/core/management/commands/receiver.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class Command(AsyncServerCommand):
"files to subscribing workers."
)
server_name = "DICOM receiver"
paths_to_watch = settings.SOURCE_FOLDERS
paths_to_watch = settings.SOURCE_PATHS

async def run_server_async(self, **options):
with tempfile.TemporaryDirectory(prefix="adit_receiver_") as tmpdir:
Expand Down
4 changes: 2 additions & 2 deletions adit/core/tests/utils/test_dicom_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def test_download_series_with_c_get(mocker: MockerFixture):
association_mock = create_association_mock()
associate_mock.return_value = association_mock
association_mock.send_c_get.return_value = DicomTestHelper.create_successful_c_get_response()
path = Path(settings.BASE_DIR) / "samples" / "dicoms"
path = Path(settings.BASE_PATH) / "samples" / "dicoms"
ds = read_dataset(next(path.rglob("*.dcm")))
received_ds = []
dicom_operator = create_dicom_operator()
Expand Down Expand Up @@ -158,7 +158,7 @@ def test_download_series_with_c_move(settings: SettingsWrapper, mocker: MockerFi
dicom_operator = create_dicom_operator()
dicom_operator.server.study_root_get_support = False
dicom_operator.server.patient_root_get_support = False
path = Path(settings.BASE_DIR) / "samples" / "dicoms"
path = Path(settings.BASE_PATH) / "samples" / "dicoms"
file_path = next(path.rglob("*.dcm"))
ds = read_dataset(file_path)
responses = [{"SOPInstanceUID": ds.SOPInstanceUID}]
Expand Down
2 changes: 1 addition & 1 deletion adit/core/tests/utils/test_file_transmit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

@pytest.mark.asyncio
async def test_start_transmit_file():
samples_path = Path(f"{settings.BASE_DIR}/samples/dicoms")
samples_path = Path(f"{settings.BASE_PATH}/samples/dicoms")
sample_files = list(samples_path.rglob("*.dcm"))

server = FileTransmitServer(HOST, PORT)
Expand Down
2 changes: 1 addition & 1 deletion adit/core/utils/testing_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def create_dicom_web_client(server_url: str, ae_title: str, token_string: str) -


def load_sample_dicoms(patient_id: str | None = None) -> Iterable[Dataset]:
test_dicoms_path = settings.BASE_DIR / "samples" / "dicoms"
test_dicoms_path = settings.BASE_PATH / "samples" / "dicoms"
if patient_id:
test_dicoms_path = test_dicoms_path / patient_id

Expand Down
32 changes: 16 additions & 16 deletions adit/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
env.read_env()

# The base directory of the project (the root of the repository)
BASE_DIR = Path(__file__).resolve(strict=True).parent.parent.parent
BASE_PATH = Path(__file__).resolve(strict=True).parent.parent.parent

# Used to monitor files for autoreload
SOURCE_FOLDERS = [BASE_DIR / "adit"]
# The source paths of the project
SOURCE_PATHS = [BASE_PATH / "adit"]

# Fetch version from the environment which is passed through from the latest git version tag
PROJECT_VERSION = env.str("PROJECT_VERSION", default="vX.Y.Z")
Expand Down Expand Up @@ -274,7 +274,7 @@
STATIC_URL = "/static/"

# Additional (project wide) static files
STATICFILES_DIRS = (BASE_DIR / "adit" / "static",)
STATICFILES_DIRS = (BASE_PATH / "adit" / "static",)

# For crispy forms
CRISPY_ALLOWED_TEMPLATE_PACKS = "bootstrap5"
Expand All @@ -295,14 +295,14 @@
DBBACKUP_CLEANUP_KEEP = 30

# Orthanc servers are integrated in ADIT by using a reverse proxy (django-revproxy).
ORTHANC1_HOST = env.str("ORTHANC1_HOST")
ORTHANC1_HTTP_PORT = env.int("ORTHANC1_HTTP_PORT")
ORTHANC1_DICOM_PORT = env.int("ORTHANC1_DICOM_PORT")
ORTHANC1_DICOMWEB_ROOT = env.str("ORTHANC1_DICOMWEB_ROOT")
ORTHANC2_HOST = env.str("ORTHANC2_HOST")
ORTHANC2_HTTP_PORT = env.int("ORTHANC2_HTTP_PORT")
ORTHANC2_DICOM_PORT = env.int("ORTHANC2_DICOM_PORT")
ORTHANC2_DICOMWEB_ROOT = env.str("ORTHANC2_DICOMWEB_ROOT")
ORTHANC1_HOST = env.str("ORTHANC1_HOST", default="localhost")
ORTHANC1_HTTP_PORT = env.int("ORTHANC1_HTTP_PORT", default=6501)
ORTHANC1_DICOM_PORT = env.int("ORTHANC1_DICOM_PORT", default=7501)
ORTHANC1_DICOMWEB_ROOT = env.str("ORTHANC1_DICOMWEB_ROOT", "dicom-web")
ORTHANC2_HOST = env.str("ORTHANC2_HOST", default="localhost")
ORTHANC2_HTTP_PORT = env.int("ORTHANC2_HTTP_PORT", default=6502)
ORTHANC2_DICOM_PORT = env.int("ORTHANC2_DICOM_PORT", default=7502)
ORTHANC2_DICOMWEB_ROOT = env.str("ORTHANC2_DICOMWEB_ROOT", "dicom-web")

# Used by django-filter
FILTERS_EMPTY_CHOICE_LABEL = "Show All"
Expand All @@ -318,16 +318,16 @@

# The address and port of the STORE SCP server (part of the receiver).
# By default the STORE SCP server listens to all interfaces (empty string)
STORE_SCP_HOST = env.str("STORE_SCP_HOST")
STORE_SCP_PORT = env.int("STORE_SCP_PORT")
STORE_SCP_HOST = env.str("STORE_SCP_HOST", "localhost")
STORE_SCP_PORT = env.int("STORE_SCP_PORT", 11112)

# The address and port of the file transmit socket server (part of the receiver)
# that is used to transfer DICOM files from the receiver to the workers (when
# the PACS server does not support C-GET).
# By default the file transmit socket server listens to all interfaces (should
# not be a problem as it is inside the docker network).
FILE_TRANSMIT_HOST = env.str("FILE_TRANSMIT_HOST")
FILE_TRANSMIT_PORT = env.int("FILE_TRANSMIT_PORT")
FILE_TRANSMIT_HOST = env.str("FILE_TRANSMIT_HOST", "localhost")
FILE_TRANSMIT_PORT = env.int("FILE_TRANSMIT_PORT", 14638)

# Usually a transfer job must be verified by an admin. By setting
# this option to True ADIT will schedule unverified transfers
Expand Down
53 changes: 53 additions & 0 deletions cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#! /usr/bin/env python3

from pathlib import Path
from typing import Annotated

import typer
from adit_radis_shared import cli_commands as commands
from adit_radis_shared import cli_helpers as helpers

helpers.PROJECT_ID = "adit"
helpers.ROOT_PATH = Path(__file__).resolve().parent

app = typer.Typer()

extra_args = {"allow_extra_args": True, "ignore_unknown_options": True}

app.command()(commands.init_workspace)
app.command()(commands.compose_up)
app.command()(commands.compose_down)
app.command()(commands.stack_deploy)
app.command()(commands.stack_rm)
app.command()(commands.lint)
app.command()(commands.format_code)
app.command(context_settings=extra_args)(commands.test)
app.command()(commands.show_outdated)
app.command()(commands.backup_db)
app.command()(commands.restore_db)
app.command()(commands.shell)
app.command()(commands.generate_certificate_files)
app.command()(commands.generate_certificate_chain)
app.command()(commands.generate_django_secret_key)
app.command()(commands.generate_secure_password)
app.command()(commands.generate_auth_token)
app.command()(commands.randomize_env_secrets)
app.command()(commands.try_github_actions)


@app.command()
def populate_orthancs(
reset: Annotated[bool, typer.Option(help="Do not build images")] = False,
simulate: Annotated[bool, typer.Option(help="Simulate the command")] = False,
):
"""Populate Orthancs with example DICOMs"""

cmd = f"{helpers.build_compose_cmd()} exec web python manage.py populate_orthancs"
if reset:
cmd += " --reset"

helpers.execute_cmd(cmd, simulate=simulate)


if __name__ == "__main__":
app()
2 changes: 1 addition & 1 deletion docs/Backups.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Backups

For database backups django-dbbackup app is used. The backups are done every night at 3 am by a periodic task using the dbbackup management command and stored in the `backups` directory which is mounted as a volume. The dbbackup command can also be called manually with `invoke backup_db`.
For database backups django-dbbackup app is used. The backups are done every night at 3 am by a periodic task using the dbbackup management command and stored in the `backups` directory which is mounted as a volume. The dbbackup command can also be called manually with `poetry run ./cli.py backup-db`.
4 changes: 2 additions & 2 deletions docs/Maintenance.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
There are different things that can be upgraded:

- The python package dependencies (normal dependencies and dev dependencies)
- Check outdated Python packages: `inv show-outdated` (check Python section in output)
- Check outdated Python packages: `poetry run ./cli.py show-outdated` (check Python section in output)
- `poetry update` will update packages according to their version range in `pyproject.toml`
- Other upgrades (e.g. major versions) must be upgraded by modifying the version range in `pyproject.toml` before calling `poetry update`
- Javascript dependencies
- Check outdated Javascript packages: `inv show-outdated` (check Javascript section in output)
- Check outdated Javascript packages: `poetry run ./cli.py show-outdated` (check Javascript section in output)
- `npm update` will update packages according to their version range in `package.json`
- Other upgrades (e.g. major versions) must be upgraded by modifying the version range in `packages.json` before calling `npm update`
- After an upgrade make sure the files in `static/vendor` still link to the correct files in `node_modules`1
Expand Down
2 changes: 1 addition & 1 deletion example.env
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ SITE_DOMAIN=localhost
# These variables are used to create a certificate key, self-signed certificate,
# and the corresponding certificate chain. If you have an existing certificate
# key and signed certificate from your CA, you can generate the corresponding
# certificate chain using 'invoke generate-certificate-chain'.
# certificate chain using 'poetry run ./cli.py generate-certificate-chain'.
SSL_HOSTNAME=localhost
SSL_IP_ADDRESSES=127.0.0.1
SSL_SERVER_CERT_FILE="./cert.pem"
Expand Down
Loading

0 comments on commit 37f2874

Please sign in to comment.