Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ jobs:
"$smoke_root/venv/bin/doris-mcp-client" --help
"$smoke_root/venv/bin/python" -c \
"import doris_mcp_client, doris_mcp_server"
"$smoke_root/venv/bin/python" \
"$GITHUB_WORKSPACE/test/deployment/check_runtime_dependencies.py"

conformance:
name: MCP 2026-07-28 conformance
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ under **Unreleased** until a new version is selected and published.
token files.
- Added coverage gates for the protocol, authentication, and core manager
domains at 80%, alongside a 55% whole-repository floor.
- Separated production dependencies from test, lint, type-check, and build
tooling across package metadata, generated requirements, Docker, and clean
wheel verification.

### Fixed

Expand Down
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,10 +262,24 @@ cd doris-mcp-server

### 2. Install Dependencies

The `dev` dependency group contains the test and quality tools used by CI:

```bash
uv sync --frozen --group dev
```

For pip-based workflows, `requirements.txt` contains production dependencies
only. `requirements-dev.txt` includes that runtime manifest and adds the same
lean test and quality toolchain:

```bash
pip install -r requirements.txt
pip install -r requirements-dev.txt
```

Production images and runtime environments must install only the package or
`requirements.txt`; pytest, linters, type checkers, and build backends are not
runtime dependencies.

### 3. Configure Environment Variables

Copy the `.env.example` file to `.env` and modify the settings according to your environment:
Expand Down Expand Up @@ -1451,7 +1465,8 @@ doris-mcp-server/
├── README.md # This documentation
├── CHANGELOG.md # Tagged release history and unreleased changes
├── .env.example # Environment variables template
├── requirements.txt # Python dependencies
├── requirements.txt # Runtime-only Python dependencies
├── requirements-dev.txt # Runtime plus test and quality dependencies
├── pyproject.toml # Project configuration and entry points
├── uv.lock # UV package manager lock file
├── generate_requirements.py # Requirements generation script
Expand Down
272 changes: 132 additions & 140 deletions generate_requirements.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,156 +16,148 @@
# specific language governing permissions and limitations
# under the License.
"""
Generate requirements.txt from pyproject.toml
Ensures consistency in dependency management
Generate separated runtime and development requirement manifests.

``pyproject.toml`` remains the source of truth:

* ``project.dependencies`` feeds the production ``requirements.txt``.
* ``dependency-groups.dev`` feeds ``requirements-dev.txt``.
"""

from __future__ import annotations

import re
import sys
import tomllib
from pathlib import Path
from typing import Any

import toml
PROJECT_PATH = Path("pyproject.toml")
RUNTIME_REQUIREMENTS_PATH = Path("requirements.txt")
DEV_REQUIREMENTS_PATH = Path("requirements-dev.txt")


def generate_requirements():
"""Generate requirements.txt from pyproject.toml"""
def load_pyproject(project_path: Path = PROJECT_PATH) -> dict[str, Any]:
"""Load project metadata from a PEP 518 TOML document."""

# Read pyproject.toml
try:
with open('pyproject.toml', encoding='utf-8') as f:
pyproject = toml.load(f)
except FileNotFoundError:
print("❌ pyproject.toml not found")
sys.exit(1)

# Get main dependencies
dependencies = pyproject.get('project', {}).get('dependencies', [])

# Get development dependencies
dev_dependencies = pyproject.get('project', {}).get('optional-dependencies', {}).get('dev', [])

# Generate requirements.txt
requirements_content = []

# Add header comment
requirements_content.append("# Main dependencies - auto-generated from pyproject.toml")
requirements_content.append("# Do not edit this file manually, use 'python generate_requirements.py' to regenerate")
requirements_content.append("")

# Add main dependencies
requirements_content.append("# === Core Dependencies ===")
for dep in dependencies:
requirements_content.append(dep)

requirements_content.append("")
requirements_content.append("# === Development Dependencies ===")
for dep in dev_dependencies:
requirements_content.append(dep)

