This is an automated email from the ASF dual-hosted git repository.
FreeOnePlus pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris-mcp-server.git
The following commit(s) were added to refs/heads/master by this push:
new 1d64994 build: separate runtime and development dependencies (#153)
1d64994 is described below
commit 1d6499499480048c54bc850d2c8634303b407851
Author: Yijia Su <[email protected]>
AuthorDate: Thu Jul 30 15:57:59 2026 +0800
build: separate runtime and development dependencies (#153)
---
.github/workflows/ci.yml | 2 +
CHANGELOG.md | 3 +
README.md | 19 +-
generate_requirements.py | 272 ++++++++++++------------
pyproject.toml | 23 +-
requirements-dev.txt | 27 +--
requirements.txt | 29 +--
test/deployment/check_runtime_dependencies.py | 69 ++++++
test/deployment/test_dependency_layers.py | 160 ++++++++++++++
test/deployment/test_github_actions_contract.py | 1 +
uv.lock | 39 ++--
11 files changed, 426 insertions(+), 218 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index adb7090..d25229f 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 31ec2a3..1e9fa59 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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
diff --git a/README.md b/README.md
index f1cab45..5fbbbd8 100644
--- a/README.md
+++ b/README.md
@@ -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:
@@ -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
diff --git a/generate_requirements.py b/generate_requirements.py
index 755c82b..2b90186 100644
--- a/generate_requirements.py
+++ b/generate_requirements.py
@@ -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())
diff --git a/pyproject.toml b/pyproject.toml
index 11d2ea8..355c976 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -65,7 +65,6 @@ dependencies = [
"pydantic>=2.5.0",
"pydantic-settings>=2.1.0",
"jsonschema>=4.23.0,<5.0.0",
- "toml>=0.10.0",
"PyYAML>=6.0.0",
"python-dotenv>=1.0.0",
# Security and authentication
@@ -86,32 +85,29 @@ dependencies = [
"uvicorn[standard]>=0.25.0",
"fastapi>=0.108.0",
"starlette>=0.27.0",
- # Development utilities
+ # CLI and runtime utilities
"click>=8.1.0",
"typer>=0.9.0",
"requests>=2.31.0",
"tqdm>=4.66.0",
- "pytest>=8.4.0",
- "pytest-asyncio>=1.0.0",
- "pytest-cov>=6.1.1",
]
[project.optional-dependencies]
dev = [
# Testing
- "pytest>=7.4.0",
- "pytest-asyncio>=0.23.0",
- "pytest-cov>=4.1.0",
+ "pytest>=8.4.0",
+ "pytest-asyncio>=1.0.0",
+ "pytest-cov>=6.1.1",
"pytest-mock>=3.12.0",
"pytest-xdist>=3.5.0",
# Code quality
- "ruff>=0.1.0",
+ "ruff>=0.11.13",
"black>=23.12.0",
"isort>=5.13.0",
"flake8>=7.0.0",
- "mypy>=1.8.0",
- "bandit>=1.7.0",
+ "mypy>=1.16.0",
+ "bandit>=1.8.3",
"safety>=2.3.0",
# Documentation
@@ -305,6 +301,11 @@ dev = [
"bandit>=1.8.3",
"mypy>=1.16.0",
"pandas-stubs>=2.2.0",
+ "pytest>=8.4.0",
+ "pytest-asyncio>=1.0.0",
+ "pytest-cov>=6.1.1",
+ "pytest-mock>=3.12.0",
+ "pytest-xdist>=3.5.0",
"ruff>=0.11.13",
"types-jsonschema>=4.23.0",
]
diff --git a/requirements-dev.txt b/requirements-dev.txt
index 18990be..8982d85 100644
--- a/requirements-dev.txt
+++ b/requirements-dev.txt
@@ -1,20 +1,15 @@
# Development dependencies - auto-generated from pyproject.toml
-# Installation command: pip install -r requirements-dev.txt
+# Install with `pip install -r requirements-dev.txt`.
-pytest>=7.4.0
-pytest-asyncio>=0.23.0
-pytest-cov>=4.1.0
+-r requirements.txt
+
+bandit>=1.8.3
+mypy>=1.16.0
+pandas-stubs>=2.2.0
+pytest>=8.4.0
+pytest-asyncio>=1.0.0
+pytest-cov>=6.1.1
pytest-mock>=3.12.0
pytest-xdist>=3.5.0
-ruff>=0.1.0
-black>=23.12.0
-isort>=5.13.0
-flake8>=7.0.0
-mypy>=1.8.0
-bandit>=1.7.0
-safety>=2.3.0
-sphinx>=7.2.0
-sphinx-rtd-theme>=2.0.0
-myst-parser>=2.0.0
-pre-commit>=3.6.0
-tox>=4.11.0
\ No newline at end of file
+ruff>=0.11.13
+types-jsonschema>=4.23.0
diff --git a/requirements.txt b/requirements.txt
index 670907c..1bd8783 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
-# Main dependencies - auto-generated from pyproject.toml
-# Do not edit this file manually, use 'python generate_requirements.py' to
regenerate
+# Runtime dependencies - auto-generated from pyproject.toml
+# Do not edit manually; run `python generate_requirements.py`.
-# === Core Dependencies ===
mcp>=2.0.0,<2.1.0
aiomysql>=0.2.0
PyMySQL>=1.1.0
@@ -19,7 +18,7 @@ python-dateutil>=2.8.0
orjson>=3.9.0
pydantic>=2.5.0
pydantic-settings>=2.1.0
-toml>=0.10.0
+jsonschema>=4.23.0,<5.0.0
PyYAML>=6.0.0
python-dotenv>=1.0.0
cryptography>=41.0.0
@@ -41,25 +40,3 @@ click>=8.1.0
typer>=0.9.0
requests>=2.31.0
tqdm>=4.66.0
-pytest>=8.4.0
-pytest-asyncio>=1.0.0
-pytest-cov>=6.1.1
-
-# === Development Dependencies ===
-pytest>=7.4.0
-pytest-asyncio>=0.23.0
-pytest-cov>=4.1.0
-pytest-mock>=3.12.0
-pytest-xdist>=3.5.0
-ruff>=0.1.0
-black>=23.12.0
-isort>=5.13.0
-flake8>=7.0.0
-mypy>=1.8.0
-bandit>=1.7.0
-safety>=2.3.0
-sphinx>=7.2.0
-sphinx-rtd-theme>=2.0.0
-myst-parser>=2.0.0
-pre-commit>=3.6.0
-tox>=4.11.0
\ No newline at end of file
diff --git a/test/deployment/check_runtime_dependencies.py
b/test/deployment/check_runtime_dependencies.py
new file mode 100644
index 0000000..fa0fb3d
--- /dev/null
+++ b/test/deployment/check_runtime_dependencies.py
@@ -0,0 +1,69 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Fail when a clean runtime environment contains development-only packages."""
+
+from __future__ import annotations
+
+from importlib.metadata import PackageNotFoundError, version
+
+DEVELOPMENT_ONLY_DISTRIBUTIONS = (
+ "bandit",
+ "black",
+ "build",
+ "flake8",
+ "hatchling",
+ "isort",
+ "mypy",
+ "pre-commit",
+ "pytest",
+ "pytest-asyncio",
+ "pytest-cov",
+ "ruff",
+ "safety",
+ "sphinx",
+ "tox",
+)
+
+
+def installed_development_packages() -> dict[str, str]:
+ """Return forbidden development distributions installed in this
interpreter."""
+
+ installed: dict[str, str] = {}
+ for distribution_name in DEVELOPMENT_ONLY_DISTRIBUTIONS:
+ try:
+ installed[distribution_name] = version(distribution_name)
+ except PackageNotFoundError:
+ continue
+ return installed
+
+
+def main() -> int:
+ installed = installed_development_packages()
+ if installed:
+ details = ", ".join(
+ f"{distribution_name}=={distribution_version}"
+ for distribution_name, distribution_version in
sorted(installed.items())
+ )
+ print(f"Runtime environment contains development-only packages:
{details}")
+ return 1
+
+ print("Runtime dependency boundary: PASS")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/test/deployment/test_dependency_layers.py
b/test/deployment/test_dependency_layers.py
new file mode 100644
index 0000000..999e2da
--- /dev/null
+++ b/test/deployment/test_dependency_layers.py
@@ -0,0 +1,160 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+"""Contracts for production and development dependency separation."""
+
+from __future__ import annotations
+
+import re
+import tomllib
+from pathlib import Path
+
+import generate_requirements
+from test.deployment import check_runtime_dependencies
+
+REPOSITORY_ROOT = Path(__file__).resolve().parents[2]
+PYPROJECT_PATH = REPOSITORY_ROOT / "pyproject.toml"
+RUNTIME_REQUIREMENTS_PATH = REPOSITORY_ROOT / "requirements.txt"
+DEV_REQUIREMENTS_PATH = REPOSITORY_ROOT / "requirements-dev.txt"
+DEVELOPMENT_ONLY = {
+ *check_runtime_dependencies.DEVELOPMENT_ONLY_DISTRIBUTIONS,
+ "pandas-stubs",
+ "pytest-mock",
+ "pytest-xdist",
+ "toml",
+ "types-jsonschema",
+}
+REQUIRED_CI_TOOLS = {
+ "bandit",
+ "mypy",
+ "pytest",
+ "pytest-asyncio",
+ "pytest-cov",
+ "ruff",
+}
+
+
+def _dependency_name(requirement: str) -> str:
+ return re.split(r"[<>=!~;\[]", requirement, maxsplit=1)[0].strip().lower()
+
+
+def _manifest_requirements(path: Path) -> list[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 _pyproject() -> dict:
+ return tomllib.loads(PYPROJECT_PATH.read_text(encoding="utf-8"))
+
+
+def test_runtime_metadata_excludes_development_and_build_tools() -> None:
+ pyproject = _pyproject()
+ runtime = {
+ _dependency_name(requirement)
+ for requirement in pyproject["project"]["dependencies"]
+ }
+
+ assert runtime.isdisjoint(DEVELOPMENT_ONLY)
+ assert pyproject["build-system"]["requires"] == ["hatchling"]
+
+
+def test_ci_development_group_owns_test_and_quality_dependencies() -> None:
+ pyproject = _pyproject()
+ development = {
+ _dependency_name(requirement)
+ for requirement in pyproject["dependency-groups"]["dev"]
+ }
+
+ assert REQUIRED_CI_TOOLS <= development
+ assert development.isdisjoint(
+ {
+ _dependency_name(requirement)
+ for requirement in pyproject["project"]["dependencies"]
+ }
+ )
+
+
+def test_generated_manifests_match_their_independent_dependency_layers() ->
None:
+ pyproject = _pyproject()
+
+ assert (
+ _manifest_requirements(RUNTIME_REQUIREMENTS_PATH)
+ == pyproject["project"]["dependencies"]
+ )
+ assert (
+ _manifest_requirements(DEV_REQUIREMENTS_PATH)
+ == pyproject["dependency-groups"]["dev"]
+ )
+ assert "-r requirements.txt" in
DEV_REQUIREMENTS_PATH.read_text(encoding="utf-8")
+ assert generate_requirements.verify_consistency(
+ PYPROJECT_PATH,
+ RUNTIME_REQUIREMENTS_PATH,
+ DEV_REQUIREMENTS_PATH,
+ )
+
+
+def test_requirement_generator_is_reproducible(tmp_path: Path) -> None:
+ runtime_path = tmp_path / "requirements.txt"
+ dev_path = tmp_path / "requirements-dev.txt"
+
+ generate_requirements.generate_requirements(PYPROJECT_PATH, runtime_path)
+ generate_requirements.generate_requirements_dev(PYPROJECT_PATH, dev_path)
+
+ assert runtime_path.read_bytes() == RUNTIME_REQUIREMENTS_PATH.read_bytes()
+ assert dev_path.read_bytes() == DEV_REQUIREMENTS_PATH.read_bytes()
+
+
+def test_consistency_rejects_cross_layer_package_with_different_specifiers(
+ tmp_path: Path,
+ capsys,
+) -> None:
+ project_path = tmp_path / "pyproject.toml"
+ runtime_path = tmp_path / "requirements.txt"
+ dev_path = tmp_path / "requirements-dev.txt"
+ project_path.write_text(
+ '[project]\ndependencies = ["shared>=1"]\n'
+ '[dependency-groups]\ndev = ["shared>=2"]\n',
+ encoding="utf-8",
+ )
+ runtime_path.write_text("shared>=1\n", encoding="utf-8")
+ dev_path.write_text("-r requirements.txt\nshared>=2\n", encoding="utf-8")
+
+ assert not generate_requirements.verify_consistency(
+ project_path,
+ runtime_path,
+ dev_path,
+ )
+ assert "runtime/development overlap: ['shared']" in capsys.readouterr().out
+
+
+def test_runtime_checker_reports_only_installed_development_packages(
+ monkeypatch,
+) -> None:
+ def fake_version(distribution_name: str) -> str:
+ if distribution_name == "pytest":
+ return "9.0.0"
+ raise check_runtime_dependencies.PackageNotFoundError
+
+ monkeypatch.setattr(check_runtime_dependencies, "version", fake_version)
+
+ assert check_runtime_dependencies.installed_development_packages() == {
+ "pytest": "9.0.0"
+ }
diff --git a/test/deployment/test_github_actions_contract.py
b/test/deployment/test_github_actions_contract.py
index 36082ea..b508f9a 100644
--- a/test/deployment/test_github_actions_contract.py
+++ b/test/deployment/test_github_actions_contract.py
@@ -96,6 +96,7 @@ def
test_quality_test_and_package_gates_cover_the_release_contract():
assert 'doris-mcp-server" --version' in package
assert 'doris-mcp-client" --help' in package
assert "import doris_mcp_client, doris_mcp_server" in package
+ assert "test/deployment/check_runtime_dependencies.py" in package
def test_conformance_gate_is_official_pinned_and_has_no_failure_baseline():
diff --git a/uv.lock b/uv.lock
index 3ee19f8..8486a54 100644
--- a/uv.lock
+++ b/uv.lock
@@ -628,9 +628,6 @@ dependencies = [
{ name = "pydantic-settings" },
{ name = "pyjwt" },
{ name = "pymysql" },
- { name = "pytest" },
- { name = "pytest-asyncio" },
- { name = "pytest-cov" },
{ name = "python-dateutil" },
{ name = "python-dotenv" },
{ name = "python-jose", extra = ["cryptography"] },
@@ -642,7 +639,6 @@ dependencies = [
{ name = "starlette", version = "0.46.2", source = { registry =
"https://pypi.org/simple" }, marker = "python_full_version < '3.14'" },
{ name = "starlette", version = "1.3.1", source = { registry =
"https://pypi.org/simple" }, marker = "python_full_version >= '3.14'" },
{ name = "structlog" },
- { name = "toml" },
{ name = "tqdm" },
{ name = "typer" },
{ name = "uvicorn", extra = ["standard"] },
@@ -693,6 +689,11 @@ dev = [
{ name = "bandit" },
{ name = "mypy" },
{ name = "pandas-stubs" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-cov" },
+ { name = "pytest-mock" },
+ { name = "pytest-xdist" },
{ name = "ruff" },
{ name = "types-jsonschema" },
]
@@ -706,7 +707,7 @@ requires-dist = [
{ name = "aiomysql", specifier = ">=0.2.0" },
{ name = "aioredis", specifier = ">=2.0.0" },
{ name = "asyncio-mqtt", specifier = ">=0.16.0" },
- { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" },
+ { name = "bandit", marker = "extra == 'dev'", specifier = ">=1.8.3" },
{ name = "bcrypt", specifier = ">=4.1.0" },
{ name = "black", marker = "extra == 'dev'", specifier = ">=23.12.0" },
{ name = "cchardet", marker = "extra == 'performance'", specifier =
">=2.1.0" },
@@ -721,7 +722,7 @@ requires-dist = [
{ name = "jaeger-client", marker = "extra == 'monitoring'", specifier =
">=4.8.0" },
{ name = "jsonschema", specifier = ">=4.23.0,<5.0.0" },
{ name = "mcp", specifier = ">=2.0.0,<2.1.0" },
- { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" },
+ { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.16.0" },
{ name = "myst-parser", marker = "extra == 'dev'", specifier = ">=2.0.0" },
{ name = "myst-parser", marker = "extra == 'docs'", specifier = ">=2.0.0"
},
{ name = "numpy", specifier = ">=1.24.0" },
@@ -739,12 +740,9 @@ requires-dist = [
{ name = "pydantic-settings", specifier = ">=2.1.0" },
{ name = "pyjwt", specifier = ">=2.8.0" },
{ name = "pymysql", specifier = ">=1.1.0" },
- { name = "pytest", specifier = ">=8.4.0" },
- { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.4.0" },
- { name = "pytest-asyncio", specifier = ">=1.0.0" },
- { name = "pytest-asyncio", marker = "extra == 'dev'", specifier =
">=0.23.0" },
- { name = "pytest-cov", specifier = ">=6.1.1" },
- { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.4.0" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier =
">=1.0.0" },
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.1.1" },
{ name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0"
},
{ name = "pytest-xdist", marker = "extra == 'dev'", specifier = ">=3.5.0"
},
{ name = "python-dateutil", specifier = ">=2.8.0" },
@@ -754,7 +752,7 @@ requires-dist = [
{ name = "pyyaml", specifier = ">=6.0.0" },
{ name = "requests", specifier = ">=2.31.0" },
{ name = "rich", specifier = ">=13.7.0" },
- { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.1.0" },
+ { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.11.13" },
{ name = "safety", marker = "extra == 'dev'", specifier = ">=2.3.0" },
{ name = "sphinx", marker = "extra == 'dev'", specifier = ">=7.2.0" },
{ name = "sphinx", marker = "extra == 'docs'", specifier = ">=7.2.0" },
@@ -764,7 +762,6 @@ requires-dist = [
{ name = "sqlparse", specifier = ">=0.4.4" },
{ name = "starlette", specifier = ">=0.27.0" },
{ name = "structlog", specifier = ">=23.2.0" },
- { name = "toml", specifier = ">=0.10.0" },
{ name = "tox", marker = "extra == 'dev'", specifier = ">=4.11.0" },
{ name = "tqdm", specifier = ">=4.66.0" },
{ name = "typer", specifier = ">=0.9.0" },
@@ -779,6 +776,11 @@ dev = [
{ name = "bandit", specifier = ">=1.8.3" },
{ name = "mypy", specifier = ">=1.16.0" },
{ name = "pandas-stubs", specifier = ">=2.2.0" },
+ { name = "pytest", specifier = ">=8.4.0" },
+ { name = "pytest-asyncio", specifier = ">=1.0.0" },
+ { name = "pytest-cov", specifier = ">=6.1.1" },
+ { name = "pytest-mock", specifier = ">=3.12.0" },
+ { name = "pytest-xdist", specifier = ">=3.5.0" },
{ name = "ruff", specifier = ">=0.11.13" },
{ name = "types-jsonschema", specifier = ">=4.23.0" },
]
@@ -2863,15 +2865,6 @@ version = "0.22.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url =
"https://files.pythonhosted.org/packages/b2/c2/db648cc10dd7d15560f2eafd92a27cd280811924696e0b4a87175fb28c94/thrift-0.22.0.tar.gz",
hash =
"sha256:42e8276afbd5f54fe1d364858b6877bc5e5a4a5ed69f6a005b94ca4918fe1466", size
= 62303, upload-time = "2025-05-23T20:49:33.309Z" }
-[[package]]
-name = "toml"
-version = "0.10.2"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url =
"https://files.pythonhosted.org/packages/be/ba/1f744cdc819428fc6b5084ec34d9b30660f6f9daaf70eead706e3203ec3c/toml-0.10.2.tar.gz",
hash =
"sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f", size
= 22253, upload-time = "2020-11-01T01:40:22.204Z" }
-wheels = [
- { url =
"https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl",
hash =
"sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size
= 16588, upload-time = "2020-11-01T01:40:20.672Z" },
-]
-
[[package]]
name = "tomlkit"
version = "0.13.3"
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]