ui.py
1 import os 2 import threading 3 import time 4 from typing import Optional 5 from typing import cast 6 7 import requests 8 import uuid6 9 from typer import BadParameter 10 from typer import Option 11 from typer import echo 12 13 from evidently.cli.main import app 14 from evidently.ui.service.app import get_config 15 from evidently.ui.service.demo_projects import DEMO_PROJECT_NAMES_FOR_CLI 16 from evidently.ui.service.demo_projects import DEMO_PROJECTS 17 from evidently.ui.service.demo_projects import DEMO_PROJECTS_NAMES 18 from evidently.ui.service.demo_projects import DemoProjectsNames 19 from evidently.ui.workspace import RemoteWorkspace 20 21 22 def setup_deterministic_generation_uuid(seed: int = 8754): 23 import uuid 24 25 from faker import Faker 26 27 Faker.seed(seed) 28 fake = Faker() 29 30 def deterministic_uuid() -> uuid.UUID: 31 return fake.uuid4(cast_to=None) 32 33 def deterministic_uuid6() -> uuid6.UUID: 34 return uuid6.UUID(int=deterministic_uuid().int) 35 36 uuid.uuid4 = deterministic_uuid 37 uuid6.uuid7 = deterministic_uuid6 38 39 40 def _create_demo_projects_task(demo_names: list[str], host: str, port: int, secret: Optional[str]): 41 """Background task that waits for server and creates demo projects.""" 42 base_url = f"http://{host}:{port}" 43 44 # Wait for server to be ready by polling /api/version 45 max_retries = 30 46 retry_delay = 0.5 47 for attempt in range(max_retries): 48 try: 49 response = requests.get(f"{base_url}/api/version", timeout=1) 50 if response.status_code == 200: 51 break 52 except Exception: 53 pass 54 55 if attempt < max_retries - 1: 56 time.sleep(retry_delay) 57 else: 58 echo(f"Warning: Could not connect to server at {base_url} after {max_retries} attempts") 59 return 60 61 # Create demo projects using RemoteWorkspace 62 try: 63 ws = RemoteWorkspace(base_url, secret=secret) 64 65 for demo_name in demo_names: 66 if demo_name not in DEMO_PROJECTS: 67 echo(f"Warning: Unknown demo project: {demo_name}") 68 continue 69 70 # TODO: better type safety 71 demo_project = DEMO_PROJECTS[cast(DemoProjectsNames, demo_name)] 72 73 # Check if project already exists 74 existing_projects = ws.list_projects() 75 has_demo_project = any(p.name == demo_project.name for p in existing_projects) 76 77 if not has_demo_project: 78 echo(f"Creating demo project '{demo_project.name}'...") 79 demo_project.create(ws) 80 echo(f"Demo project '{demo_project.name}' created successfully") 81 else: 82 echo(f"Demo project '{demo_project.name}' already exists, skipping") 83 84 except Exception as e: 85 echo(f"Error creating demo projects: {e}") 86 87 88 @app.command("ui") 89 def ui( 90 host: str = Option("127.0.0.1", help="Service host"), 91 port: int = Option(8000, help="Service port"), 92 workspace: str = Option("workspace", help="Path to workspace"), 93 demo_projects: str = Option( 94 "", 95 "--demo-projects", 96 help=f"Comma-separated list of demo projects to generate. Possible values: [{'|'.join(DEMO_PROJECT_NAMES_FOR_CLI)}]", 97 ), 98 secret: Optional[str] = Option(None, help="Secret for writing operations"), 99 litestar_request_max_body_size: Optional[int] = Option(None, help="Request body size limit"), 100 conf_path: Optional[str] = Option(None, help="Path to configuration file"), 101 ): 102 """Start Evidently UI service""" 103 if os.environ.get("EXPERIMENTAL_DETERMINISTIC_UUID"): 104 setup_deterministic_generation_uuid() 105 106 from evidently.ui.service.app import run 107 108 demos: list[str] = demo_projects.split(",") if demo_projects else [] 109 if "all" in demos: 110 # TODO: better type safety 111 demos = cast(list[str], list(DEMO_PROJECTS_NAMES)) 112 missing = [dp for dp in demos if dp not in DEMO_PROJECTS] 113 if missing: 114 raise BadParameter(f"Unknown demo project name '{missing[0]}'") 115 116 if demos: 117 # Start background task in a daemon thread 118 thread = threading.Thread( 119 target=_create_demo_projects_task, 120 args=(demos, host, port, secret), 121 daemon=True, 122 ) 123 thread.start() 124 125 config = get_config( 126 host=host, 127 port=port, 128 workspace=workspace, 129 secret=secret, 130 request_max_body_size=litestar_request_max_body_size, 131 conf_path=conf_path, 132 ) 133 run(config)