# Write requirements.txt
with open('requirements.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(requirements_content))

print("✅ Generated requirements.txt")
print(f" Main dependencies: {len(dependencies)} items")
print(f" Dev dependencies: {len(dev_dependencies)} items")

def generate_requirements_dev():
"""Generate requirements-dev.txt (only development dependencies)"""

pyproject_path = Path("pyproject.toml")
if not pyproject_path.exists():
print("Error: pyproject.toml not found")
return

with open(pyproject_path, encoding='utf-8') as f:
data = toml.load(f)

# Get development dependencies
dev_dependencies = data.get('project', {}).get('optional-dependencies', {}).get('dev', [])

# Generate requirements-dev.txt
content = []
content.append("# Development dependencies - auto-generated from pyproject.toml")
content.append("# Installation command: pip install -r requirements-dev.txt")
content.append("")

for dep in dev_dependencies:
content.append(dep)

# Write file
dev_requirements_path = Path("requirements-dev.txt")
with open(dev_requirements_path, 'w', encoding='utf-8') as f:
f.write('\n'.join(content))

print(f"✅ Generated requirements-dev.txt ({len(dev_dependencies)} development dependencies)")

def verify_consistency():
"""Verify dependency consistency"""

def extract_packages_from_requirements():
"""Extract package names from requirements.txt"""
packages = set()
try:
with open('requirements.txt') as f:
for line in f:
line = line.strip()
if line and not line.startswith('#'):
# Extract package name (remove version)
pkg = re.split(r'[>=<\[]', line)[0].strip()
if pkg:
packages.add(pkg.lower())
except FileNotFoundError:
print("requirements.txt not found")
return packages

def extract_packages_from_pyproject():
"""Extract package names from pyproject.toml"""
packages = set()
try:
with open('pyproject.toml') as f:
data = toml.load(f)

# Get main dependencies
dependencies = data.get('project', {}).get('dependencies', [])
for dep in dependencies:
pkg = re.split(r'[>=<\[]', dep)[0].strip()
if pkg:
packages.add(pkg.lower())

# Get development dependencies
dev_deps = data.get('project', {}).get('optional-dependencies', {}).get('dev', [])
for dep in dev_deps:
pkg = re.split(r'[>=<\[]', dep)[0].strip()
if pkg:
packages.add(pkg.lower())

except FileNotFoundError:
print("pyproject.toml not found")
return packages

req_packages = extract_packages_from_requirements()
toml_packages = extract_packages_from_pyproject()

only_in_req = req_packages - toml_packages
only_in_toml = toml_packages - req_packages

if len(only_in_req) == 0 and len(only_in_toml) == 0:
print("✅ Dependency consistency verification passed!")
with project_path.open("rb") as project_file:
return tomllib.load(project_file)
except FileNotFoundError as exc:
raise FileNotFoundError(f"{project_path} not found") from exc


def runtime_dependencies(pyproject: dict[str, Any]) -> list[str]:
"""Return dependencies installed for every package consumer."""

return list(pyproject.get("project", {}).get("dependencies", []))


def development_dependencies(pyproject: dict[str, Any]) -> list[str]:
"""Return the lean development group used by CI and local contributors."""

return list(pyproject.get("dependency-groups", {}).get("dev", []))


def generate_requirements(
project_path: Path = PROJECT_PATH,
output_path: Path = RUNTIME_REQUIREMENTS_PATH,
) -> int:
"""Generate the production-only requirements manifest."""

dependencies = runtime_dependencies(load_pyproject(project_path))
content = [
"# Runtime dependencies - auto-generated from pyproject.toml",
"# Do not edit manually; run `python generate_requirements.py`.",
"",
*dependencies,
"",
]
output_path.write_text("\n".join(content), encoding="utf-8")
return len(dependencies)


def generate_requirements_dev(
project_path: Path = PROJECT_PATH,
output_path: Path = DEV_REQUIREMENTS_PATH,
) -> int:
"""Generate the development manifest on top of runtime requirements."""

dependencies = development_dependencies(load_pyproject(project_path))
content = [
"# Development dependencies - auto-generated from pyproject.toml",
"# Install with `pip install -r requirements-dev.txt`.",
"",
"-r requirements.txt",
"",
*dependencies,
"",
]
output_path.write_text("\n".join(content), encoding="utf-8")
return len(dependencies)


def _manifest_dependencies(path: Path) -> set[str]:
return {
line
for raw_line in path.read_text(encoding="utf-8").splitlines()
if (line := raw_line.strip())
and not line.startswith("#")
and not line.startswith("-r ")
}


def _dependency_names(dependencies: set[str]) -> set[str]:
return {
re.split(r"[<>=!~;\[]", dependency, maxsplit=1)[0].strip().lower()
for dependency in dependencies
}


def verify_consistency(
project_path: Path = PROJECT_PATH,
runtime_path: Path = RUNTIME_REQUIREMENTS_PATH,
dev_path: Path = DEV_REQUIREMENTS_PATH,
) -> bool:
"""Verify both generated manifests against their independent source layers."""

pyproject = load_pyproject(project_path)
expected_runtime = set(runtime_dependencies(pyproject))
expected_dev = set(development_dependencies(pyproject))
actual_runtime = _manifest_dependencies(runtime_path)
actual_dev = _manifest_dependencies(dev_path)

problems = {
"runtime only in manifest": actual_runtime - expected_runtime,
"runtime only in pyproject": expected_runtime - actual_runtime,
"development only in manifest": actual_dev - expected_dev,
"development only in pyproject": expected_dev - actual_dev,
"runtime/development overlap": _dependency_names(expected_runtime)
& _dependency_names(expected_dev),
}
active_problems = {name: values for name, values in problems.items() if values}
if not active_problems:
print("✅ Dependency layer consistency verification passed!")
return True
else:
print("⚠️ Found dependency inconsistencies:")
if only_in_req:
print(f" Only in requirements.txt: {sorted(only_in_req)}")
if only_in_toml:
print(f" Only in pyproject.toml: {sorted(only_in_toml)}")
return False

if __name__ == "__main__":
print("🔄 Generating requirements.txt from pyproject.toml...")
generate_requirements()
generate_requirements_dev()
print("❌ Found dependency layer inconsistencies:")
for name, values in active_problems.items():
print(f" {name}: {sorted(values)}")
return False


def main() -> int:
print("🔄 Generating separated requirement manifests...")
runtime_count = generate_requirements()
dev_count = generate_requirements_dev()
print(f" Runtime dependencies: {runtime_count} items")
print(f" Development dependencies: {dev_count} items")
print()
print("🔍 Verifying dependency consistency...")
verify_consistency()
print("🔍 Verifying dependency layer consistency...")
if not verify_consistency():
return 1
print("✨ Completed!")
return 0


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading