/ 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()