Skip to content

fix: split prod/dev requirements to shrink Heroku slug under 1GB#978

Open
northdpole wants to merge 1 commit into
mainfrom
fix/heroku-requirements-split
Open

fix: split prod/dev requirements to shrink Heroku slug under 1GB#978
northdpole wants to merge 1 commit into
mainfrom
fix/heroku-requirements-split

Conversation

@northdpole

Copy link
Copy Markdown
Collaborator

Summary

  • Split dependencies: slim requirements.txt for Heroku/prod (no sentence-transformers/torch), fat requirements-dev.txt for local/CI/importers/Librarian ML.
  • Lazy-import heavy modules (prompt_client, playwright/nltk, gspread, alive_progress, OSCAL, coverage) so gunicorn cre:app boots without pulling importer/ML tooling.
  • Point Makefile, linter CI, and local scripts at requirements-dev.txt; Heroku/Dockerfile keep default requirements.txt.

Closes #975.

Out of scope

Test plan

  • make lint
  • Targeted: python -m unittest application.tests.web_main_test application.tests.spreadsheet_test
  • CI green on this PR
  • After merge: Deploy to OPENCREORG — confirm slug < 1000M
  • Smoke prod: /rest/v1/standards, /docs, chat (still has litellm/sklearn on prod in this PR)

Made with Cursor

Move torch/sentence-transformers and importer/test tooling into
requirements-dev.txt, keep a slim requirements.txt for Heroku, and lazy-import
heavy deps so gunicorn boot no longer pulls them. Closes #975.
@coderabbitai

coderabbitai Bot commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added clearer service-unavailable responses when optional OSCAL, machine-learning, or LLM features are not installed.
    • Development and production dependency sets are now separated for simpler environment setup.
  • Bug Fixes

    • Improved handling of duplicate spreadsheet headers with clearer validation errors.
    • Reduced startup issues by loading optional integrations only when the related functionality is used.
  • Chores

    • Updated local setup, CI, and utility scripts to use the development dependency set where appropriate.

Walkthrough

The PR separates production and development dependencies, redirects development workflows to the expanded dependency set, lazily imports optional modules, adds HTTP 503 handling for unavailable web features, and updates spreadsheet duplicate-header errors and related tests.

Changes

Dependency and import reduction

