From 285c3d5c4dc90b007105128b86f3ad632c3157ce Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 5 May 2026 01:32:33 +0000 Subject: [PATCH 1/2] Address PR #1500 review follow-ups for slim production image (#1505) Six review observations on the slim-production-image PR are addressed without altering deployed runtime semantics: - Pin Python to 3.11.15-slim-bookworm in both Dockerfiles so rebuilds on different dates do not silently pick up a new CPython point release. - Mirror local's pinned-URL spaCy install in production: download en_core_web_sm/lg via wget into the build-stage wheel cache, install via the existing pip --no-index step, and replace the runtime `spacy download` calls with a `spacy.load(...)` smoke test. wget stays out of the runtime image. - Split the release workflow's Django path into build+load -> size budget gate -> docker push, removing the load+push co-existence in a single build-push-action step. Non-Django images keep the original combined path. - Document why locale/ is safely excluded from the build context (no .po/.mo files exist, no startup script invokes compilemessages) and clarify gettext's role in the runtime apt comment. - Document the brotli >=1.1.0 floor rationale in requirements/base.txt so the constraint is no longer ambiguous. - Verify ffmpeg has zero remaining consumers across the codebase. Closes #1505 --- .dockerignore | 6 +++ .github/workflows/docker-build-release.yml | 45 ++++++++++++++++++++-- CHANGELOG.md | 10 +++++ compose/local/django/Dockerfile | 5 ++- compose/production/django/Dockerfile | 40 ++++++++++++++++--- requirements/base.txt | 2 +- 6 files changed, 97 insertions(+), 11 deletions(-) diff --git a/.dockerignore b/.dockerignore index c2ac49491c..952c331c09 100644 --- a/.dockerignore +++ b/.dockerignore @@ -38,6 +38,12 @@ schema.graphql cloudflare-og-worker/ tools/ fixtures/ +# locale/ is safe to exclude: no .po/.mo files exist in the tree (only a +# placeholder README.rst) and no entrypoint/start script invokes +# `manage.py compilemessages`. Django translations are intentionally not +# active today; if .po files are added later, drop this entry so the build +# context picks them up (the runtime image keeps `gettext` available for +# `compilemessages`). locale/ model_preloaders/ diff --git a/.github/workflows/docker-build-release.yml b/.github/workflows/docker-build-release.yml index d7021e1dbb..abaede4932 100644 --- a/.github/workflows/docker-build-release.yml +++ b/.github/workflows/docker-build-release.yml @@ -56,19 +56,26 @@ jobs: type=semver,pattern={{major}} type=sha,prefix={{tag}}- - - name: Build and push Docker image - id: build + # Django gets a two-step build: first build+load locally so the image + # is on the daemon for the size-budget gate, then `docker push` each + # tag separately. This is split intentionally — combining `load: true` + # with `push: true` in a single `docker/build-push-action` step works + # only on single-platform builds, is not well-documented across Buildx + # versions, and some runner/daemon configurations reject it. Splitting + # makes the contract explicit and resilient to future runner changes. + - name: Build Django image (load locally for size budget gate) + if: matrix.image == 'django' uses: docker/build-push-action@v7 with: context: ${{ matrix.context }} file: ${{ matrix.dockerfile }} - push: true + push: false + load: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max platforms: linux/amd64 - load: ${{ matrix.image == 'django' }} build-args: | BUILD_ENVIRONMENT=production @@ -94,3 +101,33 @@ jobs: echo "::error::Django image $human_size exceeds the $human_budget budget. See issue #1494." exit 1 fi + + # Push only after the size gate passes. Each tag in steps.meta.outputs.tags + # already points at the image loaded above (same reference Buildx tagged + # locally), so `docker push ` re-uses the local layers — no rebuild. + - name: Push Django image to registry + if: matrix.image == 'django' + run: | + set -euo pipefail + while IFS= read -r tag; do + [ -z "$tag" ] && continue + echo "Pushing $tag" + docker push "$tag" + done <<< "${{ steps.meta.outputs.tags }}" + + # Non-Django images do not have a size budget today, so the original + # combined build-and-push path stays — no `load` involved. + - name: Build and push non-Django image + if: matrix.image != 'django' + uses: docker/build-push-action@v7 + with: + context: ${{ matrix.context }} + file: ${{ matrix.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + build-args: | + BUILD_ENVIRONMENT=production diff --git a/CHANGELOG.md b/CHANGELOG.md index 4faaee9e21..5a6cd461e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- **Docker image hardening follow-ups to PR #1500** (issue #1505). Six review observations on the slim-production-image PR are addressed without altering the deployed runtime semantics: + - **Python pinned to `3.11.15-slim-bookworm`** in both `compose/production/django/Dockerfile` and `compose/local/django/Dockerfile` (was the floating `3.11-slim-bookworm` alias). Rebuilds on different dates can no longer silently pick up a new CPython point release that changes runtime behaviour. Bumps are now an intentional edit, not a side effect of the calendar. + - **spaCy model install switched to the pinned-URL `wget` pattern in production** (`compose/production/django/Dockerfile`). The runtime stage previously called `python -m spacy download en_core_web_sm` / `en_core_web_lg`, which resolves the wheel URL at build time and could silently drift to a different patch. The production build stage now downloads the same explicit GitHub release URLs already used in `compose/local/django/Dockerfile` into `/usr/src/app/wheels`, with the same retry semantics; the runtime stage installs them via the existing `pip install --no-index --find-links=/wheels/` path and runs a `spacy.load(...)` smoke test. `wget` is added to the build stage only — it never reaches the runtime image. + - **Release workflow split for the Django image** (`.github/workflows/docker-build-release.yml`). The previous step combined `load: true` (so the size-budget gate could `docker image inspect` the result) with `push: true`. That combination only works on single-platform builds, is not well-documented across Buildx versions, and some runner/daemon configurations reject it. The Django image is now built with `load: true, push: false`, gated on the 1.5 GiB size budget, then pushed via `docker push ` per tag — the daemon already has the layers from the load step, so no rebuild occurs. Non-Django images keep the original combined `push: true, load: false` path. + - **`.dockerignore` documents why `locale/` is safely excluded** — the tree contains no `.po`/`.mo` files (only a placeholder `README.rst`) and no `compose/*/start*` script invokes `manage.py compilemessages`. The runtime image still installs `gettext` so future i18n work has a build hook ready; if `.po` files are added, drop the `locale/` line so the build context picks them up. + - **`brotli>=1.1.0` floor rationale documented** in `requirements/base.txt`. The constraint was not chosen incidentally — 1.1.0 is the current major-version line under active upstream maintenance. The underlying `_brotli` C-extension API that `httpx` looks up has been stable since 1.0.x, so a future transitive constraint can lower the floor safely if needed. + - **`ffmpeg` removal verified** by an exhaustive `grep` across `opencontractserver/`, `compose/`, `config/`, `frontend/`, and the workflow tree: no Python `import`, `subprocess`, or shell invocation of `ffmpeg` exists. The PR #1500 removal of the apt package is safe; this entry records the verification trail rather than re-removing anything. + ### Fixed - **`PipelineSettings.get_parser_kwargs` now merges encrypted secrets** (`opencontractserver/documents/models.py:1260`, issue #1515). The runtime path that resolves parser configuration in `opencontractserver/tasks/doc_tasks.py:424` previously returned only `parser_kwargs[parser_class_path]` and silently dropped any secret stored under `encrypted_secrets[parser_class_path]`. Parsers like `LlamaParseParser` whose API key lives only in `encrypted_secrets` (the documented secure location) were invoked with an empty `api_key=""` placeholder and failed with `"LlamaParse API key not configured"`. Fix: `get_parser_kwargs` now overlays decrypted `encrypted_secrets[parser_class_path]` on top of `parser_kwargs[parser_class_path]`, with secrets winning on key collision. Operators may keep `parser_kwargs.api_key = ""` as a schema marker without clobbering the real secret. The merged dict is built fresh on every call so a long-lived `PipelineSettings` reference does not retain decrypted secrets in memory. Regression tests in `opencontractserver/tests/test_pipeline_settings.py` cover merge, secret-wins, secret-only, no-mutation, and log-redaction. diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile index 3c1f51e40b..03f5bfc464 100644 --- a/compose/local/django/Dockerfile +++ b/compose/local/django/Dockerfile @@ -1,4 +1,7 @@ -ARG PYTHON_VERSION=3.11-slim-bookworm +# Pin to a specific patch (not the floating 3.11 alias) so rebuilds on +# different dates do not silently pick up a new CPython point release. +# Keep this in sync with compose/production/django/Dockerfile. +ARG PYTHON_VERSION=3.11.15-slim-bookworm # ---------------------------------------------------------------------------- # # Build stage: full toolchain for compiling wheels (psycopg2, opencv, etc.) # diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile index d5d4b24a3a..d2f1b2019f 100644 --- a/compose/production/django/Dockerfile +++ b/compose/production/django/Dockerfile @@ -1,4 +1,8 @@ -ARG PYTHON_VERSION=3.11-slim-bookworm +# Pin to a specific patch (not the floating 3.11 alias) so rebuilds on +# different dates do not silently pick up a new CPython point release that +# could change runtime behaviour. Bump deliberately when upstream ships a +# CVE/feature release and CI is green against the new patch. +ARG PYTHON_VERSION=3.11.15-slim-bookworm # ---------------------------------------------------------------------------- # # Build stage: full toolchain for compiling wheels (psycopg2, opencv, etc.) # @@ -18,6 +22,9 @@ ENV PIP_RETRIES=10 \ PIP_NO_CACHE_DIR=1 # Build-only system dependencies. None of these end up in the runtime image. +# `wget` is needed to fetch the large spaCy model wheel with explicit retry +# semantics (pip's retry can fall short on a multi-hundred-MB wheel over a +# transient SSL drop). RUN apt-get update && apt-get install --no-install-recommends -y \ build-essential \ cmake \ @@ -32,6 +39,7 @@ RUN apt-get update && apt-get install --no-install-recommends -y \ libtesseract-dev \ libpq-dev \ git \ + wget \ && rm -rf /var/lib/apt/lists/* # Copy the entire requirements directory for recursive building @@ -46,6 +54,20 @@ RUN (find ./requirements -mindepth 2 -type f -name "*.txt" -print; \ | sed 's/^/-r /' \ | xargs -r pip wheel --wheel-dir /usr/src/app/wheels +# Fetch pinned spaCy model wheels into the same wheel cache so the runtime +# stage installs them via the existing `pip install --no-index` step and +# `wget` never reaches the runtime image. Pinning the exact wheel URL is +# more reproducible than `python -m spacy download`, which resolves the +# wheel URL at build time and could silently drift to a different patch. +# `wget` retry args mirror compose/local/django/Dockerfile so the two paths +# behave the same on flaky networks. +RUN wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 \ + -P /usr/src/app/wheels \ + https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \ + && wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 \ + -P /usr/src/app/wheels \ + https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.8.0/en_core_web_lg-3.8.0-py3-none-any.whl + # ---------------------------------------------------------------------------- # # Runtime stage: slim base, runtime libs only. # # ---------------------------------------------------------------------------- # @@ -70,7 +92,12 @@ RUN groupadd --system django \ # Runtime system dependencies. Keep this list minimal — every package shows # up in the final image. # libpq5 : psycopg2 (binary protocol to Postgres) -# gettext : Django translations (compilemessages) +# gettext : retained as a build hook for `manage.py +# compilemessages`. No `.po`/`.mo` files ship today +# (`locale/` is in `.dockerignore`), so this is dead +# weight at runtime — kept so future i18n work does +# not need a Dockerfile change. Drop if image size +# ever needs the ~5 MB. # poppler-utils : pdf2image (pdftoppm/pdftocairo) # tesseract-ocr* : pytesseract OCR runtime # libgl1, libsm6, : opencv-python-headless transitive runtime deps @@ -104,9 +131,12 @@ RUN pip install --upgrade pip \ # build time but immediate at first request). RUN python -c "import cv2; print(f'cv2 {cv2.__version__} import OK')" -# Spacy models used by TxtParser sentence chunker. -RUN python -m spacy download en_core_web_sm \ - && python -m spacy download en_core_web_lg +# Smoke test: spaCy models load. The wheels were installed alongside other +# dependencies via the build-stage wheel cache (see the pinned-URL `wget` +# block above). `spacy.load(...)` exercises the same code path used by +# `TxtParser`'s sentence chunker so a missing-or-corrupt model fails the +# build immediately rather than at first request. +RUN python -c "import spacy; spacy.load('en_core_web_sm'); spacy.load('en_core_web_lg'); print('spaCy models load OK')" COPY --chown=django:django ./compose/production/django/entrypoint /entrypoint RUN sed -i 's/\r$//g' /entrypoint && chmod +x /entrypoint diff --git a/requirements/base.txt b/requirements/base.txt index ce2b4945a7..e12cb90b17 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -12,7 +12,7 @@ pydantic==2.* typing-extensions==4.* # https://github.com/python/typing_extensions requests>=2.33.1,<3 # https://requests.readthedocs.io/en/latest/ httpx>=0.28.1,<1 # https://github.com/encode/httpx - async HTTP for agent tools -brotli>=1.1.0 # decoder for httpx Content-Encoding: br responses (e.g. OpenAI API, VCR cassettes) +brotli>=1.1.0 # decoder for httpx Content-Encoding: br responses (e.g. OpenAI API, VCR cassettes). Floor is 1.1.0 because that is the current major-version line (released 2023-09) under active upstream maintenance; the underlying _brotli C-extension API httpx looks up has been stable since 1.0.x, so loosening the floor is safe if a transitive constraint demands it. # Django From e8a7db7c2de17b07499db6a97790528b7236800b Mon Sep 17 00:00:00 2001 From: JSv4 Date: Mon, 4 May 2026 23:02:41 -0500 Subject: [PATCH 2/2] Address review: add spaCy wheel SHA-256 verification, confirm size-gate intact - Add sha256sum -c verification after each spaCy model wheel download in both compose/production/django/Dockerfile and compose/local/django/Dockerfile. Pinning to an explicit GitHub release URL reduces drift risk but without a hash check the build is still vulnerable to a modified release or CDN MITM; sha256sum -c fails the build immediately on mismatch. - Confirm the size-budget gate is intact: the Enforce Django image size budget step uses steps.meta.outputs.tags (not the removed steps.build.outputs.imageid) to identify the locally-loaded image for docker image inspect, so the split build path is correct as-is. - Fix post-merge mypy regression: get_embedder / aget_embedder return type is tuple[Optional[type[BaseEmbedder]], Optional[str]] but get_component_by_name returns type[PipelineComponentBase]; cast() at the return site bridges the gap without changing runtime behaviour. - Update CHANGELOG.md to document the SHA-256 addition and size-gate verification; also re-add the Docker hardening entry that was lost during the merge conflict resolution. Closes #1524 --- CHANGELOG.md | 2 ++ compose/local/django/Dockerfile | 18 +++++++++++++----- compose/production/django/Dockerfile | 4 +++- opencontractserver/utils/embeddings.py | 4 ++-- 4 files changed, 20 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c380093b7d..742ce1c498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **`.dockerignore` documents why `locale/` is safely excluded** — the tree contains no `.po`/`.mo` files (only a placeholder `README.rst`) and no `compose/*/start*` script invokes `manage.py compilemessages`. The runtime image still installs `gettext` so future i18n work has a build hook ready; if `.po` files are added, drop the `locale/` line so the build context picks them up. - **`brotli>=1.1.0` floor rationale documented** in `requirements/base.txt`. The constraint was not chosen incidentally — 1.1.0 is the current major-version line under active upstream maintenance. The underlying `_brotli` C-extension API that `httpx` looks up has been stable since 1.0.x, so a future transitive constraint can lower the floor safely if needed. - **`ffmpeg` removal verified** by an exhaustive `grep` across `opencontractserver/`, `compose/`, `config/`, `frontend/`, and the workflow tree: no Python `import`, `subprocess`, or shell invocation of `ffmpeg` exists. The PR #1500 removal of the apt package is safe; this entry records the verification trail rather than re-removing anything. + - **SHA-256 checksums added for spaCy wheel downloads** in both `compose/production/django/Dockerfile` and `compose/local/django/Dockerfile`. Pinning to an explicit release URL is good but without a hash check the build is still vulnerable to a GitHub release being retroactively modified or a CDN MITM. Each `wget` step is now followed by a `sha256sum -c` verification that fails the build immediately if the fetched wheel doesn't match the expected digest. + - **Size-budget gate confirmed intact** after the `id: build` step-ID removal. The gate at `.github/workflows/docker-build-release.yml:93` uses `${{ steps.meta.outputs.tags }}` (not the removed `steps.build.outputs.imageid`) to identify the locally-loaded image for `docker image inspect`, so the split build path works correctly. - **Graduated `opencontractserver.pipeline.*` from the mypy baseline** (issue #1483, refs #1447, #1331). All fourteen pipeline modules — the `base` package (`base_component`, `chunked_parser`, `embedder`, `parser`, `post_processor`), the embedders (`multimodal_microservice`, `openai_embedder`, `sent_transformer_microservice`), the parsers (`docling_parser_rest`, `docxodus_parser`, `llamaparse_parser`, `oc_markdown_parser`, `oc_text_parser`), `post_processors.pdf_redactor`, plus `pipeline.registry` and `pipeline.utils` — are now type-checked from scratch (103 baseline entries removed from `docs/typing/mypy_baseline.txt`). Notable changes that surfaced from the graduation: shared component metadata (`title`, `description`, `author`, `dependencies`, `input_schema`) is now declared on `PipelineComponentBase` so `type[PipelineComponentBase]` lookups in `pipeline.utils.get_metadata_for_component` resolve attributes through the base class instead of bare `type`; `BasePostProcessor` now declares `supported_file_types: list[FileTypeEnum]` (previously accessed but never declared on the base); `BaseMultimodalMicroserviceEmbedder.url_setting_name` is now a typed class attribute (referenced in error messages, never declared); `pipeline.utils.run_post_processors` validates that the class loaded by `get_component_by_name` is actually a `BasePostProcessor` subclass before calling `process_export`. Image-token construction in `docling_parser_rest` and `llamaparse_parser` only sets optional `PawlsTokenPythonType` fields when present, eliminating spurious `None` writes against `NotRequired`-but-non-`None` slots. `llamaparse_parser._build_image_token` extracted as a shared helper. No runtime behaviour change; all interactions with broader union types are preserved through targeted `cast()` calls at the boundary. - **Type-checking: graduated `opencontractserver.utils.*` chunk 1 from mypy baseline** (issue #1480, sub-issue of #1447). Removed `[mypy-…]` ignore blocks for `utils.analysis`, `utils.analyzer`, `utils.cleanup`, `utils.corpus_forking`, `utils.embeddings`, and `utils.etl` and fixed all surfaced errors. Notable production fixes uncovered by typing: - `opencontractserver/utils/cleanup.py:34` — `delete_analysis_and_annotations` was calling `Analysis.annotations.filter(...)` on the class-level reverse descriptor (would fail at runtime with `AttributeError`); now correctly uses `analysis.annotations.all().delete()`. diff --git a/compose/local/django/Dockerfile b/compose/local/django/Dockerfile index 03f5bfc464..e16d96d4bd 100644 --- a/compose/local/django/Dockerfile +++ b/compose/local/django/Dockerfile @@ -101,16 +101,24 @@ RUN pip install --upgrade pip \ # Smoke test: `import cv2` must succeed (see production Dockerfile for context). RUN python -c "import cv2; print(f'cv2 {cv2.__version__} import OK')" -# Download spacy models with retry logic. Using pip directly is more reliable -# than the `spacy download` command in Docker. -RUN pip install https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \ +# Download spacy models with retry logic and SHA-256 verification. +# Hashes are for the pinned 3.8.0 release wheels from the explosion/spacy-models +# GitHub releases page. Update hashes when bumping the model version. +RUN wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 \ + -O /tmp/en_core_web_sm-3.8.0-py3-none-any.whl \ + https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \ + && echo "1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85 /tmp/en_core_web_sm-3.8.0-py3-none-any.whl" | sha256sum -c - \ + && pip install /tmp/en_core_web_sm-3.8.0-py3-none-any.whl \ + && rm /tmp/en_core_web_sm-3.8.0-py3-none-any.whl \ && python -c "import spacy; spacy.load('en_core_web_sm')" # For the large model, use wget with retry to ensure the download completes. RUN wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 \ + -O /tmp/en_core_web_lg-3.8.0-py3-none-any.whl \ https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.8.0/en_core_web_lg-3.8.0-py3-none-any.whl \ - && pip install en_core_web_lg-3.8.0-py3-none-any.whl \ - && rm en_core_web_lg-3.8.0-py3-none-any.whl \ + && echo "293e9547a655b25499198ab15a525b05b9407a75f10255e405e8c3854329ab63 /tmp/en_core_web_lg-3.8.0-py3-none-any.whl" | sha256sum -c - \ + && pip install /tmp/en_core_web_lg-3.8.0-py3-none-any.whl \ + && rm /tmp/en_core_web_lg-3.8.0-py3-none-any.whl \ && python -c "import spacy; spacy.load('en_core_web_lg')" COPY ./compose/production/django/entrypoint /entrypoint diff --git a/compose/production/django/Dockerfile b/compose/production/django/Dockerfile index d2f1b2019f..5fdda68135 100644 --- a/compose/production/django/Dockerfile +++ b/compose/production/django/Dockerfile @@ -64,9 +64,11 @@ RUN (find ./requirements -mindepth 2 -type f -name "*.txt" -print; \ RUN wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 \ -P /usr/src/app/wheels \ https://github.com/explosion/spacy-models/releases/download/en_core_web_sm-3.8.0/en_core_web_sm-3.8.0-py3-none-any.whl \ + && echo "1932429db727d4bff3deed6b34cfc05df17794f4a52eeb26cf8928f7c1a0fb85 /usr/src/app/wheels/en_core_web_sm-3.8.0-py3-none-any.whl" | sha256sum -c - \ && wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 -t 5 \ -P /usr/src/app/wheels \ - https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.8.0/en_core_web_lg-3.8.0-py3-none-any.whl + https://github.com/explosion/spacy-models/releases/download/en_core_web_lg-3.8.0/en_core_web_lg-3.8.0-py3-none-any.whl \ + && echo "293e9547a655b25499198ab15a525b05b9407a75f10255e405e8c3854329ab63 /usr/src/app/wheels/en_core_web_lg-3.8.0-py3-none-any.whl" | sha256sum -c - # ---------------------------------------------------------------------------- # # Runtime stage: slim base, runtime libs only. # diff --git a/opencontractserver/utils/embeddings.py b/opencontractserver/utils/embeddings.py index 92092699fc..c824f12d33 100644 --- a/opencontractserver/utils/embeddings.py +++ b/opencontractserver/utils/embeddings.py @@ -1,5 +1,5 @@ import logging -from typing import Optional, Union +from typing import Optional, Union, cast from channels.db import database_sync_to_async @@ -124,7 +124,7 @@ def get_embedder( f"Return embedder class: {embedder_class}, embedder path: {embedder_path}" ) - return embedder_class, embedder_path + return cast(Optional[type[BaseEmbedder]], embedder_class), embedder_path def generate_embeddings_from_text(