From e6eeba584e29837e4013b4f0a4a8f60136f71397 Mon Sep 17 00:00:00 2001 From: FreeOnePlus Date: Thu, 30 Jul 2026 15:34:06 +0800 Subject: [PATCH] build: separate runtime and development dependencies --- .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_github_actions_contract.py | 1 + uv.lock | 39 ++- 11 files changed, 426 insertions(+), 218 deletions(-) create mode 100644 test/deployment/check_runtime_dependencies.py create mode 100644 test/deployment/test_dependency_layers.py 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"