Layer / File(s) Summary
Production and development dependency split
.github/workflows/linter.yml, Makefile, requirements*.txt, scripts/*
Production requirements are reduced, development requirements include additional tooling, and local, CI, and script installation commands use requirements-dev.txt.
Lazy imports in CLI and parser paths
application/cmd/cre_main.py, application/prompt_client/prompt_client.py, application/utils/external_project_parsers/*, cre.py
Optional prompt, parser, progress, Playwright, NLTK, and coverage imports move into the functions that use them; type-only imports use forward references.
Route-scoped optional dependencies
application/web/web_main.py, application/tests/web_main_test.py
Web handlers lazily import OSCAL, parser, CLI, and ML dependencies, return HTTP 503 when selected dependencies are unavailable, and enqueue the gap job using a module-path string.
Spreadsheet import contract and tests
application/utils/spreadsheet.py, application/tests/spreadsheet_test.py
gspread imports become lazy, duplicate headers raise ValueError, and spreadsheet tests update their patches and expected exception type.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • OWASP/OpenCRE#924: Both changes modify the /rest/v1/map_analysis handler in application/web/web_main.py.
  • OWASP/OpenCRE#968: Both changes modify REST handler code in application/web/web_main.py, including /rest/v1/map_analysis.

Suggested reviewers: Pa04rth

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 35.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the core change: splitting prod/dev requirements to reduce Heroku slug size.
Description check ✅ Passed The description matches the implemented dependency split and lazy-import cleanup, so it is on-topic.
Linked Issues check ✅ Passed The changes align with #975 by slimming prod deps, adding dev deps, and moving heavy imports off startup paths.
Out of Scope Changes check ✅ Passed The modified files are all related to dependency splitting, CI/local install paths, or import laziness; no unrelated changes stand out.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/heroku-requirements-split

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@northdpole

Copy link
Copy Markdown
Collaborator Author

Review request for the GSoC Module A–C authors (GitHub only allows formal review requests for collaborators; @Pa04rth is already requested as a reviewer):

Please take a look when you can — especially anything that affects local/CI installs via requirements-dev.txt vs the slim Heroku requirements.txt.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
application/utils/spreadsheet.py (2)

32-33: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update stale docstring to reflect ValueError instead of GSpreadException.

The docstring says "Raises GSpreadException when duplicate header names are present" but line 40 now raises ValueError. This misleads callers about the exception contract.

📝 Proposed fix
-    strings. Raises ``GSpreadException`` when duplicate header names are present
+    strings. Raises ``ValueError`` when duplicate header names are present
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/utils/spreadsheet.py` around lines 32 - 33, Update the docstring
for the affected spreadsheet header-handling function to state that duplicate
header names raise ValueError, matching the exception raised at line 40. Remove
the stale GSpreadException reference while preserving the rest of the documented
behavior.

93-97: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

wsh may be unbound in the except handler if exception is raised before the worksheet loop.

The handler at line 96 calls findDups(wsh.row_values(1)), but wsh is only assigned inside the for wsh in sh.worksheets() loop. If a ValueError or GSpreadException is raised by gspread.oauth(), gspread.service_account(), or gc.open_by_url() — before the loop begins — wsh is undefined and the handler itself will raise UnboundLocalError, masking the original error.

Adding ValueError to this catch clause slightly widens the exposure since gspread.oauth() / gspread.service_account() can raise ValueError for malformed credentials.

🛡️ Proposed fix
     except (gspread.exceptions.GSpreadException, ValueError) as gse:
-        logger.error(
-            "If this exception says you have a duplicate cell name, the duplicate is",
-            findDups(wsh.row_values(1)),
-        )
+        logger.error("Error reading spreadsheet '%s': '%s'", alias, url)
+        logger.error(gse)
+        try:
+            logger.error(
+                "If this exception says you have a duplicate cell name, the duplicate is %s",
+                findDups(wsh.row_values(1)),
+            )
+        except NameError:
+            pass  # wsh not yet assigned if the error occurred before the worksheet loop
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/utils/spreadsheet.py` around lines 93 - 97, Update the exception
handler surrounding the spreadsheet setup and worksheet loop so it does not
access the loop variable wsh unless a worksheet was successfully assigned;
preserve the original GSpreadException or ValueError while logging
duplicate-cell details only when wsh is available. Anchor the change to the
existing wsh loop and logger.error handler.
🧹 Nitpick comments (1)
application/web/web_main.py (1)

1084-1101: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add regression coverage for unavailable chat dependencies.

Test both the ImportError path and PromptHandler’s missing-litellm RuntimeError path, asserting HTTP 503. This protects the new production dependency boundary from regressing to a 500 response.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/web/web_main.py` around lines 1084 - 1101, Add regression tests
for the chat initialization flow in web_main, covering both an ImportError while
loading prompt_client and a RuntimeError from PromptHandler indicating missing
litellm. Assert each scenario returns HTTP 503 rather than 500, while preserving
propagation of unrelated RuntimeError instances.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@application/prompt_client/prompt_client.py`:
- Around line 232-239: Move the PlaywrightError and PlaywrightTimeoutError
imports in get_content below the _is_likely_pdf_url(url) branch, ensuring PDF
URLs use _fetch_pdf_text_for_embeddings without importing Playwright while
non-PDF paths retain access to those exception types.

In `@requirements.txt`:
- Line 17: Remove the unpinned setuptools entry from the production requirements
list. Keep build and CI tooling dependencies in their existing dedicated
configuration, and only retain setuptools here if Heroku requires it with an
explicitly reviewed version.

In `@scripts/run-local.sh`:
- Around line 15-17: Update the dependency check in the local runner around the
Flask import so it validates the development environment represented by
requirements-dev.txt, not only Flask. Use a development-install marker or
representative dev-only package, or run the idempotent requirements-dev
installation unconditionally, while preserving the existing installation message
and command behavior.

In `@scripts/update-cwe.sh`:
- Around line 18-20: Replace the requests import check in the update-CWE setup
flow with a dedicated development-installation marker, or install
requirements-dev.txt unconditionally; do not use the production dependency as
the readiness signal. Ensure the path invoking cre.py --cwe_in always has all
development dependencies available.

---

Outside diff comments:
In `@application/utils/spreadsheet.py`:
- Around line 32-33: Update the docstring for the affected spreadsheet
header-handling function to state that duplicate header names raise ValueError,
matching the exception raised at line 40. Remove the stale GSpreadException
reference while preserving the rest of the documented behavior.
- Around line 93-97: Update the exception handler surrounding the spreadsheet
setup and worksheet loop so it does not access the loop variable wsh unless a
worksheet was successfully assigned; preserve the original GSpreadException or
ValueError while logging duplicate-cell details only when wsh is available.
Anchor the change to the existing wsh loop and logger.error handler.

---

Nitpick comments:
In `@application/web/web_main.py`:
- Around line 1084-1101: Add regression tests for the chat initialization flow
in web_main, covering both an ImportError while loading prompt_client and a
RuntimeError from PromptHandler indicating missing litellm. Assert each scenario
returns HTTP 503 rather than 500, while preserving propagation of unrelated
RuntimeError instances.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 13ddd54c-327d-406e-8dce-50bd206b7fff

📥 Commits

Reviewing files that changed from the base of the PR and between 6b6a5ac and 5b7b6f6.

📒 Files selected for processing (17)
  • .github/workflows/linter.yml
  • Makefile
  • application/cmd/cre_main.py
  • application/prompt_client/prompt_client.py
  • application/tests/spreadsheet_test.py
  • application/tests/web_main_test.py
  • application/utils/external_project_parsers/base_parser.py
  • application/utils/external_project_parsers/base_parser_defs.py
  • application/utils/spreadsheet.py
  • application/web/web_main.py
  • cre.py
  • requirements-dev.txt
  • requirements.txt
  • scripts/build_labeled_dataset.py
  • scripts/run-local.sh
  • scripts/sync_embeddings_table.py
  • scripts/update-cwe.sh

Comment on lines 232 to 239
def get_content(self, url) -> Optional[str]:
# Playwright is for scrape/import embedding generation only — not chat.
from playwright.sync_api import Error as PlaywrightError
from playwright.sync_api import TimeoutError as PlaywrightTimeoutError

for attempts in range(1, 10):
if _is_likely_pdf_url(url):
text = _fetch_pdf_text_for_embeddings(url)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("application/prompt_client/prompt_client.py")
print("exists:", path.exists())
lines = path.read_text().splitlines()
for start in (210, 220, 230, 240, 250, 260):
    end = min(start + 30, len(lines))
    print(f"\n--- lines {start}-{end} ---")
    for i in range(start, end):
        print(f"{i+1:4d}: {lines[i]}")
PY

Repository: OWASP/OpenCRE

Length of output: 8470


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

files = [
    Path("application/prompt_client/prompt_client.py"),
    Path("pyproject.toml"),
    Path("requirements.txt"),
    Path("requirements-dev.txt"),
    Path("setup.py"),
    Path("setup.cfg"),
    Path("Pipfile"),
    Path("poetry.lock"),
]

for p in files:
    if p.exists():
        print(f"\n### {p}")
        text = p.read_text(errors="ignore").splitlines()
        for i, line in enumerate(text, 1):
            if "playwright" in line.lower() or "pdf" in line.lower() or "prompt_client" in line.lower():
                print(f"{i}: {line}")
PY

Repository: OWASP/OpenCRE

Length of output: 4540


Move the Playwright imports below the PDF branch

get_content() imports playwright.sync_api before checking _is_likely_pdf_url(url), so PDF-only calls still fail when Playwright isn’t installed. Move those imports into the non-PDF path to keep PDF extraction lazy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@application/prompt_client/prompt_client.py` around lines 232 - 239, Move the
PlaywrightError and PlaywrightTimeoutError imports in get_content below the
_is_likely_pdf_url(url) branch, ensuring PDF URLs use
_fetch_pdf_text_for_embeddings without importing Playwright while non-PDF paths
retain access to those exception types.

Comment thread requirements.txt
scikit_learn
scipy
semver
setuptools

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the relevant dependency file and nearby context.
if [ -f requirements.txt ]; then
  wc -l requirements.txt
  cat -n requirements.txt | sed -n '1,80p'
fi

# Look for references that would justify setuptools as a runtime dependency.
rg -n --hidden --glob '!**/.git/**' '\bsetuptools\b|pkg_resources|distutils' .

Repository: OWASP/OpenCRE

Length of output: 2008


Drop setuptools from the production dependency set. It only appears here and in build/CI tooling, so keeping it in requirements.txt pulls build tooling into the runtime image. If Heroku truly needs it, pin it to a reviewed version instead of leaving it unconstrained.

🧰 Tools
🪛 OSV Scanner (2.4.0)

[HIGH] 17-17: setuptools 9.1.0: undefined

(PYSEC-2022-43012)


[HIGH] 17-17: setuptools 9.1.0: undefined

(PYSEC-2025-49)


[HIGH] 17-17: setuptools 9.1.0: setuptools vulnerable to Command Injection via package URL

(PYSEC-2026-1918)


[HIGH] 17-17: setuptools 9.1.0: setuptools has a path traversal vulnerability in PackageIndex.download that leads to Arbitrary File Write

(GHSA-5rjg-fvgr-3xxf)


[HIGH] 17-17: setuptools 9.1.0: setuptools vulnerable to Command Injection via package URL

(GHSA-cx63-2mw6-8hw5)


[HIGH] 17-17: setuptools 9.1.0: pypa/setuptools vulnerable to Regular Expression Denial of Service (ReDoS)

(GHSA-r9hx-vwmv-q579)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@requirements.txt` at line 17, Remove the unpinned setuptools entry from the
production requirements list. Keep build and CI tooling dependencies in their
existing dedicated configuration, and only retain setuptools here if Heroku
requires it with an explicitly reviewed version.

Source: Linters/SAST tools

Comment thread scripts/run-local.sh
Comment on lines 15 to +17
if ! python -c "import flask" >/dev/null 2>&1; then
echo "Installing Python dependencies"
pip install -r "$ROOT_DIR/requirements.txt"
pip install -r "$ROOT_DIR/requirements-dev.txt"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Check for the development environment, not just Flask.

A pre-existing virtual environment with Flask installed will skip this installation even when the newly separated development dependencies are absent. Use a dev-install marker, check a representative dev-only package, or run the idempotent requirements-dev installation unconditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/run-local.sh` around lines 15 - 17, Update the dependency check in
the local runner around the Flask import so it validates the development
environment represented by requirements-dev.txt, not only Flask. Use a
development-install marker or representative dev-only package, or run the
idempotent requirements-dev installation unconditionally, while preserving the
existing installation message and command behavior.

Comment thread scripts/update-cwe.sh
Comment on lines 18 to +20
if ! python -c "import requests" >/dev/null 2>&1; then
echo "Installing Python dependencies"
pip install -r "$ROOT_DIR/requirements.txt"
pip install -r "$ROOT_DIR/requirements-dev.txt"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not use requests as the development-installation readiness check.

Because requests remains in production requirements, an existing environment can pass this check while missing importer dependencies required by cre.py --cwe_in. Use a dedicated dev-install marker or install requirements-dev.txt unconditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/update-cwe.sh` around lines 18 - 20, Replace the requests import
check in the update-CWE setup flow with a dedicated development-installation
marker, or install requirements-dev.txt unconditionally; do not use the
production dependency as the readiness signal. Ensure the path invoking cre.py
--cwe_in always has all development dependencies available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

P1: Unblock Heroku prod deploy — split requirements + aggressive slug strip

1 participant