diff --git a/.github/workflows/medcat-demo-app_build.yml b/.github/workflows/medcat-demo-app_build.yml deleted file mode 100644 index 22be8f88b..000000000 --- a/.github/workflows/medcat-demo-app_build.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: medcat-demo-app - Test - -on: - push: - branches: [ main ] - pull_request: - paths: - - 'medcat-demo-app/**' - - 'medcat-v2/**' - - '.github/workflows/medcat-demo-app**' -defaults: - run: - working-directory: ./medcat-demo-app - -jobs: - integration-test: - runs-on: ubuntu-latest - env: - MEDCAT_BRANCH: ${{ github.head_ref || github.ref_name }} - - steps: - - uses: actions/checkout@v6 - - - name: ๐Ÿงน Free up disk space - run: | - df -h # Optional: Check space before build - # Remove large, unnecessary packages/tools - sudo rm -rf /usr/share/dotnet - sudo rm -rf /usr/local/lib/android - sudo rm -rf /usr/share/swift - sudo rm -rf /usr/share/rust - sudo rm -rf /usr/share/powershell - # Remove cached tools and files - sudo apt-get clean - df -h # Optional: Check space before build - - - name: Set up Docker Compose - run: sudo apt-get update && sudo apt-get install -y docker-compose - - - name: Make medact-v2 available within webapp - run: cp -r ../medcat-v2 webapp/medcat-v2 - - - name: Build and start containers - run: docker-compose -f docker-compose-test.yml up -d --build - - - name: Run integration test - run: ./tests/test_integration.sh - - - name: Check container logs for errors - run: | - docker-compose logs medcatweb - # NOTE: ignore line "Applying auth.0007_alter_validators_add_error_messages" - not an error - docker-compose logs medcatweb | grep -v "Applying auth.0007_alter_validators_add_error_messages" | grep -i 'error' && exit 1 || true - - - name: Tear down - run: docker-compose -f docker-compose-test.yml down diff --git a/.github/workflows/medcat-model-distributor_build.yml b/.github/workflows/medcat-model-distributor_build.yml new file mode 100644 index 000000000..cc53a382f --- /dev/null +++ b/.github/workflows/medcat-model-distributor_build.yml @@ -0,0 +1,107 @@ +name: medcat-model-distributor - Test + +permissions: + contents: read + +on: + push: + branches: [ main ] + tags: + - 'medcat-model-distributor/v*.*.*' + pull_request: + paths: + - 'medcat-model-distributor/**' + - 'medcat-v2/**' + - '.github/workflows/medcat-model-distributor**' +defaults: + run: + working-directory: ./medcat-model-distributor + +jobs: + integration-test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: ๐Ÿงน Free up disk space + run: | + df -h # Optional: Check space before build + # Remove large, unnecessary packages/tools + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/swift + sudo rm -rf /usr/share/rust + sudo rm -rf /usr/share/powershell + # Remove cached tools and files + sudo apt-get clean + df -h # Optional: Check space before build + + - name: Set up Docker Compose + run: sudo apt-get update && sudo apt-get install -y docker-compose + + - name: Build and start containers + run: docker-compose -f docker-compose-test.yml up -d --build + + - name: Run integration test + run: ./tests/test_integration.sh --model-file webapp/manage.py + + - name: Check container logs for errors + run: | + docker-compose logs medcatweb + # NOTE: ignore line "Applying auth.0007_alter_validators_add_error_messages" - not an error + docker-compose logs medcatweb | grep -v "Applying auth.0007_alter_validators_add_error_messages" | grep -i 'error' && exit 1 || true + + - name: Tear down + run: docker-compose -f docker-compose-test.yml down + + publish-to-docker-hub: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + needs: integration-test + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata (tags, labels) for Docker medcat-model-distributor + id: meta + uses: docker/metadata-action@v5 + with: + images: cogstacksystems/medcat-model-distributor + tags: | + # Include all default tags + type=schedule + type=ref,event=branch + type=ref,event=tag + type=ref,event=pr + type=sha + # Create version tag based on tag prefix + type=match,pattern=medcat-model-distributor/v(\d+\.\d+\.\d+),group=1 + flavor: latest=false + + - name: Build and push Docker medcat-model-distributor image + id: docker_build + uses: docker/build-push-action@v6 + with: + context: ./medcat-model-distributor/webapp + push: true + allow: network.host + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=cogstacksystems/medcat-model-distributor:buildcache + cache-to: type=registry,ref=cogstacksystems/medcat-model-distributor:buildcache,mode=max + build-args: | + REINSTALL_CORE_FROM_LOCAL=true + + - name: Image digest + run: echo ${{ steps.docker_build.outputs.digest }} + working-directory: "./" diff --git a/medcat-demo-app/tests/test_integration.py b/medcat-demo-app/tests/test_integration.py deleted file mode 100644 index 228b00192..000000000 --- a/medcat-demo-app/tests/test_integration.py +++ /dev/null @@ -1,33 +0,0 @@ -# tests/test_integration.py - -import requests - -from bs4 import BeautifulSoup - -URL = "http://localhost:8000/" -session = requests.Session() - -# GET the page to get the CSRF token -resp = session.get(URL) -soup = BeautifulSoup(resp.text, "html.parser") -csrf = soup.find("input", {"name": "csrfmiddlewaretoken"}).get("value") - -disease = "kidney failure" - -text = f"Patient had been diagnosed with acute {disease} the week before" - -# POST with the token and same session (cookies preserved) -resp = session.post(URL, data={ - "text": text, - "csrfmiddlewaretoken": csrf}) - -print(f"RESPOONSE:\n{resp.text}") - -soup = BeautifulSoup(resp.text, "html.parser") - -annotations = soup.select("div.entities mark.entity") -assert annotations, "No annotations found in the response" -assert any(disease in mark.text.lower() for mark in annotations), ( - f"Disease '{disease}' not found in annotations") - -assert disease in resp.text diff --git a/medcat-demo-app/tests/test_integration.sh b/medcat-demo-app/tests/test_integration.sh deleted file mode 100755 index f81c30a12..000000000 --- a/medcat-demo-app/tests/test_integration.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "Waiting for service to start..." - -# install bs4 for tests -python -m pip install beautifulsoup4 - -# Wait until curl doesn't fail -for i in {1..60}; do - if curl -s --fail http://localhost:8000 > /dev/null; then - echo "Service is up" - break - else - echo "Waiting ($i)..." - sleep 2 - fi -done - -python tests/test_integration.py diff --git a/medcat-demo-app/.gitignore b/medcat-model-distributor/.gitignore similarity index 100% rename from medcat-demo-app/.gitignore rename to medcat-model-distributor/.gitignore diff --git a/medcat-demo-app/README.md b/medcat-model-distributor/README.md similarity index 100% rename from medcat-demo-app/README.md rename to medcat-model-distributor/README.md diff --git a/medcat-demo-app/docker-compose-test.yml b/medcat-model-distributor/docker-compose-test.yml similarity index 84% rename from medcat-demo-app/docker-compose-test.yml rename to medcat-model-distributor/docker-compose-test.yml index 6b0882cd0..d1ce643de 100644 --- a/medcat-demo-app/docker-compose-test.yml +++ b/medcat-model-distributor/docker-compose-test.yml @@ -1,11 +1,7 @@ -version: '3.4' - services: medcatweb: build: context: ./webapp - args: - REINSTALL_CORE_FROM_LOCAL: "true" command: > bash -c " python manage.py migrate --noinput && diff --git a/medcat-demo-app/docker-compose.yml b/medcat-model-distributor/docker-compose.yml similarity index 87% rename from medcat-demo-app/docker-compose.yml rename to medcat-model-distributor/docker-compose.yml index d75168e24..c75f43111 100644 --- a/medcat-demo-app/docker-compose.yml +++ b/medcat-model-distributor/docker-compose.yml @@ -1,10 +1,6 @@ -version: '3.4' - services: medcatweb: - build: - network: host - context: ./webapp + image: cogstacksystems/medcat-model-distributor command: > bash -c " python manage.py migrate --noinput && diff --git a/medcat-demo-app/envs/env_db_backup b/medcat-model-distributor/envs/env_db_backup similarity index 100% rename from medcat-demo-app/envs/env_db_backup rename to medcat-model-distributor/envs/env_db_backup diff --git a/medcat-demo-app/envs/env_medmen b/medcat-model-distributor/envs/env_medmen similarity index 100% rename from medcat-demo-app/envs/env_medmen rename to medcat-model-distributor/envs/env_medmen diff --git a/medcat-demo-app/envs/env_medmen_test b/medcat-model-distributor/envs/env_medmen_test similarity index 100% rename from medcat-demo-app/envs/env_medmen_test rename to medcat-model-distributor/envs/env_medmen_test diff --git a/medcat-model-distributor/tests/setup/setup_in_container.py b/medcat-model-distributor/tests/setup/setup_in_container.py new file mode 100644 index 000000000..1b597117e --- /dev/null +++ b/medcat-model-distributor/tests/setup/setup_in_container.py @@ -0,0 +1,53 @@ +import sys, os, secrets +import django +from django.core.files import File +from django.utils import timezone +from datetime import timedelta + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "webapp.settings") +django.setup() + +# โ”€โ”€ Import models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +try: + from demo.models import MedcatModel, APIKey +except ImportError as e: + print(f"[SEED] ImportError: {e}", file=sys.stderr) + sys.exit(1) + +def main(model_path: str, model_name: str, model_display_name: str, + model_description: str, api_key_identifier: str): + # โ”€โ”€ MedcatModel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if not os.path.exists(model_path): + print(f"[SEED] ERROR: model file not found in container: {model_path}", file=sys.stderr) + sys.exit(1) + + obj, created = MedcatModel.objects.get_or_create( + model_name=model_name, + defaults={ + "model_display_name": model_display_name, + "model_description": model_description, + }, + ) + if created or not obj.model_file: + with open(model_path, "rb") as f: + obj.model_file.save(os.path.basename(model_path), File(f), save=True) + print(f"[SEED] MedcatModel created: {obj.model_name}", file=sys.stderr) + else: + print(f"[SEED] MedcatModel already exists: {obj.model_name}", file=sys.stderr) + + # โ”€โ”€ APIKey โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + key_value = secrets.token_hex(32) # 64-char hex, fits max_length=64 + APIKey.objects.create( + key=key_value, + identifier=api_key_identifier, + expires_at=timezone.now() + timedelta(hours=1), + is_active=True, + ) + print(f"[SEED] APIKey created for identifier: {api_key_identifier}", file=sys.stderr) + + # Print ONLY the key to stdout so the shell can capture it cleanly + print(key_value) + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/medcat-model-distributor/tests/test_integration.sh b/medcat-model-distributor/tests/test_integration.sh new file mode 100755 index 000000000..9b48ee8d8 --- /dev/null +++ b/medcat-model-distributor/tests/test_integration.sh @@ -0,0 +1,183 @@ +#!/usr/bin/env bash +# ============================================================================= +# Integration test: build โ†’ seed DB โ†’ call auth-callback-api โ†’ verify +# ============================================================================= +# Usage: +# ./integration_test.sh --model-file /path/to/model.zip [OPTIONS] +# +# Options: +# --model-file PATH (required) Path to the MedcatModel file on the HOST +# --model-name NAME Model name stored in DB (default: test_model) +# --model-display NAME Model display name (default: Test Model) +# --model-desc TEXT Model description (default: Integration test model) +# --api-key-id IDENTIFIER APIKey identifier label (default: integration-test) +# --compose-file PATH Path to docker-compose file (default: docker-compose.yml) +# --service NAME Django service name in compose file (default: web) +# --port PORT Host port the app is mapped to (default: 8000) +# --base-url URL Override full base URL (default: http://localhost:PORT) +# --app-label LABEL Django app label for model imports (default: yourapp) +# --keep-up Don't tear down containers after the test +# --help Show this message +# ============================================================================= +set -euo pipefail + +# โ”€โ”€ Defaults โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +MODEL_FILE="" +MODEL_NAME="test_model" +MODEL_DISPLAY_NAME="Test Model" +MODEL_DESCRIPTION="Integration test model" +API_KEY_IDENTIFIER="integration-test" +COMPOSE_FILE="docker-compose-test.yml" +SERVICE="medcatweb" +PORT="8000" +BASE_URL="" +KEEP_UP=false +WORKDIR="/webapp" + +# โ”€โ”€ Colour helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BOLD='\033[1m'; NC='\033[0m' +info() { echo -e "${GREEN}[INFO]${NC} $*"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } +error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } +die() { error "$*"; exit 1; } +step() { echo -e "\n${BOLD}=== $* ===${NC}"; } + +# โ”€โ”€ Argument parsing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +while [[ $# -gt 0 ]]; do + case "$1" in + --model-file) MODEL_FILE="$2"; shift 2 ;; + --model-name) MODEL_NAME="$2"; shift 2 ;; + --model-display) MODEL_DISPLAY_NAME="$2"; shift 2 ;; + --model-desc) MODEL_DESCRIPTION="$2"; shift 2 ;; + --api-key-id) API_KEY_IDENTIFIER="$2"; shift 2 ;; + --compose-file) COMPOSE_FILE="$2"; shift 2 ;; + --service) SERVICE="$2"; shift 2 ;; + --port) PORT="$2"; shift 2 ;; + --base-url) BASE_URL="$2"; shift 2 ;; + --keep-up) KEEP_UP=true; shift ;; + --help) + sed -n '/^# Usage:/,/^# =====/p' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) die "Unknown argument: $1" ;; + esac +done + +# โ”€โ”€ Validate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +[[ -z "$MODEL_FILE" ]] && die "--model-file is required" +[[ ! -f "$MODEL_FILE" ]] && die "Model file not found: $MODEL_FILE" +[[ ! -f "$COMPOSE_FILE" ]] && die "Compose file not found: $COMPOSE_FILE" + +MODEL_FILE_ABS="$(realpath "$MODEL_FILE")" +MODEL_FILE_DIR="$(dirname "$MODEL_FILE_ABS")" +MODEL_FILE_BASENAME="$(basename "$MODEL_FILE_ABS")" +[[ -z "$BASE_URL" ]] && BASE_URL="http://localhost:${PORT}" + +info "Model file : $MODEL_FILE_ABS" +info "Compose file: $COMPOSE_FILE (service: $SERVICE)" +info "Base URL : $BASE_URL" + +# โ”€โ”€ Cleanup trap โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +cleanup() { + local exit_code=$? + if [[ "$KEEP_UP" == false ]]; then + info "Tearing down containers..." + docker compose -f "$COMPOSE_FILE" down -v --remove-orphans 2>/dev/null || true + else + warn "--keep-up specified; containers left running." + fi + if [[ $exit_code -ne 0 ]]; then + echo -e "\n${RED}${BOLD}Integration test FAILED โœ—${NC}" + fi + exit $exit_code +} +trap cleanup EXIT + +# ============================================================================= +# STEP 1 โ€“ Build and start the stack +# ============================================================================= +step "STEP 1 โ€“ Build & start containers" + +docker compose -f "$COMPOSE_FILE" up -d --build + +# Wait until the health endpoint (or root) responds with any HTTP status. +info "Waiting for app to be reachable at ${BASE_URL}/health (max 90s)..." +MAX_WAIT=90; WAITED=0 +until curl -sf --max-time 3 "${BASE_URL}/health" -o /dev/null 2>/dev/null; do + if [[ $WAITED -ge $MAX_WAIT ]]; then + error "App did not become ready within ${MAX_WAIT}s. Service logs:" + docker compose -f "$COMPOSE_FILE" logs --tail 50 "$SERVICE" + die "Aborting." + fi + sleep 3; WAITED=$((WAITED + 3)) +done +info "App is ready (${WAITED}s elapsed)" + +# ============================================================================= +# STEP 2 โ€“ Find the model file inside the container +# ============================================================================= +step "STEP 2 โ€“ Locate model file inside container" + +# Attempt auto-detection +DETECTED_PATH="$(docker compose -f "$COMPOSE_FILE" exec -T "$SERVICE" find / -name "$MODEL_FILE_BASENAME" -not -path "/proc/*" -not -path "/sys/*" -print -quit 2>/dev/null | tr -d '\r')" + +if [[ -n "$DETECTED_PATH" ]]; then + CONTAINER_MODEL_PATH="$DETECTED_PATH" +else + # This is the crucial part: Hardcode where your app lives in the Docker image + # Assuming your Dockerfile puts the code in /webapp + warn "Auto-detection failed. Using known container path..." + CONTAINER_MODEL_PATH="/webapp/$MODEL_FILE_BASENAME" +fi + +info "Container model path: ${CONTAINER_MODEL_PATH}" + +# ============================================================================= +# STEP 3 โ€“ Seed: MedcatModel + APIKey via manage.py shell +# ============================================================================= +step "STEP 3 โ€“ Seed database" + +# get container ID +CONTAINER=$(docker compose -f "$COMPOSE_FILE" ps -q "$SERVICE") + +# copy setup file to container +docker cp tests/setup/setup_in_container.py "$CONTAINER":$WORKDIR/seed_script.py + +# We capture stdout only for the API key; all other output goes to stderr. +API_KEY_VALUE="$( + docker compose -f "$COMPOSE_FILE" exec -T "$SERVICE" \ + python3 $WORKDIR/seed_script.py "$CONTAINER_MODEL_PATH" "$MODEL_NAME" "$MODEL_DISPLAY_NAME" "$MODEL_DESCRIPTION" "$API_KEY_IDENTIFIER" +)" + +[[ -z "$API_KEY_VALUE" ]] && die "could not get API key value" +info "APIKey value (first 10 chars): ${API_KEY_VALUE:0:10}..." + +# ============================================================================= +# STEP 4 โ€“ Call the protected endpoint +# ============================================================================= +step "STEP 4 โ€“ Call manual-api-callback" + +AUTH_URL="${BASE_URL}/manual-api-callback/?api_key=${API_KEY_VALUE}" +info "GET ${AUTH_URL}" + +# -D - : dump response headers to stdout; we parse status from them +HTTP_RESPONSE_FILE="$(mktemp)" +HTTP_STATUS="$( + curl -s -o "$HTTP_RESPONSE_FILE" \ + -w "%{http_code}" \ + --max-time 15 \ + "${AUTH_URL}" +)" +HTTP_BODY="$(cat "$HTTP_RESPONSE_FILE")" +rm -f "$HTTP_RESPONSE_FILE" + +info "HTTP status: ${HTTP_STATUS}" + +# ============================================================================= +# STEP 5 โ€“ Verify the response +# ============================================================================= +step "STEP 5 โ€“ Verify response" + +python3 tests/verification/verify_response.py "${HTTP_STATUS}" "${MODEL_DISPLAY_NAME}" "${HTTP_BODY}" + +echo -e "\n${GREEN}${BOLD}Integration test PASSED โœ“${NC}" \ No newline at end of file diff --git a/medcat-model-distributor/tests/verification/verify_response.py b/medcat-model-distributor/tests/verification/verify_response.py new file mode 100644 index 000000000..4b46dc578 --- /dev/null +++ b/medcat-model-distributor/tests/verification/verify_response.py @@ -0,0 +1,44 @@ +import sys + + +def main(status: str, model_display: str, body: str): + + errors = [] + + # 1. HTTP 200 + if status != "200": + errors.append(f"Expected HTTP 200, got {status}") + + # 2. Not an API-key rejection JSON + if '"error"' in body and "API key" in body: + errors.append("Response looks like an API key rejection (JSON error body returned)") + + # 3. Model display name present in the rendered page + if model_display not in body: + errors.append(f"Model display name '{model_display}' not found in page body") + + # 4. No Django error page + if "Exception Value" in body or "Traceback (most recent call last)" in body: + errors.append("Django error page / traceback detected in response body") + + # 5. Valid-key message present (from the view context message) + if "Manually obtained API key is being used" not in body: + errors.append("Expected validity message ('Manually obtained API key is being used') not in body") + + # โ”€โ”€ Report โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + if errors: + print("\n[FAIL] Verification FAILED:") + for e in errors: + print(f" โœ— {e}") + sys.exit(1) + else: + print("\n[PASS] All checks passed:") + print(f" โœ“ HTTP 200 received") + print(f" โœ“ No API-key rejection in body") + print(f" โœ“ Model '{model_display}' listed on page") + print(f" โœ“ No Django error page") + print(f" โœ“ Valid-key message present") + + +if __name__ == "__main__": + main(*sys.argv[1:]) diff --git a/medcat-demo-app/webapp/.dockerignore b/medcat-model-distributor/webapp/.dockerignore similarity index 100% rename from medcat-demo-app/webapp/.dockerignore rename to medcat-model-distributor/webapp/.dockerignore diff --git a/medcat-demo-app/webapp/Dockerfile b/medcat-model-distributor/webapp/Dockerfile similarity index 61% rename from medcat-demo-app/webapp/Dockerfile rename to medcat-model-distributor/webapp/Dockerfile index e8c0567b2..416a5974c 100644 --- a/medcat-demo-app/webapp/Dockerfile +++ b/medcat-model-distributor/webapp/Dockerfile @@ -17,24 +17,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libsqlite3-dev -# Install Python dependencies -ARG USE_CPU_TORCH=false -# NOTE: Allow building without GPU so as to lower image size (disabled by default) -RUN pip install -U pip && if [ "$USE_CPU_TORCH" = "true" ]; then \ - pip install -r /webapp/requirements.txt --extra-index-url https://download.pytorch.org/whl/cpu/; \ - else \ - pip install -r /webapp/requirements.txt; \ - fi - -# NOTE: for test/CI time, this will install medcat based on local path -ARG REINSTALL_CORE_FROM_LOCAL=false -RUN if [ "$REINSTALL_CORE_FROM_LOCAL" = "true" ]; then \ - echo "Reinstalling medcat based on relative path"; \ - SETUPTOOLS_SCM_PRETEND_VERSION="2.4.0-dev0" pip install -e "/webapp/medcat-v2[meta-cat,spacy]"; \ - fi - -# Get the spacy model (for later copy) -RUN python -m spacy download en_core_web_md +RUN pip install -U pip && pip install -r /webapp/requirements.txt # Stage 2: Final (production) image FROM python:3.12-slim AS final diff --git a/medcat-demo-app/webapp/data/.keep b/medcat-model-distributor/webapp/data/.keep similarity index 100% rename from medcat-demo-app/webapp/data/.keep rename to medcat-model-distributor/webapp/data/.keep diff --git a/medcat-demo-app/webapp/db/.keep b/medcat-model-distributor/webapp/db/.keep similarity index 100% rename from medcat-demo-app/webapp/db/.keep rename to medcat-model-distributor/webapp/db/.keep diff --git a/medcat-demo-app/webapp/demo/__init__.py b/medcat-model-distributor/webapp/demo/__init__.py similarity index 100% rename from medcat-demo-app/webapp/demo/__init__.py rename to medcat-model-distributor/webapp/demo/__init__.py diff --git a/medcat-demo-app/webapp/demo/admin.py b/medcat-model-distributor/webapp/demo/admin.py similarity index 90% rename from medcat-demo-app/webapp/demo/admin.py rename to medcat-model-distributor/webapp/demo/admin.py index f936542dc..9b6627b63 100644 --- a/medcat-demo-app/webapp/demo/admin.py +++ b/medcat-model-distributor/webapp/demo/admin.py @@ -7,14 +7,6 @@ admin.site.register(Downloader) admin.site.register(MedcatModel) -def remove_text(modeladmin, request, queryset): - UploadedText.objects.all().delete() - - -class UploadedTextAdmin(admin.ModelAdmin): - model = UploadedText - actions = [remove_text] - class APIKeyAdmin(admin.ModelAdmin): list_display = ('key_short', 'identifier', 'created_at', 'expires_at', 'is_active', 'is_expired') @@ -66,5 +58,4 @@ def api_key_link(self, obj: APIKey): # Register your models here. -admin.site.register(UploadedText, UploadedTextAdmin) admin.site.register(APIKey, APIKeyAdmin) diff --git a/medcat-demo-app/webapp/demo/apps.py b/medcat-model-distributor/webapp/demo/apps.py similarity index 100% rename from medcat-demo-app/webapp/demo/apps.py rename to medcat-model-distributor/webapp/demo/apps.py diff --git a/medcat-demo-app/webapp/demo/db_backup.py b/medcat-model-distributor/webapp/demo/db_backup.py similarity index 100% rename from medcat-demo-app/webapp/demo/db_backup.py rename to medcat-model-distributor/webapp/demo/db_backup.py diff --git a/medcat-demo-app/webapp/demo/decorators.py b/medcat-model-distributor/webapp/demo/decorators.py similarity index 97% rename from medcat-demo-app/webapp/demo/decorators.py rename to medcat-model-distributor/webapp/demo/decorators.py index 5de46c3d7..d4f892fb2 100644 --- a/medcat-demo-app/webapp/demo/decorators.py +++ b/medcat-model-distributor/webapp/demo/decorators.py @@ -6,7 +6,7 @@ def require_valid_api_key(view_func): """ Decorator to protect endpoints with API key authentication - + Usage: @require_valid_api_key def my_protected_view(request): @@ -21,22 +21,22 @@ def wrapper(request, *args, **kwargs): request.GET.get('api_key') or request.POST.get('api_key') ) - + if not api_key: return JsonResponse({ 'error': 'API key required', 'message': 'Please provide an API key via X-API-Key header or api_key parameter' }, status=401) - + if not APIKey.is_valid(api_key): return JsonResponse({ 'error': 'Invalid or expired API key', 'message': 'Please obtain a valid API key' }, status=401) - + # API key is valid, proceed with the view return view_func(request, *args, **kwargs) - + return wrapper diff --git a/medcat-demo-app/webapp/demo/forms.py b/medcat-model-distributor/webapp/demo/forms.py similarity index 100% rename from medcat-demo-app/webapp/demo/forms.py rename to medcat-model-distributor/webapp/demo/forms.py diff --git a/medcat-demo-app/webapp/demo/migrations/0001_initial.py b/medcat-model-distributor/webapp/demo/migrations/0001_initial.py similarity index 100% rename from medcat-demo-app/webapp/demo/migrations/0001_initial.py rename to medcat-model-distributor/webapp/demo/migrations/0001_initial.py diff --git a/medcat-demo-app/webapp/demo/migrations/0002_downloader_medcatmodel.py b/medcat-model-distributor/webapp/demo/migrations/0002_downloader_medcatmodel.py similarity index 100% rename from medcat-demo-app/webapp/demo/migrations/0002_downloader_medcatmodel.py rename to medcat-model-distributor/webapp/demo/migrations/0002_downloader_medcatmodel.py diff --git a/medcat-demo-app/webapp/demo/migrations/0003_auto_20251120_2221.py b/medcat-model-distributor/webapp/demo/migrations/0003_auto_20251120_2221.py similarity index 100% rename from medcat-demo-app/webapp/demo/migrations/0003_auto_20251120_2221.py rename to medcat-model-distributor/webapp/demo/migrations/0003_auto_20251120_2221.py diff --git a/medcat-demo-app/webapp/demo/migrations/0004_alter_apikey_expires_at.py b/medcat-model-distributor/webapp/demo/migrations/0004_alter_apikey_expires_at.py similarity index 100% rename from medcat-demo-app/webapp/demo/migrations/0004_alter_apikey_expires_at.py rename to medcat-model-distributor/webapp/demo/migrations/0004_alter_apikey_expires_at.py diff --git a/medcat-model-distributor/webapp/demo/migrations/0005_delete_uploadedtext.py b/medcat-model-distributor/webapp/demo/migrations/0005_delete_uploadedtext.py new file mode 100644 index 000000000..67573f71b --- /dev/null +++ b/medcat-model-distributor/webapp/demo/migrations/0005_delete_uploadedtext.py @@ -0,0 +1,16 @@ +# Generated by Django 3.2.25 on 2026-02-24 16:03 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('demo', '0004_alter_apikey_expires_at'), + ] + + operations = [ + migrations.DeleteModel( + name='UploadedText', + ), + ] diff --git a/medcat-demo-app/webapp/demo/migrations/__init__.py b/medcat-model-distributor/webapp/demo/migrations/__init__.py similarity index 100% rename from medcat-demo-app/webapp/demo/migrations/__init__.py rename to medcat-model-distributor/webapp/demo/migrations/__init__.py diff --git a/medcat-demo-app/webapp/demo/models.py b/medcat-model-distributor/webapp/demo/models.py similarity index 92% rename from medcat-demo-app/webapp/demo/models.py rename to medcat-model-distributor/webapp/demo/models.py index ee0793291..44a95886c 100644 --- a/medcat-demo-app/webapp/demo/models.py +++ b/medcat-model-distributor/webapp/demo/models.py @@ -13,12 +13,6 @@ cooldown_days = 14 -# Create your models here. -class UploadedText(models.Model): - text = models.TextField(default="", blank=True) - create_time = models.DateTimeField(auto_now_add=True) - - class Downloader(models.Model): first_name = models.CharField(max_length=20) last_name = models.CharField(max_length=20) diff --git a/medcat-demo-app/webapp/demo/static/css/annotations.css b/medcat-model-distributor/webapp/demo/static/css/annotations.css similarity index 100% rename from medcat-demo-app/webapp/demo/static/css/annotations.css rename to medcat-model-distributor/webapp/demo/static/css/annotations.css diff --git a/medcat-demo-app/webapp/demo/static/css/base.css b/medcat-model-distributor/webapp/demo/static/css/base.css similarity index 100% rename from medcat-demo-app/webapp/demo/static/css/base.css rename to medcat-model-distributor/webapp/demo/static/css/base.css diff --git a/medcat-demo-app/webapp/demo/static/css/home.css b/medcat-model-distributor/webapp/demo/static/css/home.css similarity index 100% rename from medcat-demo-app/webapp/demo/static/css/home.css rename to medcat-model-distributor/webapp/demo/static/css/home.css diff --git a/medcat-demo-app/webapp/demo/static/image/favicon.ico b/medcat-model-distributor/webapp/demo/static/image/favicon.ico similarity index 100% rename from medcat-demo-app/webapp/demo/static/image/favicon.ico rename to medcat-model-distributor/webapp/demo/static/image/favicon.ico diff --git a/medcat-demo-app/webapp/demo/static/js/.keep b/medcat-model-distributor/webapp/demo/static/js/.keep similarity index 100% rename from medcat-demo-app/webapp/demo/static/js/.keep rename to medcat-model-distributor/webapp/demo/static/js/.keep diff --git a/medcat-demo-app/webapp/demo/static/js/anns.js b/medcat-model-distributor/webapp/demo/static/js/anns.js similarity index 100% rename from medcat-demo-app/webapp/demo/static/js/anns.js rename to medcat-model-distributor/webapp/demo/static/js/anns.js diff --git a/medcat-demo-app/webapp/demo/templates/base.html b/medcat-model-distributor/webapp/demo/templates/base.html similarity index 94% rename from medcat-demo-app/webapp/demo/templates/base.html rename to medcat-model-distributor/webapp/demo/templates/base.html index 1c37e4fc8..1abf3e75c 100644 --- a/medcat-demo-app/webapp/demo/templates/base.html +++ b/medcat-model-distributor/webapp/demo/templates/base.html @@ -15,7 +15,7 @@