/ api-reference / generate.py
generate.py
1 #!/usr/bin/env -S uv run --script 2 # /// script 3 # requires-python = ">=3.11" 4 # dependencies = [ 5 # "pdoc>=16.0.0", 6 # "tracely>=0.2.12", 7 # "typer>=0.3", 8 # ] 9 # /// 10 11 """ 12 CLI script to generate api reference documentation for Evidently. 13 """ 14 15 import os 16 import re 17 import subprocess 18 import sys 19 from pathlib import Path 20 21 from typer import BadParameter 22 from typer import Option 23 from typer import Typer 24 from typer import echo 25 26 # Constants 27 EVIDENTLY_GITHUB_REPO = "evidentlyai/evidently" 28 THEME_DIR = "evidently-theme" 29 OUTPUT_DIR = "dist" 30 SQL_EXTRAS = "[sql]" 31 32 # Get the script's directory (api-reference directory) to ensure paths are always correct 33 SCRIPT_DIR = Path(__file__).parent.resolve() 34 REPO_DIR = SCRIPT_DIR.parent.resolve() 35 THEME_DIR_PATH = SCRIPT_DIR / THEME_DIR 36 OUTPUT_DIR_PATH = SCRIPT_DIR / OUTPUT_DIR 37 MAIN_MODULES = ["evidently", "evidently.core"] 38 39 40 # pdoc flags (will be set dynamically to use absolute paths) 41 def get_pdoc_flags(output_path: str, github_blob_url: str) -> list[str]: 42 """Get pdoc flags with correct theme path.""" 43 44 blob_flags = ["-e", f"evidently={github_blob_url}"] if github_blob_url else [] 45 output_flags = ["-o", output_path] 46 47 return [ 48 # "--no-include-undocumented", 49 "--no-show-source", 50 "-t", 51 str(THEME_DIR_PATH), 52 "--logo", 53 "https://demo.evidentlyai.com/static/img/evidently-ai-logo.png", 54 "--favicon", 55 "https://demo.evidentlyai.com/favicon.ico", 56 *blob_flags, 57 *output_flags, 58 ] 59 60 61 def becho(message: str) -> None: 62 """Print blue message.""" 63 print(f"\033[36m{message}\033[0m") 64 65 66 def yecho(message: str) -> None: 67 """Print yellow message.""" 68 print(f"\033[33m{message}\033[0m") 69 70 71 def merge_additional_modules_with_defaults(modules: list[str]) -> list[str]: 72 return list(set([*MAIN_MODULES, *modules])) 73 74 75 def build_uv_run_flags(uv_run_flags: str = "", no_cache: bool = False) -> list[str]: 76 """Build uv run flags list with defaults.""" 77 DEFAULT_FLAGS = ["--no-project"] 78 79 flags = [] 80 flags.extend(DEFAULT_FLAGS) 81 82 if uv_run_flags: 83 flags.extend(uv_run_flags.split()) 84 if no_cache: 85 flags.append("--no-cache") 86 87 return flags 88 89 90 def add_extras_to_ref(ref: str) -> str: 91 extras = SQL_EXTRAS 92 93 if extras in ref: 94 return ref 95 96 # Result: "git+...@v0.7.16[sql]" or "/path/to/evidently[sql]" 97 return f"{ref}{extras}" 98 99 100 def build_with_flag_for_evidently(evidently_ref: str) -> list[str]: 101 """Build dependency flags list with the appropriate flag and ref pair.""" 102 evidently_ref = add_extras_to_ref(evidently_ref) 103 104 is_local_path = evidently_ref.startswith("/") 105 flag = "--with-editable" if is_local_path else "--with" 106 107 return [flag, evidently_ref] 108 109 110 def get_revision_info(revision: str) -> tuple[str, str]: 111 # Check if it's a hash (hexadecimal string, typically 7-40 characters) 112 revision = revision.lower() 113 if re.match(r"^[0-9a-f]{7,40}$", revision): 114 return (revision, f"hash: {revision}") 115 # Check if it's a semver version (e.g., v1.2.3, 1.2.3) 116 elif re.match(r"^v?\d+\.\d+\.\d+$", revision): 117 return (revision, revision) 118 119 return (revision.replace("/", "-"), f"branch: {revision}") 120 121 122 def format_local_path(path: Path) -> str: 123 """Format a local file system path into a clean directory name format.""" 124 path_str = str(path).lower() 125 126 if path_str.startswith("/"): 127 return path_str[1:].replace("/", "-") 128 129 return path_str.replace("/", "-") 130 131 132 def build_github_repo_url(github_repo: str) -> str: 133 """Build full GitHub repository URL from repo identifier (e.g., 'owner/repo').""" 134 return f"https://github.com/{github_repo}" 135 136 137 def generate_docs_impl( 138 *, 139 revision: str | None, 140 local_source: bool, 141 no_cache: bool, 142 uv_run_flags: str, 143 modules: list[str], 144 github_repo: str, 145 api_reference_index_href: str, 146 output_prefix: str = "", 147 ) -> None: 148 repo_url = build_github_repo_url(github_repo) 149 github_blob_prefix = f"{repo_url}/blob" 150 151 github_blob_url = "" 152 output_path = OUTPUT_DIR_PATH / (output_prefix + format_local_path(REPO_DIR)) 153 version = f"Local: {REPO_DIR}" 154 155 if revision: 156 path_to_artifact, version = get_revision_info(revision) 157 github_blob_url = f"{github_blob_prefix}/{revision}/src/evidently/" 158 output_path = OUTPUT_DIR_PATH / (output_prefix + path_to_artifact) 159 evidently_ref = f"git+{repo_url}.git@{revision}" 160 161 if local_source: 162 evidently_ref = str(REPO_DIR) 163 becho("Generating documentation for local path...") 164 yecho(evidently_ref) 165 else: 166 becho("Generating documentation for git revision...") 167 yecho(revision) 168 169 run_pdoc( 170 version=version, 171 evidently_ref=evidently_ref, 172 github_blob_url=github_blob_url, 173 output_path=str(output_path), 174 no_cache=no_cache, 175 uv_run_flags=uv_run_flags, 176 modules=modules, 177 api_reference_index_href=api_reference_index_href, 178 ) 179 180 181 def run_pdoc( 182 *, 183 version: str, 184 evidently_ref: str, 185 github_blob_url: str, 186 output_path: str, 187 no_cache: bool = False, 188 uv_run_flags: str = "", 189 modules: list[str], 190 api_reference_index_href: str = "/", 191 ) -> None: 192 """Run pdoc command with the given parameters.""" 193 194 # Set environment variables 195 env = os.environ.copy() 196 env["VERSION"] = version 197 env["API_REFERENCE_INDEX_HREF"] = api_reference_index_href 198 199 cmd = [ 200 "uv", 201 "run", 202 *build_uv_run_flags(uv_run_flags, no_cache), 203 *build_with_flag_for_evidently(evidently_ref), 204 "pdoc", 205 *get_pdoc_flags(output_path, github_blob_url), 206 *modules, 207 ] 208 209 becho(" ".join(cmd)) 210 211 result = subprocess.run(cmd, env=env, check=False) 212 213 if result.returncode != 0: 214 echo(f"Error: Command failed with exit code {result.returncode}", err=True) 215 sys.exit(result.returncode) 216 217 218 app = Typer( 219 context_settings={"help_option_names": ["-h", "--help"]}, 220 add_completion=False, 221 ) 222 223 224 @app.callback(invoke_without_command=True) 225 def generate_docs( 226 github_repo: str = Option( 227 EVIDENTLY_GITHUB_REPO, 228 "--github-repo", 229 help="GitHub repository identifier (e.g., 'owner/repo', default: evidentlyai/evidently)", 230 ), 231 git_revision: str = Option( 232 None, 233 "--git-revision", 234 help="Git revision (branch, tag, or commit). Required unless --local-source-code is used. " 235 "When used with --local-source-code, sets the GitHub blob URL and output directory name.", 236 ), 237 local_source_code: bool = Option( 238 False, 239 "--local-source-code", 240 help="Generate documentation from local source code instead of fetching from git. " 241 "Can be combined with --git-revision for output naming.", 242 ), 243 # Additional flags 244 no_cache: bool = Option(False, "--no-cache", help="Disable cache for uv run"), 245 uv_run_flags: str = Option("", "--uv-run-flags", help="Additional flags to pass to uv run (space-separated)"), 246 additional_modules: str = Option( 247 "", "--additional-modules", help="Comma-separated list of additional modules to document" 248 ), 249 api_reference_index_href: str = Option( 250 "/", "--api-reference-index-href", help="Href path for the 'All versions' link (default: '/')" 251 ), 252 output_prefix: str = Option("", "--output-prefix", help="Prefix to add to output directory path (default: '')"), 253 ): 254 """Generate documentation for Evidently. 255 256 Usage modes: 257 --git-revision <rev> Build from git revision 258 --local-source-code Build from local source (output named by local path) 259 --local-source-code --git-revision <rev> Build from local source (output named by revision) 260 """ 261 # Validate: at least one of git_revision or local_source_code must be provided 262 if not git_revision and not local_source_code: 263 raise BadParameter("You must specify --git-revision and/or --local-source-code") 264 265 additional_modules_list = [m.strip() for m in additional_modules.split(",") if m.strip()] 266 modules = merge_additional_modules_with_defaults(additional_modules_list) 267 268 generate_docs_impl( 269 revision=git_revision, 270 local_source=local_source_code, 271 no_cache=no_cache, 272 uv_run_flags=uv_run_flags, 273 modules=modules, 274 github_repo=github_repo, 275 api_reference_index_href=api_reference_index_href, 276 output_prefix=output_prefix, 277 ) 278 279 becho("Done") 280 281 282 def main(): 283 """Main entry point.""" 284 app() 285 286 287 if __name__ == "__main__": 288 main()