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
3 changes: 2 additions & 1 deletion .genignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ pyproject.toml
examples/*
/utils/*
src/mistral/extra/*
pylintrc
pylintrc
scripts/prepare_readme.py
1 change: 0 additions & 1 deletion .github/workflows/lint_custom_code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/run_example_scripts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/test_custom_code.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
1 change: 0 additions & 1 deletion .github/workflows/update_speakeasy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions packages/mistralai_azure/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
pythonpath = ["src"]

[tool.mypy]
Expand All @@ -62,4 +61,3 @@ ignore_missing_imports = true
[tool.pyright]
venvPath = "."
venv = ".venv"

2 changes: 1 addition & 1 deletion packages/mistralai_azure/scripts/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 1 addition & 3 deletions packages/mistralai_gcp/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
Expand Down Expand Up @@ -48,7 +48,6 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
pythonpath = ["src"]

[tool.mypy]
Expand All @@ -65,4 +64,3 @@ ignore_missing_imports = true
[tool.pyright]
venvPath = "."
venv = ".venv"

9 changes: 0 additions & 9 deletions packages/mistralai_gcp/scripts/prepare_readme.py

This file was deleted.

4 changes: 1 addition & 3 deletions packages/mistralai_gcp/scripts/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 1 addition & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -89,7 +89,6 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
pythonpath = ["src"]

[tool.mypy]
Expand Down
6 changes: 6 additions & 0 deletions scripts/lint_custom_code.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"
Expand All @@ -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"
Expand Down
134 changes: 103 additions & 31 deletions scripts/prepare_readme.py
Original file line number Diff line number Diff line change
@@ -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:]))
4 changes: 1 addition & 3 deletions scripts/publish.sh
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions tests/test_prepare_readme.py
Original file line number Diff line number Diff line change
@@ -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