Skip to main content
Glama
by mckinsey
pycafe_utils.py8.96 kB
"""Utility functions for generating PyCafe links.""" import base64 import gzip import json import re import textwrap from dataclasses import dataclass from urllib.parse import quote, urlencode import requests from github import Auth, Github from github.Commit import Commit from github.Repository import Repository from playwright.sync_api import sync_playwright # Function to extract version string from file content using regex def _extract_version(content: str) -> str: version_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) if version_match: return version_match.group(1) return "unknown" def fetch_package_versions(repo_name: str, commit_sha: str) -> tuple[str, str]: """Fetch package versions directly from the repository files. This function retrieves the version strings from the __init__.py files of vizro and vizro-ai packages for the specific commit being tested. Args: repo_name: Name of the GitHub repository commit_sha: The commit SHA to fetch versions from Returns: A tuple with (vizro_version, vizro_ai_version) """ vizro_version = "unknown" vizro_ai_version = "unknown" # Define paths to __init__.py files that contain version information version_files = {"vizro": "vizro-core/src/vizro/__init__.py", "vizro-ai": "vizro-ai/src/vizro_ai/__init__.py"} # Fetch each file and extract the version for package, file_path in version_files.items(): url = f"https://raw.githubusercontent.com/{repo_name}/{commit_sha}/{file_path}" try: response = requests.get(url, timeout=10) response.raise_for_status() content = response.text # Update the appropriate version version = _extract_version(content) if package == "vizro" and version != "unknown": vizro_version = version elif package == "vizro-ai" and version != "unknown": vizro_ai_version = version except Exception as e: print(f"Failed to fetch version for {package}: {str(e)}") # noqa return vizro_version, vizro_ai_version @dataclass class PyCafeConfig: """Configuration for PyCafe link generation.""" github_token: str repo_name: str run_id: str commit_sha: str pr_number: int | None = None pycafe_url: str = "https://py.cafe" vizro_raw_url: str = "https://raw.githubusercontent.com/mckinsey/vizro" vizro_version: str = "unknown" vizro_ai_version: str = "unknown" def create_github_client(config: PyCafeConfig) -> tuple[Repository, Commit]: """Create GitHub client and return repo and commit objects.""" auth = Auth.Token(config.github_token) github = Github(auth=auth) repo = github.get_repo(config.repo_name) commit = repo.get_commit(config.commit_sha) return repo, commit def _get_vizro_requirement(config: PyCafeConfig, use_latest_release: bool = False) -> str: """Get the Vizro requirement string for PyCafe.""" if use_latest_release: return "vizro" return ( f"{config.pycafe_url}/gh/artifact/mckinsey/vizro/actions/runs/{config.run_id}/" f"pip/vizro-{config.vizro_version}-py3-none-any.whl" ) def _get_vizro_ai_requirement(config: PyCafeConfig, use_latest_release: bool = False) -> str: """Get the Vizro AI requirement string for PyCafe.""" if use_latest_release: return "vizro-ai" return ( f"{config.pycafe_url}/gh/artifact/mckinsey/vizro/actions/runs/{config.run_id}/" f"pip2/vizro_ai-{config.vizro_ai_version}-py3-none-any.whl" ) def _fetch_app_content(base_url: str) -> str: """Fetch and process app.py content from the repository.""" response = requests.get(f"{base_url}/app.py", timeout=10) response.raise_for_status() app_content = response.text app_content_split = app_content.split('if __name__ == "__main__":') if len(app_content_split) > 1: return app_content_split[0] + textwrap.dedent(app_content_split[1]) return app_content def _fetch_directory_files(config: PyCafeConfig, directory_path: str) -> list[dict]: """Fetch files in a directory from GitHub API.""" url = f"https://api.github.com/repos/{config.repo_name}/git/trees/{config.commit_sha}?recursive=1" response = requests.get(url, timeout=20) response.raise_for_status() files = response.json().get("tree", []) return [file for file in files if file["path"].startswith(directory_path)] def generate_link( config: PyCafeConfig, directory_path: str, extra_requirements: list[str] | None = None, use_latest_release: bool = False, ) -> str: """Generate a PyCafe link for the example dashboard.""" base_url = f"{config.vizro_raw_url}/{config.commit_sha}/{directory_path}" # Requirements - either use latest release or commit's wheel file requirements = [] if directory_path.startswith("vizro-ai/"): # An example in this folder may require the latest vizro-ai and vizro-core releases requirements.extend( [_get_vizro_ai_requirement(config, use_latest_release), _get_vizro_requirement(config, use_latest_release)] ) else: # All other examples do not require vizro-ai, but still the latest vizro-core release requirements.extend([_get_vizro_requirement(config, use_latest_release)]) if extra_requirements: requirements.extend(extra_requirements) # App file - get current commit, and modify to remove if clause app_content = _fetch_app_content(base_url) # Get directory files folder_files = _fetch_directory_files(config, directory_path) # JSON object json_object = { "code": app_content, "requirements": "\n".join(requirements), "files": [ { "name": file["path"].removeprefix(f"{directory_path}"), "url": f"{base_url}{file['path'].removeprefix(f'{directory_path}')}", } for file in folder_files if file["type"] == "blob" and file["path"] not in {f"{directory_path}/app.py", f"{directory_path}/requirements.txt"} ], } json_text = json.dumps(json_object) compressed_json_text = gzip.compress(json_text.encode("utf8")) base64_text = base64.b64encode(compressed_json_text).decode("utf8") query = urlencode({"c": base64_text}, quote_via=quote) return f"{config.pycafe_url}/snippet/vizro/v1?{query}" def generate_comparison_links( config: PyCafeConfig, directory_path: str, extra_requirements: list[str] | None = None ) -> dict[str, str]: """Generate both commit and release links for comparison.""" return { "commit": generate_link(config, directory_path, extra_requirements, use_latest_release=False), "release": generate_link(config, directory_path, extra_requirements, use_latest_release=True), } def create_status_check( commit: Commit, directory: str, url: str, state: str = "success", description: str = "Test out the app live on PyCafe", ): """Create a GitHub status check for a PyCafe link.""" context = f"PyCafe Example ({directory})" commit.create_status(state=state, target_url=url, description=description, context=context) print(f"Status created for {context} with URL: {url}") # noqa def get_example_directories() -> dict[str, list[str] | None]: """Return a dictionary of example directories and their requirements.""" return { "vizro-core/examples/scratch_dev": None, "vizro-core/examples/dev/": ["openpyxl"], "vizro-core/examples/visual-vocabulary/": [ "autoflake==2.3.1", "black==24.4.2", "isort==5.13.2", "plotly==5.24.1", ], "vizro-core/examples/tutorial/": None, } def test_pycafe_link(url: str, wait_for_text: str | bool, wait_for_locator: str | bool) -> int: """Test if a PyCafe link loads and renders correctly. Return code is showing appropriate exit code. Success = 0, Failure = 1. """ with sync_playwright() as p: browser = p.chromium.launch(headless=True) page = browser.new_page() try: # Navigate to the page page.goto(url, timeout=60000) # Get the app frame and wait for title or element frame = page.frame_locator("#app") if wait_for_text: frame.get_by_text(wait_for_text).wait_for(timeout=90000) if wait_for_locator: frame.locator(wait_for_locator).wait_for(timeout=90000) print(f"✅ Successfully verified PyCafe link: {url}") # noqa return 0 except Exception as e: print(f"❌ Failed to verify PyCafe link: {url}") # noqa print(f"Error: {str(e)}") # noqa return 1 finally: browser.close()

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mckinsey/vizro'

If you have feedback or need assistance with the MCP directory API, please join our Discord server