From a173de03c2d1f302377e8ab64502fb8df83af4e6 Mon Sep 17 00:00:00 2001 From: jean-malo Date: Wed, 31 Dec 2025 15:24:09 +0100 Subject: [PATCH 1/3] feat!: unify README handling across packages This commit standardizes the way README files are handled during builds and publishes across all packages. The main changes include: 1. Removing the need for separate README-PYPI.md files by using a more flexible approach 2. Updating the prepare_readme.py script to handle relative links more robustly 3. Simplifying the build and publish workflows by removing redundant steps 4. Adding tests for the README preparation functionality The changes make the build process more consistent across different packages and reduce duplication in the workflow files. The new approach also provides better handling of relative links in README files during package publication. BREAKING CHANGE: The way README files are prepared for publication has changed, which may affect custom build processes that relied on the previous approach. --- .github/workflows/lint_custom_code.yaml | 1 - .github/workflows/run_example_scripts.yaml | 1 - .github/workflows/test_custom_code.yaml | 4 +- .github/workflows/update_speakeasy.yaml | 1 - packages/mistralai_azure/pyproject.toml | 2 - packages/mistralai_azure/scripts/publish.sh | 2 +- packages/mistralai_gcp/pyproject.toml | 4 +- .../mistralai_gcp/scripts/prepare_readme.py | 9 -- packages/mistralai_gcp/scripts/publish.sh | 4 +- pyproject.toml | 3 +- scripts/prepare_readme.py | 136 ++++++++++++++---- scripts/publish.sh | 4 +- tests/test_prepare_readme.py | 37 +++++ 13 files changed, 151 insertions(+), 57 deletions(-) delete mode 100644 packages/mistralai_gcp/scripts/prepare_readme.py create mode 100644 tests/test_prepare_readme.py diff --git a/.github/workflows/lint_custom_code.yaml b/.github/workflows/lint_custom_code.yaml index f6147b55..9dcb04e4 100644 --- a/.github/workflows/lint_custom_code.yaml +++ b/.github/workflows/lint_custom_code.yaml @@ -26,7 +26,6 @@ jobs: - name: Install dependencies run: | - touch README-PYPI.md uv sync --all-extras # The init, sdkhooks.py and types.py files in the _hooks folders are generated by Speakeasy hence the exclusion diff --git a/.github/workflows/run_example_scripts.yaml b/.github/workflows/run_example_scripts.yaml index 84896d26..cecefb0e 100644 --- a/.github/workflows/run_example_scripts.yaml +++ b/.github/workflows/run_example_scripts.yaml @@ -39,7 +39,6 @@ jobs: - name: Build the package run: | - touch README-PYPI.md # Create this file since the client is not built by Speakeasy uv build - name: Install client with extras and run all examples. diff --git a/.github/workflows/test_custom_code.yaml b/.github/workflows/test_custom_code.yaml index 8a22fcb1..9a53c1e5 100644 --- a/.github/workflows/test_custom_code.yaml +++ b/.github/workflows/test_custom_code.yaml @@ -27,8 +27,10 @@ jobs: - name: Install dependencies run: | - touch README-PYPI.md uv sync --all-extras - name: Run the 'src/mistralai/extra' package unit tests run: uv run python3.12 -m unittest discover -s src/mistralai/extra/tests -t src + + - name: Run pytest for repository tests + run: uv run pytest tests/ diff --git a/.github/workflows/update_speakeasy.yaml b/.github/workflows/update_speakeasy.yaml index f596cf66..9628bffa 100644 --- a/.github/workflows/update_speakeasy.yaml +++ b/.github/workflows/update_speakeasy.yaml @@ -38,7 +38,6 @@ jobs: - name: Install dependencies run: | - cp README.md README-PYPI.md uv sync --group dev --no-default-groups - name: Install Speakeasy CLI diff --git a/packages/mistralai_azure/pyproject.toml b/packages/mistralai_azure/pyproject.toml index 2842c215..016378d5 100644 --- a/packages/mistralai_azure/pyproject.toml +++ b/packages/mistralai_azure/pyproject.toml @@ -43,7 +43,6 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" pythonpath = ["src"] [tool.mypy] @@ -62,4 +61,3 @@ ignore_missing_imports = true [tool.pyright] venvPath = "." venv = ".venv" - diff --git a/packages/mistralai_azure/scripts/publish.sh b/packages/mistralai_azure/scripts/publish.sh index f2f31e59..0c07c589 100755 --- a/packages/mistralai_azure/scripts/publish.sh +++ b/packages/mistralai_azure/scripts/publish.sh @@ -2,5 +2,5 @@ export UV_PUBLISH_TOKEN=${PYPI_TOKEN} -uv build +uv run python ../../scripts/prepare_readme.py --repo-subdir packages/mistralai_azure -- uv build uv publish diff --git a/packages/mistralai_gcp/pyproject.toml b/packages/mistralai_gcp/pyproject.toml index 650ef73b..79b8193b 100644 --- a/packages/mistralai_gcp/pyproject.toml +++ b/packages/mistralai_gcp/pyproject.toml @@ -4,7 +4,7 @@ version = "1.6.0" description = "Python Client SDK for the Mistral AI API in GCP." authors = [{ name = "Mistral" }] requires-python = ">=3.10" -readme = "README-PYPI.md" +readme = "README.md" dependencies = [ "eval-type-backport >=0.2.0", "google-auth (>=2.31.0,<3.0.0)", @@ -48,7 +48,6 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" pythonpath = ["src"] [tool.mypy] @@ -65,4 +64,3 @@ ignore_missing_imports = true [tool.pyright] venvPath = "." venv = ".venv" - diff --git a/packages/mistralai_gcp/scripts/prepare_readme.py b/packages/mistralai_gcp/scripts/prepare_readme.py deleted file mode 100644 index 825d9ded..00000000 --- a/packages/mistralai_gcp/scripts/prepare_readme.py +++ /dev/null @@ -1,9 +0,0 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - -import shutil - -try: - shutil.copyfile("README.md", "README-PYPI.md") -except Exception as e: - print("Failed to copy README.md to README-PYPI.md") - print(e) diff --git a/packages/mistralai_gcp/scripts/publish.sh b/packages/mistralai_gcp/scripts/publish.sh index d2bef9f7..e9eb1f0b 100755 --- a/packages/mistralai_gcp/scripts/publish.sh +++ b/packages/mistralai_gcp/scripts/publish.sh @@ -2,7 +2,5 @@ export UV_PUBLISH_TOKEN=${PYPI_TOKEN} -uv run python scripts/prepare_readme.py - -uv build +uv run python ../../scripts/prepare_readme.py --repo-subdir packages/mistralai_gcp -- uv build uv publish diff --git a/pyproject.toml b/pyproject.toml index 933a3162..3c5b4574 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ version = "1.10.0" description = "Python Client SDK for the Mistral AI API." authors = [{ name = "Mistral" }] requires-python = ">=3.10" -readme = "README-PYPI.md" +readme = "README.md" dependencies = [ "eval-type-backport >=0.2.0", "httpx >=0.28.1", @@ -89,7 +89,6 @@ requires = ["hatchling"] build-backend = "hatchling.build" [tool.pytest.ini_options] -asyncio_default_fixture_loop_scope = "function" pythonpath = ["src"] [tool.mypy] diff --git a/scripts/prepare_readme.py b/scripts/prepare_readme.py index 1b0a56ec..6fa672f8 100644 --- a/scripts/prepare_readme.py +++ b/scripts/prepare_readme.py @@ -1,35 +1,111 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" +"""Rewrite README links for PyPI builds and run a command or print to stdout.""" +from __future__ import annotations + +import argparse import re -import shutil - -try: - with open("README.md", "r", encoding="utf-8") as rh: - readme_contents = rh.read() - GITHUB_URL = "https://github.com/mistralai/client-python.git" - GITHUB_URL = ( - GITHUB_URL[: -len(".git")] if GITHUB_URL.endswith(".git") else GITHUB_URL +import subprocess +import sys +from pathlib import Path + +DEFAULT_REPO_URL = "https://github.com/mistralai/client-python.git" +DEFAULT_BRANCH = "main" +LINK_PATTERN = re.compile(r"(\[[^\]]+\]\()((?!https?:)[^\)]+)(\))") + + +def build_base_url(repo_url: str, branch: str, repo_subdir: str) -> str: + """Build the GitHub base URL used to rewrite relative README links.""" + normalized_repo_url = repo_url[:-4] if repo_url.endswith(".git") else repo_url + normalized_subdir = repo_subdir.strip("/") + if normalized_subdir: + normalized_subdir = f"{normalized_subdir}/" + return f"{normalized_repo_url}/blob/{branch}/{normalized_subdir}" + + +def rewrite_relative_links(contents: str, base_url: str) -> str: + """Rewrite Markdown relative links to absolute GitHub URLs.""" + return LINK_PATTERN.sub( + lambda match: f"{match.group(1)}{base_url}{match.group(2)}{match.group(3)}", + contents, + ) + + +def run_with_rewritten_readme( + readme_path: Path, base_url: str, command: list[str] +) -> int: + """Rewrite README links, run a command, and restore the original README.""" + original_contents = readme_path.read_text(encoding="utf-8") + rewritten_contents = rewrite_relative_links(original_contents, base_url) + readme_path.write_text(rewritten_contents, encoding="utf-8") + try: + if not command: + return 0 + result = subprocess.run(command, check=False) + return result.returncode + finally: + readme_path.write_text(original_contents, encoding="utf-8") + + +def parse_args(argv: list[str]) -> argparse.Namespace: + """Parse command-line arguments for README rewriting.""" + parser = argparse.ArgumentParser( + description=( + "Rewrite README links to absolute GitHub URLs while running a command." ) - REPO_SUBDIR = "" - # links on PyPI should have absolute URLs - readme_contents = re.sub( - r"(\[[^\]]+\]\()((?!https?:)[^\)]+)(\))", - lambda m: m.group(1) - + GITHUB_URL - + "/blob/master/" - + REPO_SUBDIR - + m.group(2) - + m.group(3), - readme_contents, + ) + parser.add_argument( + "--readme", + type=Path, + default=Path("README.md"), + help="Path to the README file to rewrite.", + ) + parser.add_argument( + "--repo-url", + default=DEFAULT_REPO_URL, + help="Repository URL used to build absolute links.", + ) + parser.add_argument( + "--branch", + default=DEFAULT_BRANCH, + help="Repository branch used for absolute links.", + ) + parser.add_argument( + "--repo-subdir", + default="", + help="Repository subdirectory that contains the README.", + ) + parser.add_argument( + "command", + nargs=argparse.REMAINDER, + help=( + "Command to run (prefix with -- to stop option parsing). " + "If omitted, the rewritten README is printed to stdout." + ), + ) + return parser.parse_args(argv) + + +def main(argv: list[str]) -> int: + """Entry point for rewriting README links during build commands.""" + args = parse_args(argv) + readme_path = args.readme + if not readme_path.is_file(): + raise FileNotFoundError(f"README file not found: {readme_path}") + base_url = build_base_url(args.repo_url, args.branch, args.repo_subdir) + command = ( + args.command[1:] + if args.command and args.command[0] == "--" + else args.command + ) + if not command: + rewritten_contents = rewrite_relative_links( + readme_path.read_text(encoding="utf-8"), + base_url, ) + sys.stdout.write(rewritten_contents) + return 0 + return run_with_rewritten_readme(readme_path, base_url, command) - with open("README-PYPI.md", "w", encoding="utf-8") as wh: - wh.write(readme_contents) -except Exception as e: - try: - print("Failed to rewrite README.md to README-PYPI.md, copying original instead") - print(e) - shutil.copyfile("README.md", "README-PYPI.md") - except Exception as ie: - print("Failed to copy README.md to README-PYPI.md") - print(ie) + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/scripts/publish.sh b/scripts/publish.sh index 6ff725f3..c41f3efb 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -1,7 +1,5 @@ #!/usr/bin/env bash export UV_PUBLISH_TOKEN=${PYPI_TOKEN} -uv run python scripts/prepare_readme.py - -uv build +uv run python scripts/prepare_readme.py -- uv build uv publish diff --git a/tests/test_prepare_readme.py b/tests/test_prepare_readme.py new file mode 100644 index 00000000..ce3e11c9 --- /dev/null +++ b/tests/test_prepare_readme.py @@ -0,0 +1,37 @@ +import importlib.util +from pathlib import Path + +SCRIPT_PATH = Path(__file__).resolve().parents[1] / "scripts" / "prepare_readme.py" +SPEC = importlib.util.spec_from_file_location("prepare_readme", SCRIPT_PATH) +if SPEC is None or SPEC.loader is None: + raise ImportError(f"Unable to load prepare_readme from {SCRIPT_PATH}") +prepare_readme = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(prepare_readme) + + +def test_rewrite_relative_links_keeps_absolute() -> None: + base_url = "https://example.com/blob/main/" + contents = "[Migration](MIGRATION.md)\n[Docs](https://docs.mistral.ai)" + expected = ( + "[Migration](https://example.com/blob/main/MIGRATION.md)\n" + "[Docs](https://docs.mistral.ai)" + ) + assert prepare_readme.rewrite_relative_links(contents, base_url) == expected + + +def test_main_prints_rewritten_readme_with_defaults(tmp_path, capsys) -> None: + original = "[Migration](MIGRATION.md)\n" + base_url = prepare_readme.build_base_url( + prepare_readme.DEFAULT_REPO_URL, + prepare_readme.DEFAULT_BRANCH, + "", + ) + expected = f"[Migration]({base_url}MIGRATION.md)\n" + readme_path = tmp_path / "README.md" + readme_path.write_text(original, encoding="utf-8") + + exit_code = prepare_readme.main(["--readme", str(readme_path)]) + + captured = capsys.readouterr() + assert exit_code == 0 + assert captured.out == expected From 67cf6a5ab51a6755125ec23630560270b222076a Mon Sep 17 00:00:00 2001 From: jean-malo Date: Wed, 31 Dec 2025 15:26:59 +0100 Subject: [PATCH 2/3] ignore --- .genignore | 3 ++- scripts/prepare_readme.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/.genignore b/.genignore index 3ef32897..b80cf0f6 100644 --- a/.genignore +++ b/.genignore @@ -2,4 +2,5 @@ pyproject.toml examples/* /utils/* src/mistral/extra/* -pylintrc \ No newline at end of file +pylintrc +scripts/prepare_readme.py diff --git a/scripts/prepare_readme.py b/scripts/prepare_readme.py index 6fa672f8..c220a055 100644 --- a/scripts/prepare_readme.py +++ b/scripts/prepare_readme.py @@ -1,7 +1,3 @@ -"""Rewrite README links for PyPI builds and run a command or print to stdout.""" - -from __future__ import annotations - import argparse import re import subprocess From d0904db122dc5764580a8e63b9fcf323cc00a5a4 Mon Sep 17 00:00:00 2001 From: jean-malo Date: Wed, 31 Dec 2025 15:28:11 +0100 Subject: [PATCH 3/3] chore: add linting for scripts directory This commit adds mypy, pyright, and ruff linting checks for the scripts directory to ensure code quality and consistency across the project. The changes include: - Adding mypy checks for the scripts directory - Adding pyright checks for the scripts directory - Adding ruff checks for the scripts directory This helps maintain consistent code quality standards across all parts of the codebase. --- scripts/lint_custom_code.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/scripts/lint_custom_code.sh b/scripts/lint_custom_code.sh index 3b03883d..7c084463 100755 --- a/scripts/lint_custom_code.sh +++ b/scripts/lint_custom_code.sh @@ -10,6 +10,8 @@ uv run mypy src/mistralai/extra/ || ERRORS=1 echo "-> running on hooks" uv run mypy src/mistralai/_hooks/ \ --exclude __init__.py --exclude sdkhooks.py --exclude types.py || ERRORS=1 +echo "-> running on scripts" +uv run mypy scripts/ || ERRORS=1 echo "Running pyright..." # TODO: Uncomment once the examples are fixed @@ -18,6 +20,8 @@ echo "-> running on extra" uv run pyright src/mistralai/extra/ || ERRORS=1 echo "-> running on hooks" uv run pyright src/mistralai/_hooks/ || ERRORS=1 +echo "-> running on scripts" +uv run pyright scripts/ || ERRORS=1 echo "Running ruff..." echo "-> running on examples" @@ -27,6 +31,8 @@ uv run ruff check src/mistralai/extra/ || ERRORS=1 echo "-> running on hooks" uv run ruff check src/mistralai/_hooks/ \ --exclude __init__.py --exclude sdkhooks.py --exclude types.py || ERRORS=1 +echo "-> running on scripts" +uv run ruff check scripts/ || ERRORS=1 if [ "$ERRORS" -ne 0 ]; then echo "❌ One or more linters failed"