Skip to content

Commit fd2ab3d

Browse files
committed
Add generate_api_text.py script.
1 parent 779c8e1 commit fd2ab3d

1 file changed

Lines changed: 189 additions & 0 deletions

File tree

scripts/generate_api_text.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
#!/usr/bin/env python
2+
"""Generate API.md for an Azure SDK package.
3+
4+
Usage:
5+
python scripts/generate_api_text.py azure-ai-projects
6+
"""
7+
8+
import argparse
9+
import glob
10+
import os
11+
import shutil
12+
import subprocess
13+
import sys
14+
import tempfile
15+
16+
17+
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
18+
APIVIEW_REQS = os.path.join(REPO_ROOT, "eng", "apiview_reqs.txt")
19+
AZURE_SDK_INDEX = "https://pkgs.dev.azure.com/azure-sdk/public/_packaging/azure-sdk-for-python/pypi/simple/"
20+
EXPORT_SCRIPT = os.path.join(REPO_ROOT, "eng", "common", "scripts", "Export-APIViewMarkdown.ps1")
21+
22+
23+
def find_package_dir(package_name: str) -> str:
24+
"""Find the package directory under sdk/*/{package_name}/."""
25+
pattern = os.path.join(REPO_ROOT, "sdk", "*", package_name)
26+
matches = glob.glob(pattern)
27+
# Filter to directories that contain a pyproject.toml or setup.py
28+
valid = [
29+
m for m in matches
30+
if os.path.isdir(m) and (
31+
os.path.exists(os.path.join(m, "pyproject.toml"))
32+
or os.path.exists(os.path.join(m, "setup.py"))
33+
)
34+
]
35+
if not valid:
36+
raise FileNotFoundError(f"Package '{package_name}' not found under sdk/*/")
37+
if len(valid) > 1:
38+
raise ValueError(f"Multiple matches for '{package_name}': {valid}")
39+
return valid[0]
40+
41+
42+
def get_installed_version(package: str) -> str | None:
43+
"""Get the currently installed version of a package, or None."""
44+
try:
45+
result = subprocess.run(
46+
[sys.executable, "-m", "pip", "show", package],
47+
capture_output=True, text=True, check=True,
48+
)
49+
for line in result.stdout.splitlines():
50+
if line.startswith("Version:"):
51+
return line.split(":", 1)[1].strip()
52+
except subprocess.CalledProcessError:
53+
pass
54+
return None
55+
56+
57+
def get_latest_version(package: str) -> str | None:
58+
"""Query the Azure SDK feed for the latest version of a package."""
59+
try:
60+
result = subprocess.run(
61+
[sys.executable, "-m", "pip", "index", "versions", package,
62+
"--index-url", AZURE_SDK_INDEX],
63+
capture_output=True, text=True, check=True,
64+
)
65+
# Output format: "apiview-stub-generator (0.3.28)"
66+
for line in result.stdout.splitlines():
67+
if package in line and "(" in line:
68+
version = line.split("(")[1].split(")")[0].strip()
69+
return version
70+
except subprocess.CalledProcessError:
71+
pass
72+
return None
73+
74+
75+
def ensure_latest_apiview_stub_generator():
76+
"""Ensure the latest apiview-stub-generator is installed from the Azure SDK feed."""
77+
installed = get_installed_version("apiview-stub-generator")
78+
latest = get_latest_version("apiview-stub-generator")
79+
80+
print(f"apiview-stub-generator: installed={installed}, latest={latest}")
81+
82+
if installed and latest and installed == latest:
83+
print("Already at latest version.")
84+
return
85+
86+
# Install from apiview_reqs.txt first (gets dependencies right)
87+
print("Installing apiview_reqs.txt...")
88+
subprocess.run(
89+
[sys.executable, "-m", "pip", "install", "-r", APIVIEW_REQS,
90+
f"--index-url={AZURE_SDK_INDEX}"],
91+
check=True,
92+
)
93+
94+
# Override with latest version (not the pinned one)
95+
print("Upgrading apiview-stub-generator to latest...")
96+
subprocess.run(
97+
[sys.executable, "-m", "pip", "install", "--upgrade", "apiview-stub-generator",
98+
f"--index-url={AZURE_SDK_INDEX}"],
99+
check=True,
100+
)
101+
102+
new_version = get_installed_version("apiview-stub-generator")
103+
print(f"apiview-stub-generator now at version {new_version}")
104+
105+
106+
def build_wheel(package_dir: str, output_dir: str) -> str:
107+
"""Build a wheel for the package and return the path to the .whl file."""
108+
subprocess.run(
109+
[sys.executable, "-m", "pip", "wheel", package_dir, "--no-deps", "-w", output_dir],
110+
check=True,
111+
)
112+
whls = glob.glob(os.path.join(output_dir, "*.whl"))
113+
if not whls:
114+
raise FileNotFoundError(f"No .whl file found in {output_dir}")
115+
return whls[0]
116+
117+
118+
def run_apistub(whl_path: str, out_path: str):
119+
"""Run apiview-stub-generator on the wheel."""
120+
subprocess.run(
121+
[sys.executable, "-m", "apistub",
122+
"--pkg-path", whl_path,
123+
"--out-path", out_path,
124+
"--skip-pylint"],
125+
check=True,
126+
)
127+
128+
129+
def export_api_markdown(token_json_path: str, output_path: str):
130+
"""Run the Export-APIViewMarkdown.ps1 script to convert token JSON to API.md."""
131+
subprocess.run(
132+
["pwsh", EXPORT_SCRIPT, "-TokenJsonPath", token_json_path, "-OutputPath", output_path],
133+
check=True,
134+
)
135+
136+
137+
def main():
138+
parser = argparse.ArgumentParser(description="Generate API.md for an Azure SDK package.")
139+
parser.add_argument("package", help="Package name (e.g. azure-ai-projects)")
140+
args = parser.parse_args()
141+
142+
package_name = args.package
143+
print(f"Generating API.md for {package_name}...")
144+
145+
# Find the package
146+
package_dir = find_package_dir(package_name)
147+
print(f"Found package at: {package_dir}")
148+
149+
# Ensure latest apiview-stub-generator
150+
ensure_latest_apiview_stub_generator()
151+
152+
# Build wheel in a temp directory
153+
tmp_dir = tempfile.mkdtemp(prefix="apistub_")
154+
try:
155+
print("Building wheel...")
156+
whl_path = build_wheel(package_dir, tmp_dir)
157+
print(f"Built: {os.path.basename(whl_path)}")
158+
159+
# Run apiview-stub-generator
160+
print("Running apiview-stub-generator...")
161+
run_apistub(whl_path, tmp_dir)
162+
163+
# Find the generated token JSON
164+
token_json = os.path.join(tmp_dir, f"{package_name}_python.json")
165+
if not os.path.exists(token_json):
166+
# Try with underscores (package name normalization)
167+
normalized = package_name.replace("-", "_")
168+
token_json = os.path.join(tmp_dir, f"{normalized}_python.json")
169+
if not os.path.exists(token_json):
170+
# Find any json file
171+
jsons = glob.glob(os.path.join(tmp_dir, "*_python.json"))
172+
if jsons:
173+
token_json = jsons[0]
174+
else:
175+
raise FileNotFoundError(f"No token JSON found in {tmp_dir}")
176+
177+
# Export to API.md
178+
api_md_path = os.path.join(package_dir, "API.md")
179+
print("Exporting API.md...")
180+
export_api_markdown(token_json, api_md_path)
181+
print(f"Generated: {api_md_path}")
182+
183+
finally:
184+
# Clean up temp directory (wheel + token json)
185+
shutil.rmtree(tmp_dir, ignore_errors=True)
186+
187+
188+
if __name__ == "__main__":
189+
main()

0 commit comments

Comments
 (0)