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/.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/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" diff --git a/scripts/prepare_readme.py b/scripts/prepare_readme.py index 1b0a56ec..c220a055 100644 --- a/scripts/prepare_readme.py +++ b/scripts/prepare_readme.py @@ -1,35 +1,107 @@ -"""Code generated by Speakeasy (https://speakeasy.com). DO NOT EDIT.""" - +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