This is page 3 of 6. Use http://codebase.md/datalayer/jupyter-mcp-server?lines=true&page={x} to view the full context. # Directory Structure ``` ├── .dockerignore ├── .github │ ├── copilot-instructions.md │ ├── dependabot.yml │ └── workflows │ ├── build.yml │ ├── fix-license-header.yml │ ├── lint.sh │ ├── release.yml │ └── test.yml ├── .gitignore ├── .licenserc.yaml ├── .pre-commit-config.yaml ├── .vscode │ ├── mcp.json │ └── settings.json ├── ARCHITECTURE.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── dev │ ├── content │ │ ├── new.ipynb │ │ ├── notebook.ipynb │ │ └── README.md │ └── README.md ├── Dockerfile ├── docs │ ├── .gitignore │ ├── .yarnrc.yml │ ├── babel.config.js │ ├── docs │ │ ├── _category_.yaml │ │ ├── clients │ │ │ ├── _category_.yaml │ │ │ ├── claude_desktop │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── cline │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── cursor │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ ├── vscode │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ └── windsurf │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── configure │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── contribute │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── deployment │ │ │ ├── _category_.yaml │ │ │ ├── datalayer │ │ │ │ ├── _category_.yaml │ │ │ │ └── streamable-http │ │ │ │ └── index.mdx │ │ │ ├── index.mdx │ │ │ └── jupyter │ │ │ ├── _category_.yaml │ │ │ ├── index.mdx │ │ │ ├── stdio │ │ │ │ ├── _category_.yaml │ │ │ │ └── index.mdx │ │ │ └── streamable-http │ │ │ ├── _category_.yaml │ │ │ ├── jupyter-extension │ │ │ │ └── index.mdx │ │ │ └── standalone │ │ │ └── index.mdx │ │ ├── index.mdx │ │ ├── releases │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ ├── resources │ │ │ ├── _category_.yaml │ │ │ └── index.mdx │ │ └── tools │ │ ├── _category_.yaml │ │ └── index.mdx │ ├── docusaurus.config.js │ ├── LICENSE │ ├── Makefile │ ├── package.json │ ├── README.md │ ├── sidebars.js │ ├── src │ │ ├── components │ │ │ ├── HomepageFeatures.js │ │ │ ├── HomepageFeatures.module.css │ │ │ ├── HomepageProducts.js │ │ │ └── HomepageProducts.module.css │ │ ├── css │ │ │ └── custom.css │ │ ├── pages │ │ │ ├── index.module.css │ │ │ ├── markdown-page.md │ │ │ └── testimonials.tsx │ │ └── theme │ │ └── CustomDocItem.tsx │ └── static │ └── img │ ├── datalayer │ │ ├── logo.png │ │ └── logo.svg │ ├── favicon.ico │ ├── feature_1.svg │ ├── feature_2.svg │ ├── feature_3.svg │ ├── product_1.svg │ ├── product_2.svg │ └── product_3.svg ├── examples │ └── integration_example.py ├── jupyter_mcp_server │ ├── __init__.py │ ├── __main__.py │ ├── __version__.py │ ├── config.py │ ├── enroll.py │ ├── env.py │ ├── jupyter_extension │ │ ├── __init__.py │ │ ├── backends │ │ │ ├── __init__.py │ │ │ ├── base.py │ │ │ ├── local_backend.py │ │ │ └── remote_backend.py │ │ ├── context.py │ │ ├── extension.py │ │ ├── handlers.py │ │ └── protocol │ │ ├── __init__.py │ │ └── messages.py │ ├── models.py │ ├── notebook_manager.py │ ├── server_modes.py │ ├── server.py │ ├── tools │ │ ├── __init__.py │ │ ├── _base.py │ │ ├── _registry.py │ │ ├── assign_kernel_to_notebook_tool.py │ │ ├── delete_cell_tool.py │ │ ├── execute_cell_tool.py │ │ ├── execute_ipython_tool.py │ │ ├── insert_cell_tool.py │ │ ├── insert_execute_code_cell_tool.py │ │ ├── list_cells_tool.py │ │ ├── list_files_tool.py │ │ ├── list_kernels_tool.py │ │ ├── list_notebooks_tool.py │ │ ├── overwrite_cell_source_tool.py │ │ ├── read_cell_tool.py │ │ ├── read_cells_tool.py │ │ ├── restart_notebook_tool.py │ │ ├── unuse_notebook_tool.py │ │ └── use_notebook_tool.py │ └── utils.py ├── jupyter-config │ ├── jupyter_notebook_config │ │ └── jupyter_mcp_server.json │ └── jupyter_server_config.d │ └── jupyter_mcp_server.json ├── LICENSE ├── Makefile ├── pyproject.toml ├── pytest.ini ├── README.md ├── RELEASE.md ├── smithery.yaml └── tests ├── __init__.py ├── conftest.py ├── test_common.py ├── test_config.py ├── test_jupyter_extension.py ├── test_list_kernels.py ├── test_tools.py └── test_use_notebook.py ``` # Files -------------------------------------------------------------------------------- /jupyter_mcp_server/notebook_manager.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """ 6 | Unified Notebook and Kernel Management Module 7 | 8 | This module provides centralized management for Jupyter notebooks and kernels, 9 | replacing the scattered global variable approach with a unified architecture. 10 | """ 11 | 12 | from typing import Dict, Any, Optional, Callable, Union 13 | from types import TracebackType 14 | 15 | from jupyter_nbmodel_client import NbModelClient, get_notebook_websocket_url 16 | from jupyter_kernel_client import KernelClient 17 | 18 | from .config import get_config 19 | 20 | 21 | class NotebookConnection: 22 | """ 23 | Context manager for Notebook connections that handles the lifecycle 24 | of NbModelClient instances. 25 | 26 | Note: This is only used in MCP_SERVER mode with remote Jupyter servers that have RTC enabled. 27 | In JUPYTER_SERVER mode (local), notebook content is accessed directly via contents_manager. 28 | """ 29 | 30 | def __init__(self, notebook_info: Dict[str, str], is_local: bool = False): 31 | self.notebook_info = notebook_info 32 | self.is_local = is_local 33 | self._notebook: Optional[NbModelClient] = None 34 | 35 | async def __aenter__(self) -> NbModelClient: 36 | """Enter context, establish notebook connection.""" 37 | if self.is_local: 38 | raise ValueError( 39 | "NotebookConnection cannot be used in local/JUPYTER_SERVER mode. " 40 | "Cell operations in local mode should use contents_manager directly to read notebook JSON files." 41 | ) 42 | 43 | config = get_config() 44 | ws_url = get_notebook_websocket_url( 45 | server_url=self.notebook_info.get("server_url", config.document_url), 46 | token=self.notebook_info.get("token", config.document_token), 47 | path=self.notebook_info.get("path", config.document_id), 48 | provider=config.provider 49 | ) 50 | self._notebook = NbModelClient(ws_url) 51 | await self._notebook.__aenter__() 52 | return self._notebook 53 | 54 | async def __aexit__( 55 | self, 56 | exc_type: Optional[type], 57 | exc_val: Optional[BaseException], 58 | exc_tb: Optional[TracebackType] 59 | ) -> None: 60 | """Exit context, clean up connection.""" 61 | if self._notebook: 62 | await self._notebook.__aexit__(exc_type, exc_val, exc_tb) 63 | 64 | 65 | class NotebookManager: 66 | """ 67 | Centralized manager for multiple notebooks and their corresponding kernels. 68 | 69 | This class replaces the global kernel variable approach with a unified 70 | management system that supports both single and multiple notebook scenarios. 71 | """ 72 | 73 | def __init__(self): 74 | self._notebooks: Dict[str, Dict[str, Any]] = {} 75 | self._default_notebook_name = "default" 76 | self._current_notebook: Optional[str] = None # Currently active notebook 77 | 78 | def __contains__(self, name: str) -> bool: 79 | """Check if a notebook is managed by this instance.""" 80 | return name in self._notebooks 81 | 82 | def __iter__(self): 83 | """Iterate over notebook name, info pairs.""" 84 | return iter(self._notebooks.items()) 85 | 86 | def add_notebook( 87 | self, 88 | name: str, 89 | kernel: Union[KernelClient, Dict[str, Any]], # Can be KernelClient or dict with kernel metadata 90 | server_url: Optional[str] = None, 91 | token: Optional[str] = None, 92 | path: Optional[str] = None 93 | ) -> None: 94 | """ 95 | Add a notebook to the manager. 96 | 97 | Args: 98 | name: Unique identifier for the notebook 99 | kernel: Kernel client instance (MCP_SERVER mode) or kernel metadata dict (JUPYTER_SERVER mode) 100 | server_url: Jupyter server URL (optional, uses config default). Use "local" for JUPYTER_SERVER mode. 101 | token: Authentication token (optional, uses config default) 102 | path: Notebook file path (optional, uses config default) 103 | """ 104 | config = get_config() 105 | 106 | # Determine if this is local (JUPYTER_SERVER) mode or HTTP (MCP_SERVER) mode 107 | is_local_mode = server_url == "local" 108 | 109 | self._notebooks[name] = { 110 | "kernel": kernel, 111 | "is_local": is_local_mode, 112 | "notebook_info": { 113 | "server_url": server_url or config.document_url, 114 | "token": token or config.document_token, 115 | "path": path or config.document_id 116 | } 117 | } 118 | 119 | # For backward compatibility: if this is the first notebook or it's "default", 120 | # set it as the current notebook 121 | if self._current_notebook is None or name == self._default_notebook_name: 122 | self._current_notebook = name 123 | 124 | def remove_notebook(self, name: str) -> bool: 125 | """ 126 | Remove a notebook from the manager. 127 | 128 | Args: 129 | name: Notebook identifier 130 | 131 | Returns: 132 | True if removed successfully, False if not found 133 | """ 134 | if name in self._notebooks: 135 | try: 136 | notebook_data = self._notebooks[name] 137 | is_local = notebook_data.get("is_local", False) 138 | kernel = notebook_data["kernel"] 139 | 140 | # Only stop kernel if it's an HTTP KernelClient (MCP_SERVER mode) 141 | # In JUPYTER_SERVER mode, kernel is just metadata, actual kernel managed elsewhere 142 | if not is_local and kernel and hasattr(kernel, 'stop'): 143 | kernel.stop() 144 | except Exception: 145 | # Ignore errors during kernel cleanup 146 | pass 147 | finally: 148 | del self._notebooks[name] 149 | 150 | # If we removed the current notebook, update the current pointer 151 | if self._current_notebook == name: 152 | # Set to another notebook if available, prefer "default" for compatibility 153 | if self._default_notebook_name in self._notebooks: 154 | self._current_notebook = self._default_notebook_name 155 | elif self._notebooks: 156 | # Set to the first available notebook 157 | self._current_notebook = next(iter(self._notebooks.keys())) 158 | else: 159 | # No notebooks left 160 | self._current_notebook = None 161 | return True 162 | return False 163 | 164 | def get_kernel(self, name: str) -> Optional[Union[KernelClient, Dict[str, Any]]]: 165 | """ 166 | Get the kernel for a specific notebook. 167 | 168 | Args: 169 | name: Notebook identifier 170 | 171 | Returns: 172 | Kernel client (MCP_SERVER mode) or kernel metadata dict (JUPYTER_SERVER mode), or None if not found 173 | """ 174 | if name in self._notebooks: 175 | return self._notebooks[name]["kernel"] 176 | return None 177 | 178 | def get_kernel_id(self, name: str) -> Optional[str]: 179 | """ 180 | Get the kernel ID for a specific notebook. 181 | 182 | Args: 183 | name: Notebook identifier 184 | 185 | Returns: 186 | Kernel ID string or None if not found 187 | """ 188 | if name in self._notebooks: 189 | kernel = self._notebooks[name]["kernel"] 190 | # Handle both KernelClient objects and kernel metadata dicts 191 | if isinstance(kernel, dict): 192 | return kernel.get("id") 193 | elif hasattr(kernel, 'kernel_id'): 194 | return kernel.kernel_id 195 | return None 196 | 197 | def is_local_notebook(self, name: str) -> bool: 198 | """ 199 | Check if a notebook is using local (JUPYTER_SERVER) mode. 200 | 201 | Args: 202 | name: Notebook identifier 203 | 204 | Returns: 205 | True if local mode, False otherwise 206 | """ 207 | if name in self._notebooks: 208 | return self._notebooks[name].get("is_local", False) 209 | return False 210 | 211 | def get_notebook_connection(self, name: str) -> NotebookConnection: 212 | """ 213 | Get a context manager for notebook connection. 214 | 215 | Args: 216 | name: Notebook identifier 217 | 218 | Returns: 219 | NotebookConnection context manager 220 | 221 | Raises: 222 | ValueError: If notebook doesn't exist 223 | """ 224 | if name not in self._notebooks: 225 | raise ValueError(f"Notebook '{name}' does not exist in manager") 226 | 227 | return NotebookConnection(self._notebooks[name]["notebook_info"]) 228 | 229 | def restart_notebook(self, name: str) -> bool: 230 | """ 231 | Restart the kernel for a specific notebook. 232 | 233 | Args: 234 | name: Notebook identifier 235 | 236 | Returns: 237 | True if restarted successfully, False otherwise 238 | """ 239 | if name in self._notebooks: 240 | try: 241 | kernel = self._notebooks[name]["kernel"] 242 | if kernel and hasattr(kernel, 'restart'): 243 | kernel.restart() 244 | return True 245 | except Exception: 246 | return False 247 | return False 248 | 249 | def is_empty(self) -> bool: 250 | """Check if the manager is empty (no notebooks).""" 251 | return len(self._notebooks) == 0 252 | 253 | def ensure_kernel_alive(self, name: str, kernel_factory: Callable[[], KernelClient]) -> KernelClient: 254 | """ 255 | Ensure a kernel is alive, create if necessary. 256 | 257 | Args: 258 | name: Notebook identifier 259 | kernel_factory: Function to create a new kernel 260 | 261 | Returns: 262 | The alive kernel instance 263 | """ 264 | kernel = self.get_kernel(name) 265 | if kernel is None or not hasattr(kernel, 'is_alive') or not kernel.is_alive(): 266 | # Create new kernel 267 | new_kernel = kernel_factory() 268 | self.add_notebook(name, new_kernel) 269 | return new_kernel 270 | return kernel 271 | 272 | def set_current_notebook(self, name: str) -> bool: 273 | """ 274 | Set the currently active notebook. 275 | 276 | Args: 277 | name: Notebook identifier 278 | 279 | Returns: 280 | True if set successfully, False if notebook doesn't exist 281 | """ 282 | if name in self._notebooks: 283 | self._current_notebook = name 284 | return True 285 | return False 286 | 287 | def get_current_notebook(self) -> Optional[str]: 288 | """ 289 | Get the name of the currently active notebook. 290 | 291 | Returns: 292 | Current notebook name or None if no active notebook 293 | """ 294 | return self._current_notebook 295 | 296 | def get_current_connection(self) -> NotebookConnection: 297 | """ 298 | Get the connection for the currently active notebook. 299 | For backward compatibility, defaults to "default" if no current notebook is set. 300 | 301 | Returns: 302 | NotebookConnection context manager for the current notebook 303 | 304 | Raises: 305 | ValueError: If no notebooks exist and no default config is available 306 | """ 307 | current = self._current_notebook or self._default_notebook_name 308 | 309 | # For backward compatibility: if the requested notebook doesn't exist but we're 310 | # asking for default, create a connection using the default config 311 | if current not in self._notebooks and current == self._default_notebook_name: 312 | # Return a connection using default configuration 313 | config = get_config() 314 | return NotebookConnection({ 315 | "server_url": config.document_url, 316 | "token": config.document_token, 317 | "path": config.document_id 318 | }) 319 | 320 | return self.get_notebook_connection(current) 321 | 322 | def get_current_notebook_path(self) -> Optional[str]: 323 | """ 324 | Get the file path of the currently active notebook. 325 | 326 | Returns: 327 | Notebook file path or None if no active notebook 328 | """ 329 | current = self._current_notebook or self._default_notebook_name 330 | if current in self._notebooks: 331 | return self._notebooks[current]["notebook_info"].get("path") 332 | return None 333 | 334 | def list_all_notebooks(self) -> Dict[str, Dict[str, Any]]: 335 | """ 336 | Get information about all managed notebooks. 337 | 338 | Returns: 339 | Dictionary with notebook names as keys and their info as values 340 | """ 341 | result = {} 342 | for name, notebook_data in self._notebooks.items(): 343 | kernel = notebook_data["kernel"] 344 | notebook_info = notebook_data["notebook_info"] 345 | 346 | # Check kernel status 347 | kernel_status = "unknown" 348 | if kernel: 349 | try: 350 | kernel_status = "alive" if hasattr(kernel, 'is_alive') and kernel.is_alive() else "dead" 351 | except Exception: 352 | kernel_status = "error" 353 | else: 354 | kernel_status = "not_initialized" 355 | 356 | result[name] = { 357 | "path": notebook_info.get("path", ""), 358 | "kernel_status": kernel_status, 359 | "is_current": name == self._current_notebook 360 | } 361 | 362 | return result 363 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/use_notebook_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Use notebook tool implementation.""" 6 | 7 | import logging 8 | from typing import Any, Optional, Literal 9 | from pathlib import Path 10 | from jupyter_server_api import JupyterServerClient, NotFoundError 11 | from jupyter_kernel_client import KernelClient 12 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 13 | from jupyter_mcp_server.notebook_manager import NotebookManager 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class UseNotebookTool(BaseTool): 19 | """Tool to use (connect to or create) a notebook file.""" 20 | 21 | @property 22 | def name(self) -> str: 23 | return "use_notebook" 24 | 25 | @property 26 | def description(self) -> str: 27 | return """Use a notebook file (connect to existing, create new, or switch to already-connected notebook). 28 | 29 | Args: 30 | notebook_name: Unique identifier for the notebook 31 | notebook_path: Path to the notebook file, relative to the Jupyter server root (e.g. "notebook.ipynb"). 32 | Optional - if not provided, switches to an already-connected notebook with the given name. 33 | mode: "connect" to connect to existing, "create" to create new 34 | kernel_id: Specific kernel ID to use (optional, will create new if not provided) 35 | 36 | Returns: 37 | str: Success message with notebook information""" 38 | 39 | async def _check_path_http( 40 | self, 41 | server_client: JupyterServerClient, 42 | notebook_path: str, 43 | mode: str 44 | ) -> tuple[bool, Optional[str]]: 45 | """Check if path exists using HTTP API.""" 46 | path = Path(notebook_path) 47 | try: 48 | parent_path = path.parent.as_posix() if path.parent.as_posix() != "." else "" 49 | 50 | if parent_path: 51 | dir_contents = server_client.contents.list_directory(parent_path) 52 | else: 53 | dir_contents = server_client.contents.list_directory("") 54 | 55 | if mode == "connect": 56 | file_exists = any(file.name == path.name for file in dir_contents) 57 | if not file_exists: 58 | return False, f"'{notebook_path}' not found in jupyter server, please check the notebook already exists." 59 | 60 | return True, None 61 | except NotFoundError: 62 | parent_dir = path.parent.as_posix() if path.parent.as_posix() != "." else "root directory" 63 | return False, f"'{parent_dir}' not found in jupyter server, please check the directory path already exists." 64 | except Exception as e: 65 | return False, f"Failed to check the path '{notebook_path}': {e}" 66 | 67 | async def _check_path_local( 68 | self, 69 | contents_manager: Any, 70 | notebook_path: str, 71 | mode: str 72 | ) -> tuple[bool, Optional[str]]: 73 | """Check if path exists using local contents_manager API.""" 74 | path = Path(notebook_path) 75 | try: 76 | parent_path = str(path.parent) if str(path.parent) != "." else "" 77 | 78 | # Get directory contents using local API 79 | model = await contents_manager.get(parent_path, content=True, type='directory') 80 | 81 | if mode == "connect": 82 | file_exists = any(item['name'] == path.name for item in model.get('content', [])) 83 | if not file_exists: 84 | return False, f"'{notebook_path}' not found in jupyter server, please check the notebook already exists." 85 | 86 | return True, None 87 | except Exception as e: 88 | parent_dir = str(path.parent) if str(path.parent) != "." else "root directory" 89 | return False, f"'{parent_dir}' not found in jupyter server: {e}" 90 | 91 | async def execute( 92 | self, 93 | mode: ServerMode, 94 | server_client: Optional[JupyterServerClient] = None, 95 | kernel_client: Optional[Any] = None, 96 | contents_manager: Optional[Any] = None, 97 | kernel_manager: Optional[Any] = None, 98 | kernel_spec_manager: Optional[Any] = None, 99 | session_manager: Optional[Any] = None, 100 | notebook_manager: Optional[NotebookManager] = None, 101 | # Tool-specific parameters 102 | notebook_name: str = None, 103 | notebook_path: Optional[str] = None, 104 | use_mode: Literal["connect", "create"] = "connect", 105 | kernel_id: Optional[str] = None, 106 | runtime_url: Optional[str] = None, 107 | runtime_token: Optional[str] = None, 108 | **kwargs 109 | ) -> str: 110 | """Execute the use_notebook tool. 111 | 112 | Args: 113 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 114 | server_client: HTTP client for MCP_SERVER mode 115 | contents_manager: Direct API access for JUPYTER_SERVER mode 116 | kernel_manager: Direct kernel manager for JUPYTER_SERVER mode 117 | session_manager: Session manager for creating kernel-notebook associations 118 | notebook_manager: Notebook manager instance 119 | notebook_name: Unique identifier for the notebook 120 | notebook_path: Path to the notebook file (optional, if not provided switches to existing notebook) 121 | use_mode: "connect" or "create" 122 | kernel_id: Optional specific kernel ID 123 | runtime_url: Runtime URL for HTTP mode 124 | runtime_token: Runtime token for HTTP mode 125 | **kwargs: Additional parameters 126 | 127 | Returns: 128 | Success message with notebook information 129 | """ 130 | # If no notebook_path provided, switch to already-connected notebook 131 | if notebook_path is None: 132 | if notebook_name not in notebook_manager: 133 | return f"Notebook '{notebook_name}' is not connected. Please provide a notebook_path to connect to it first." 134 | 135 | # Switch to the existing notebook 136 | notebook_manager.set_current_notebook(notebook_name) 137 | return f"Successfully switched to notebook '{notebook_name}'." 138 | 139 | # Rest of the logic for connecting/creating new notebooks 140 | if notebook_name in notebook_manager: 141 | return f"Notebook '{notebook_name}' is already using. Use unuse_notebook first if you want to reconnect." 142 | 143 | # Check server connectivity (HTTP mode only) 144 | if mode == ServerMode.MCP_SERVER and server_client is not None: 145 | try: 146 | server_client.get_status() 147 | except Exception as e: 148 | return f"Failed to connect the Jupyter server: {e}" 149 | 150 | # Check the path exists 151 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None: 152 | path_ok, error_msg = await self._check_path_local(contents_manager, notebook_path, use_mode) 153 | elif mode == ServerMode.MCP_SERVER and server_client is not None: 154 | path_ok, error_msg = await self._check_path_http(server_client, notebook_path, use_mode) 155 | else: 156 | return f"Invalid mode or missing required clients: mode={mode}" 157 | 158 | if not path_ok: 159 | return error_msg 160 | 161 | # Check kernel if kernel_id provided (HTTP mode only for now) 162 | if kernel_id and mode == ServerMode.MCP_SERVER and server_client is not None: 163 | kernels = server_client.kernels.list_kernels() 164 | kernel_exists = any(kernel.id == kernel_id for kernel in kernels) 165 | if not kernel_exists: 166 | return f"Kernel '{kernel_id}' not found in jupyter server, please check the kernel already exists." 167 | 168 | # Create notebook if needed 169 | if use_mode == "create": 170 | content = { 171 | "cells": [{ 172 | "cell_type": "markdown", 173 | "metadata": {}, 174 | "source": [ 175 | "New Notebook Created by Jupyter MCP Server", 176 | ] 177 | }], 178 | "metadata": {}, 179 | "nbformat": 4, 180 | "nbformat_minor": 4 181 | } 182 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None: 183 | # Use local API to create notebook 184 | await contents_manager.new(model={'type': 'notebook'}, path=notebook_path) 185 | elif mode == ServerMode.MCP_SERVER and server_client is not None: 186 | server_client.contents.create_notebook(notebook_path, content=content) 187 | 188 | # Create/connect to kernel based on mode 189 | if mode == ServerMode.JUPYTER_SERVER and kernel_manager is not None: 190 | # JUPYTER_SERVER mode: Use local kernel manager API directly 191 | if kernel_id: 192 | # Connect to existing kernel - verify it exists 193 | if kernel_id not in kernel_manager: 194 | return f"Kernel '{kernel_id}' not found in local kernel manager." 195 | kernel_info = {"id": kernel_id} 196 | else: 197 | # Start a new kernel using local API 198 | kernel_id = await kernel_manager.start_kernel() 199 | logger.info(f"Started kernel '{kernel_id}', waiting for it to be ready...") 200 | 201 | # CRITICAL: Wait for the kernel to actually start and be ready 202 | # The start_kernel() call returns immediately, but kernel takes time to start 203 | import asyncio 204 | max_wait_time = 30 # seconds 205 | wait_interval = 0.5 # seconds 206 | elapsed = 0 207 | kernel_ready = False 208 | 209 | while elapsed < max_wait_time: 210 | try: 211 | # Get kernel model to check its state 212 | kernel_model = kernel_manager.get_kernel(kernel_id) 213 | if kernel_model is not None: 214 | # Kernel exists, check if it's ready 215 | # In Jupyter, we can try to get connection info which indicates readiness 216 | try: 217 | kernel_manager.get_connection_info(kernel_id) 218 | kernel_ready = True 219 | logger.info(f"Kernel '{kernel_id}' is ready (took {elapsed:.1f}s)") 220 | break 221 | except: 222 | # Connection info not available yet, kernel still starting 223 | pass 224 | except Exception as e: 225 | logger.debug(f"Waiting for kernel to start: {e}") 226 | 227 | await asyncio.sleep(wait_interval) 228 | elapsed += wait_interval 229 | 230 | if not kernel_ready: 231 | logger.warning(f"Kernel '{kernel_id}' may not be fully ready after {max_wait_time}s wait") 232 | 233 | kernel_info = {"id": kernel_id} 234 | 235 | # Create a Jupyter session to associate the kernel with the notebook 236 | # This is CRITICAL for JupyterLab to recognize the kernel-notebook connection 237 | if session_manager is not None: 238 | try: 239 | # create_session is an async method, so we await it directly 240 | session_dict = await session_manager.create_session( 241 | path=notebook_path, 242 | kernel_id=kernel_id, 243 | type="notebook", 244 | name=notebook_path 245 | ) 246 | logger.info(f"Created Jupyter session '{session_dict.get('id')}' for notebook '{notebook_path}' with kernel '{kernel_id}'") 247 | except Exception as e: 248 | logger.warning(f"Failed to create Jupyter session: {e}. Notebook may not be properly connected in JupyterLab UI.") 249 | else: 250 | logger.warning("No session_manager available. Notebook may not be properly connected in JupyterLab UI.") 251 | 252 | # For JUPYTER_SERVER mode, store kernel info (not KernelClient object) 253 | # The actual kernel is managed by kernel_manager 254 | notebook_manager.add_notebook( 255 | notebook_name, 256 | kernel_info, # Store kernel metadata, not client object 257 | server_url="local", # Indicate local mode 258 | token=None, 259 | path=notebook_path 260 | ) 261 | elif mode == ServerMode.MCP_SERVER and runtime_url: 262 | # MCP_SERVER mode: Use HTTP-based kernel client 263 | kernel = KernelClient( 264 | server_url=runtime_url, 265 | token=runtime_token, 266 | kernel_id=kernel_id 267 | ) 268 | kernel.start() 269 | 270 | # Add notebook to manager with HTTP client 271 | notebook_manager.add_notebook( 272 | notebook_name, 273 | kernel, 274 | server_url=runtime_url, 275 | token=runtime_token, 276 | path=notebook_path 277 | ) 278 | else: 279 | return f"Invalid configuration: mode={mode}, runtime_url={runtime_url}, kernel_manager={kernel_manager is not None}" 280 | 281 | notebook_manager.set_current_notebook(notebook_name) 282 | 283 | # Return message based on mode 284 | if use_mode == "create": 285 | return f"Successfully created and using notebook '{notebook_name}' at path '{notebook_path}' in {mode.value} mode." 286 | else: 287 | return f"Successfully using notebook '{notebook_name}' at path '{notebook_path}' in {mode.value} mode." 288 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/insert_execute_code_cell_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Insert and execute code cell tool implementation.""" 6 | 7 | import asyncio 8 | import logging 9 | from pathlib import Path 10 | from typing import Any, Optional, List, Union 11 | from jupyter_server_api import JupyterServerClient 12 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 13 | from jupyter_mcp_server.notebook_manager import NotebookManager 14 | from jupyter_mcp_server.utils import get_current_notebook_context, safe_extract_outputs, execute_via_execution_stack 15 | from mcp.types import ImageContent 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | 20 | class InsertExecuteCodeCellTool(BaseTool): 21 | """Tool to insert and execute a code cell.""" 22 | 23 | @property 24 | def name(self) -> str: 25 | return "insert_execute_code_cell" 26 | 27 | @property 28 | def description(self) -> str: 29 | return """Insert and execute a code cell in a Jupyter notebook. 30 | 31 | Args: 32 | cell_index: Index of the cell to insert (0-based). Use -1 to append at end and execute. 33 | cell_source: Code source 34 | 35 | Returns: 36 | list[Union[str, ImageContent]]: List of outputs from the executed cell""" 37 | 38 | async def _get_jupyter_ydoc(self, serverapp: Any, file_id: str): 39 | """Get the YNotebook document if it's currently open in a collaborative session.""" 40 | try: 41 | yroom_manager = serverapp.web_app.settings.get("yroom_manager") 42 | if yroom_manager is None: 43 | return None 44 | 45 | room_id = f"json:notebook:{file_id}" 46 | 47 | if yroom_manager.has_room(room_id): 48 | yroom = yroom_manager.get_room(room_id) 49 | notebook = await yroom.get_jupyter_ydoc() 50 | return notebook 51 | except Exception: 52 | pass 53 | 54 | return None 55 | 56 | async def _insert_execute_ydoc( 57 | self, 58 | serverapp: Any, 59 | notebook_path: str, 60 | cell_index: int, 61 | cell_source: str, 62 | kernel_manager, 63 | kernel_id: str, 64 | safe_extract_outputs_fn 65 | ) -> List[Union[str, ImageContent]]: 66 | """Insert and execute cell using YDoc (collaborative editing mode).""" 67 | # Get file_id from file_id_manager 68 | file_id_manager = serverapp.web_app.settings.get("file_id_manager") 69 | if file_id_manager is None: 70 | raise RuntimeError("file_id_manager not available in serverapp") 71 | 72 | file_id = file_id_manager.get_id(notebook_path) 73 | 74 | # Try to get YDoc 75 | ydoc = await self._get_jupyter_ydoc(serverapp, file_id) 76 | 77 | if ydoc: 78 | # Notebook is open in collaborative mode, use YDoc 79 | total_cells = len(ydoc.ycells) 80 | actual_index = cell_index if cell_index != -1 else total_cells 81 | 82 | if actual_index < 0 or actual_index > total_cells: 83 | raise ValueError( 84 | f"Cell index {cell_index} is out of range. Notebook has {total_cells} cells. Use -1 to append at end." 85 | ) 86 | 87 | # Create and insert the cell 88 | cell = { 89 | "cell_type": "code", 90 | "source": cell_source, 91 | } 92 | ycell = ydoc.create_ycell(cell) 93 | 94 | if actual_index >= total_cells: 95 | ydoc.ycells.append(ycell) 96 | else: 97 | ydoc.ycells.insert(actual_index, ycell) 98 | 99 | # Get the inserted cell's ID for RTC metadata 100 | inserted_cell_id = ycell.get("id") 101 | 102 | # Build document_id for RTC (format: json:notebook:<file_id>) 103 | document_id = f"json:notebook:{file_id}" 104 | 105 | # Execute the cell using ExecutionStack with RTC metadata 106 | # This will automatically update the cell outputs in the YDoc 107 | return await execute_via_execution_stack( 108 | serverapp, kernel_id, cell_source, 109 | document_id=document_id, 110 | cell_id=inserted_cell_id, 111 | timeout=300, 112 | logger=logger 113 | ) 114 | else: 115 | # YDoc not available - use file operations + direct kernel execution 116 | # This path is used when notebook is not open in JupyterLab but we still have kernel access 117 | logger.info("YDoc not available, using file operations + ExecutionStack execution fallback") 118 | 119 | # Insert cell using file operations 120 | from jupyter_mcp_server.tools.insert_cell_tool import InsertCellTool 121 | insert_tool = InsertCellTool() 122 | 123 | # Call the file-based insertion method directly 124 | await insert_tool._insert_cell_file(notebook_path, cell_index, "code", cell_source) 125 | 126 | # Calculate actual index where cell was inserted 127 | import nbformat 128 | with open(notebook_path, 'r', encoding='utf-8') as f: 129 | notebook = nbformat.read(f, as_version=4) 130 | total_cells = len(notebook.cells) 131 | actual_index = cell_index if cell_index != -1 else total_cells - 1 132 | 133 | # Then execute directly via ExecutionStack (without RTC metadata since notebook not open) 134 | outputs = await execute_via_execution_stack( 135 | serverapp, kernel_id, cell_source, timeout=300, logger=logger 136 | ) 137 | 138 | # CRITICAL: Write outputs back to the notebook file so they're visible in UI 139 | logger.info(f"Writing {len(outputs)} outputs back to notebook cell {actual_index}") 140 | await self._write_outputs_to_cell(notebook_path, actual_index, outputs) 141 | 142 | return outputs 143 | 144 | async def _insert_execute_websocket( 145 | self, 146 | notebook_manager: NotebookManager, 147 | cell_index: int, 148 | cell_source: str, 149 | ensure_kernel_alive 150 | ) -> List[Union[str, ImageContent]]: 151 | """Insert and execute cell using WebSocket connection (MCP_SERVER mode).""" 152 | # Ensure kernel is alive 153 | if ensure_kernel_alive: 154 | kernel = ensure_kernel_alive() 155 | else: 156 | # Fallback: get kernel from notebook_manager 157 | current_notebook = notebook_manager.get_current_notebook() or "default" 158 | kernel = notebook_manager.get_kernel(current_notebook) 159 | if not kernel: 160 | raise RuntimeError("No kernel available for execution") 161 | 162 | async with notebook_manager.get_current_connection() as notebook: 163 | actual_index = cell_index if cell_index != -1 else len(notebook) 164 | 165 | if actual_index < 0 or actual_index > len(notebook): 166 | raise ValueError(f"Cell index {cell_index} out of range") 167 | 168 | notebook.insert_cell(actual_index, cell_source, "code") 169 | notebook.execute_cell(actual_index, kernel) 170 | 171 | outputs = notebook[actual_index].get("outputs", []) 172 | return safe_extract_outputs(outputs) 173 | 174 | async def _write_outputs_to_cell( 175 | self, 176 | notebook_path: str, 177 | cell_index: int, 178 | outputs: List[Union[str, ImageContent]] 179 | ): 180 | """Write execution outputs back to a notebook cell. 181 | 182 | This is critical for making outputs visible in JupyterLab when using 183 | file-based execution (when YDoc/RTC is not available). 184 | 185 | Args: 186 | notebook_path: Path to the notebook file 187 | cell_index: Index of the cell to update 188 | outputs: List of output strings or ImageContent objects 189 | """ 190 | import nbformat 191 | from jupyter_mcp_server.utils import _clean_notebook_outputs 192 | 193 | # Read the notebook 194 | with open(notebook_path, 'r', encoding='utf-8') as f: 195 | notebook = nbformat.read(f, as_version=4) 196 | 197 | # Clean any transient fields 198 | _clean_notebook_outputs(notebook) 199 | 200 | if cell_index < 0 or cell_index >= len(notebook.cells): 201 | logger.warning(f"Cell index {cell_index} out of range, cannot write outputs") 202 | return 203 | 204 | cell = notebook.cells[cell_index] 205 | if cell.cell_type != 'code': 206 | logger.warning(f"Cell {cell_index} is not a code cell, cannot write outputs") 207 | return 208 | 209 | # Convert formatted outputs to nbformat structure 210 | cell.outputs = [] 211 | for output in outputs: 212 | if isinstance(output, ImageContent): 213 | # Image output 214 | cell.outputs.append(nbformat.v4.new_output( 215 | output_type='display_data', 216 | data={output.mimeType: output.data}, 217 | metadata={} 218 | )) 219 | elif isinstance(output, str): 220 | # Text output - determine if it's an error or regular output 221 | if output.startswith('[ERROR:') or output.startswith('[TIMEOUT ERROR:'): 222 | # Error output 223 | cell.outputs.append(nbformat.v4.new_output( 224 | output_type='stream', 225 | name='stderr', 226 | text=output 227 | )) 228 | else: 229 | # Regular output (assume execute_result for simplicity) 230 | cell.outputs.append(nbformat.v4.new_output( 231 | output_type='execute_result', 232 | data={'text/plain': output}, 233 | metadata={}, 234 | execution_count=None 235 | )) 236 | 237 | # Update execution count 238 | max_count = 0 239 | for c in notebook.cells: 240 | if c.cell_type == 'code' and c.execution_count: 241 | max_count = max(max_count, c.execution_count) 242 | cell.execution_count = max_count + 1 243 | 244 | # Write back to file 245 | with open(notebook_path, 'w', encoding='utf-8') as f: 246 | nbformat.write(notebook, f) 247 | 248 | logger.info(f"Wrote {len(outputs)} outputs to cell {cell_index} in {notebook_path}") 249 | 250 | async def execute( 251 | self, 252 | mode: ServerMode, 253 | server_client: Optional[JupyterServerClient] = None, 254 | kernel_client: Optional[Any] = None, 255 | contents_manager: Optional[Any] = None, 256 | kernel_manager: Optional[Any] = None, 257 | kernel_spec_manager: Optional[Any] = None, 258 | notebook_manager: Optional[NotebookManager] = None, 259 | # Tool-specific parameters 260 | cell_index: int = None, 261 | cell_source: str = None, 262 | # Helper function passed from server.py 263 | ensure_kernel_alive = None, 264 | **kwargs 265 | ) -> List[Union[str, ImageContent]]: 266 | """Execute the insert_execute_code_cell tool. 267 | 268 | Args: 269 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 270 | kernel_manager: Kernel manager for JUPYTER_SERVER mode 271 | notebook_manager: Notebook manager instance 272 | cell_index: Index to insert cell (0-based, -1 to append) 273 | cell_source: Code source 274 | ensure_kernel_alive: Function to ensure kernel is alive 275 | **kwargs: Additional parameters 276 | 277 | Returns: 278 | List of outputs from the executed cell 279 | """ 280 | if mode == ServerMode.JUPYTER_SERVER and kernel_manager is not None: 281 | # JUPYTER_SERVER mode: Use YDoc and kernel_manager 282 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 283 | from jupyter_mcp_server.config import get_config 284 | 285 | context = get_server_context() 286 | serverapp = context.serverapp 287 | 288 | notebook_path, kernel_id = get_current_notebook_context(notebook_manager) 289 | 290 | # Resolve to absolute path FIRST 291 | if serverapp and not Path(notebook_path).is_absolute(): 292 | root_dir = serverapp.root_dir 293 | notebook_path = str(Path(root_dir) / notebook_path) 294 | 295 | if kernel_id is None: 296 | # No kernel available - start a new one on demand 297 | logger.info("No kernel_id available, starting new kernel for insert_execute_code_cell") 298 | kernel_id = await kernel_manager.start_kernel() 299 | 300 | # Wait a bit for kernel to initialize 301 | await asyncio.sleep(1.0) 302 | logger.info(f"Kernel {kernel_id} started and initialized") 303 | 304 | # Store the kernel with ABSOLUTE path in notebook_manager 305 | if notebook_manager is not None: 306 | kernel_info = {"id": kernel_id} 307 | notebook_manager.add_notebook( 308 | name=notebook_path, 309 | kernel=kernel_info, 310 | server_url="local", 311 | path=notebook_path 312 | ) 313 | 314 | if serverapp: 315 | return await self._insert_execute_ydoc( 316 | serverapp, notebook_path, cell_index, cell_source, 317 | kernel_manager, kernel_id, safe_extract_outputs 318 | ) 319 | else: 320 | raise RuntimeError("serverapp not available in JUPYTER_SERVER mode") 321 | 322 | elif mode == ServerMode.MCP_SERVER and notebook_manager is not None: 323 | # MCP_SERVER mode: Use WebSocket connection 324 | return await self._insert_execute_websocket( 325 | notebook_manager, cell_index, cell_source, ensure_kernel_alive 326 | ) 327 | else: 328 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}") 329 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/jupyter_extension/backends/local_backend.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """ 6 | Local Backend Implementation 7 | 8 | This backend uses the Jupyter Server's local API directly when running as an extension. 9 | It provides efficient local access to contents_manager and kernel_manager. 10 | """ 11 | 12 | from typing import Optional, Any, Union, Literal, TYPE_CHECKING 13 | import asyncio 14 | from mcp.types import ImageContent 15 | from jupyter_mcp_server.jupyter_extension.backends.base import Backend 16 | from jupyter_mcp_server.utils import safe_extract_outputs 17 | 18 | if TYPE_CHECKING: 19 | from jupyter_server.serverapp import ServerApp 20 | 21 | 22 | class LocalBackend(Backend): 23 | """ 24 | Backend that uses local Jupyter Server API directly. 25 | 26 | Uses: 27 | - serverapp.contents_manager for notebook file operations 28 | - serverapp.kernel_manager for kernel management 29 | - serverapp.kernel_spec_manager for kernel specs 30 | 31 | This backend is only available when running as a Jupyter Server extension 32 | with document_url="local" or runtime_url="local". 33 | """ 34 | 35 | def __init__(self, serverapp: 'ServerApp'): 36 | """ 37 | Initialize local backend with direct serverapp access. 38 | 39 | Args: 40 | serverapp: Jupyter ServerApp instance 41 | """ 42 | self.serverapp = serverapp 43 | self.contents_manager = serverapp.contents_manager 44 | self.kernel_manager = serverapp.kernel_manager 45 | self.kernel_spec_manager = serverapp.kernel_spec_manager 46 | 47 | # Notebook operations 48 | 49 | async def get_notebook_content(self, path: str) -> dict[str, Any]: 50 | """ 51 | Get notebook content using local contents_manager. 52 | 53 | Args: 54 | path: Path to notebook file 55 | 56 | Returns: 57 | Notebook content dictionary 58 | """ 59 | model = await asyncio.to_thread( 60 | self.contents_manager.get, 61 | path, 62 | type='notebook', 63 | content=True 64 | ) 65 | return model['content'] 66 | 67 | async def list_notebooks(self, path: str = "") -> list[str]: 68 | """ 69 | List all notebooks recursively using local contents_manager. 70 | 71 | Args: 72 | path: Directory path to search 73 | 74 | Returns: 75 | List of notebook paths 76 | """ 77 | notebooks = [] 78 | await self._list_notebooks_recursive(path, notebooks) 79 | return notebooks 80 | 81 | async def _list_notebooks_recursive(self, path: str, notebooks: list[str]) -> None: 82 | """Helper to recursively list notebooks.""" 83 | try: 84 | model = await asyncio.to_thread( 85 | self.contents_manager.get, 86 | path, 87 | content=True 88 | ) 89 | 90 | if model['type'] == 'directory': 91 | for item in model['content']: 92 | item_path = f"{path}/{item['name']}" if path else item['name'] 93 | 94 | if item['type'] == 'directory': 95 | await self._list_notebooks_recursive(item_path, notebooks) 96 | elif item['type'] == 'notebook' or item['name'].endswith('.ipynb'): 97 | notebooks.append(item_path) 98 | except Exception: 99 | # Skip directories we can't access 100 | pass 101 | 102 | async def notebook_exists(self, path: str) -> bool: 103 | """ 104 | Check if notebook exists using local contents_manager. 105 | 106 | Args: 107 | path: Path to notebook 108 | 109 | Returns: 110 | True if exists 111 | """ 112 | try: 113 | await asyncio.to_thread( 114 | self.contents_manager.get, 115 | path, 116 | content=False 117 | ) 118 | return True 119 | except Exception: 120 | return False 121 | 122 | async def create_notebook(self, path: str) -> dict[str, Any]: 123 | """ 124 | Create a new notebook using local contents_manager. 125 | 126 | Args: 127 | path: Path for new notebook 128 | 129 | Returns: 130 | Created notebook content 131 | """ 132 | model = await asyncio.to_thread( 133 | self.contents_manager.new, 134 | path=path 135 | ) 136 | return model['content'] 137 | 138 | # Cell operations 139 | 140 | async def read_cells( 141 | self, 142 | path: str, 143 | start_index: Optional[int] = None, 144 | end_index: Optional[int] = None 145 | ) -> list[dict[str, Any]]: 146 | """ 147 | Read cells from notebook. 148 | 149 | Args: 150 | path: Notebook path 151 | start_index: Start index 152 | end_index: End index 153 | 154 | Returns: 155 | List of cells 156 | """ 157 | content = await self.get_notebook_content(path) 158 | cells = content.get('cells', []) 159 | 160 | if start_index is not None or end_index is not None: 161 | start = start_index or 0 162 | end = end_index if end_index is not None else len(cells) 163 | cells = cells[start:end] 164 | 165 | return cells 166 | 167 | async def append_cell( 168 | self, 169 | path: str, 170 | cell_type: Literal["code", "markdown"], 171 | source: Union[str, list[str]] 172 | ) -> int: 173 | """ 174 | Append a cell to notebook. 175 | 176 | Args: 177 | path: Notebook path 178 | cell_type: Cell type 179 | source: Cell source 180 | 181 | Returns: 182 | Index of appended cell 183 | """ 184 | content = await self.get_notebook_content(path) 185 | cells = content.get('cells', []) 186 | 187 | # Normalize source to list of strings 188 | if isinstance(source, str): 189 | source = source.splitlines(keepends=True) 190 | 191 | new_cell = { 192 | 'cell_type': cell_type, 193 | 'metadata': {}, 194 | 'source': source 195 | } 196 | 197 | if cell_type == 'code': 198 | new_cell['outputs'] = [] 199 | new_cell['execution_count'] = None 200 | 201 | cells.append(new_cell) 202 | content['cells'] = cells 203 | 204 | # Save updated notebook 205 | await asyncio.to_thread( 206 | self.contents_manager.save, 207 | { 208 | 'type': 'notebook', 209 | 'content': content 210 | }, 211 | path 212 | ) 213 | 214 | return len(cells) - 1 215 | 216 | async def insert_cell( 217 | self, 218 | path: str, 219 | cell_index: int, 220 | cell_type: Literal["code", "markdown"], 221 | source: Union[str, list[str]] 222 | ) -> int: 223 | """ 224 | Insert a cell at specific index. 225 | 226 | Args: 227 | path: Notebook path 228 | cell_index: Insert position 229 | cell_type: Cell type 230 | source: Cell source 231 | 232 | Returns: 233 | Index of inserted cell 234 | """ 235 | content = await self.get_notebook_content(path) 236 | cells = content.get('cells', []) 237 | 238 | # Normalize source 239 | if isinstance(source, str): 240 | source = source.splitlines(keepends=True) 241 | 242 | new_cell = { 243 | 'cell_type': cell_type, 244 | 'metadata': {}, 245 | 'source': source 246 | } 247 | 248 | if cell_type == 'code': 249 | new_cell['outputs'] = [] 250 | new_cell['execution_count'] = None 251 | 252 | cells.insert(cell_index, new_cell) 253 | content['cells'] = cells 254 | 255 | # Save updated notebook 256 | await asyncio.to_thread( 257 | self.contents_manager.save, 258 | { 259 | 'type': 'notebook', 260 | 'content': content 261 | }, 262 | path 263 | ) 264 | 265 | return cell_index 266 | 267 | async def delete_cell(self, path: str, cell_index: int) -> None: 268 | """ 269 | Delete a cell from notebook. 270 | 271 | Args: 272 | path: Notebook path 273 | cell_index: Index to delete 274 | """ 275 | content = await self.get_notebook_content(path) 276 | cells = content.get('cells', []) 277 | 278 | if 0 <= cell_index < len(cells): 279 | cells.pop(cell_index) 280 | content['cells'] = cells 281 | 282 | await asyncio.to_thread( 283 | self.contents_manager.save, 284 | { 285 | 'type': 'notebook', 286 | 'content': content 287 | }, 288 | path 289 | ) 290 | 291 | async def overwrite_cell( 292 | self, 293 | path: str, 294 | cell_index: int, 295 | new_source: Union[str, list[str]] 296 | ) -> tuple[str, str]: 297 | """ 298 | Overwrite cell content. 299 | 300 | Args: 301 | path: Notebook path 302 | cell_index: Cell index 303 | new_source: New source 304 | 305 | Returns: 306 | Tuple of (old_source, new_source) 307 | """ 308 | content = await self.get_notebook_content(path) 309 | cells = content.get('cells', []) 310 | 311 | if cell_index < 0 or cell_index >= len(cells): 312 | raise ValueError(f"Cell index {cell_index} out of range") 313 | 314 | cell = cells[cell_index] 315 | old_source = ''.join(cell['source']) if isinstance(cell['source'], list) else cell['source'] 316 | 317 | # Normalize new source 318 | if isinstance(new_source, str): 319 | new_source_str = new_source 320 | new_source = new_source.splitlines(keepends=True) 321 | else: 322 | new_source_str = ''.join(new_source) 323 | 324 | cell['source'] = new_source 325 | content['cells'] = cells 326 | 327 | await asyncio.to_thread( 328 | self.contents_manager.save, 329 | { 330 | 'type': 'notebook', 331 | 'content': content 332 | }, 333 | path 334 | ) 335 | 336 | return (old_source, new_source_str) 337 | 338 | # Kernel operations 339 | 340 | async def get_or_create_kernel(self, path: str, kernel_id: Optional[str] = None) -> str: 341 | """ 342 | Get existing kernel or create new one. 343 | 344 | Args: 345 | path: Notebook path (for context) 346 | kernel_id: Specific kernel ID 347 | 348 | Returns: 349 | Kernel ID 350 | """ 351 | if kernel_id and kernel_id in self.kernel_manager: 352 | return kernel_id 353 | 354 | # Start new kernel 355 | kernel_id = await self.kernel_manager.start_kernel() 356 | return kernel_id 357 | 358 | async def execute_cell( 359 | self, 360 | path: str, 361 | cell_index: int, 362 | kernel_id: str, 363 | timeout_seconds: int = 300 364 | ) -> list[Union[str, ImageContent]]: 365 | """ 366 | Execute a cell using local kernel manager. 367 | 368 | Args: 369 | path: Notebook path 370 | cell_index: Cell index 371 | kernel_id: Kernel ID 372 | timeout_seconds: Timeout 373 | 374 | Returns: 375 | List of outputs 376 | """ 377 | # Get cell source 378 | cells = await self.read_cells(path) 379 | if cell_index < 0 or cell_index >= len(cells): 380 | raise ValueError(f"Cell index {cell_index} out of range") 381 | 382 | cell = cells[cell_index] 383 | source = ''.join(cell['source']) if isinstance(cell['source'], list) else cell['source'] 384 | 385 | # Get kernel client 386 | kernel = self.kernel_manager.get_kernel(kernel_id) 387 | client = kernel.client() 388 | 389 | # Execute code 390 | msg_id = client.execute(source) 391 | 392 | # Collect outputs 393 | outputs = [] 394 | start_time = asyncio.get_event_loop().time() 395 | 396 | while True: 397 | if asyncio.get_event_loop().time() - start_time > timeout_seconds: 398 | raise TimeoutError(f"Cell execution exceeded {timeout_seconds} seconds") 399 | 400 | try: 401 | msg = await asyncio.wait_for( 402 | asyncio.to_thread(client.get_iopub_msg, timeout=1), 403 | timeout=2 404 | ) 405 | 406 | msg_type = msg['header']['msg_type'] 407 | 408 | if msg_type == 'status': 409 | if msg['content']['execution_state'] == 'idle': 410 | break 411 | elif msg_type in ['execute_result', 'display_data']: 412 | outputs.append(msg['content']) 413 | elif msg_type == 'stream': 414 | outputs.append(msg['content']) 415 | elif msg_type == 'error': 416 | outputs.append(msg['content']) 417 | except asyncio.TimeoutError: 418 | continue 419 | except Exception: 420 | break 421 | 422 | # Update cell with outputs 423 | content = await self.get_notebook_content(path) 424 | if cell_index < len(content['cells']): 425 | content['cells'][cell_index]['outputs'] = outputs 426 | await asyncio.to_thread( 427 | self.contents_manager.save, 428 | {'type': 'notebook', 'content': content}, 429 | path 430 | ) 431 | 432 | return safe_extract_outputs(outputs) 433 | 434 | async def interrupt_kernel(self, kernel_id: str) -> None: 435 | """Interrupt a kernel.""" 436 | if kernel_id in self.kernel_manager: 437 | kernel = self.kernel_manager.get_kernel(kernel_id) 438 | await kernel.interrupt() 439 | 440 | async def restart_kernel(self, kernel_id: str) -> None: 441 | """Restart a kernel.""" 442 | if kernel_id in self.kernel_manager: 443 | await self.kernel_manager.restart_kernel(kernel_id) 444 | 445 | async def shutdown_kernel(self, kernel_id: str) -> None: 446 | """Shutdown a kernel.""" 447 | if kernel_id in self.kernel_manager: 448 | await self.kernel_manager.shutdown_kernel(kernel_id) 449 | 450 | async def list_kernels(self) -> list[dict[str, Any]]: 451 | """List all running kernels.""" 452 | return [ 453 | { 454 | 'id': kid, 455 | 'name': self.kernel_manager.get_kernel(kid).kernel_name 456 | } 457 | for kid in self.kernel_manager.list_kernel_ids() 458 | ] 459 | 460 | async def kernel_exists(self, kernel_id: str) -> bool: 461 | """Check if kernel exists.""" 462 | return kernel_id in self.kernel_manager 463 | ``` -------------------------------------------------------------------------------- /jupyter_mcp_server/tools/insert_cell_tool.py: -------------------------------------------------------------------------------- ```python 1 | # Copyright (c) 2023-2024 Datalayer, Inc. 2 | # 3 | # BSD 3-Clause License 4 | 5 | """Insert cell tool implementation.""" 6 | 7 | from typing import Any, Optional, Literal 8 | from pathlib import Path 9 | import nbformat 10 | from jupyter_server_api import JupyterServerClient 11 | from jupyter_mcp_server.tools._base import BaseTool, ServerMode 12 | from jupyter_mcp_server.notebook_manager import NotebookManager 13 | from jupyter_mcp_server.utils import get_current_notebook_context 14 | from jupyter_mcp_server.utils import get_surrounding_cells_info 15 | 16 | 17 | class InsertCellTool(BaseTool): 18 | """Tool to insert a cell at a specified position.""" 19 | 20 | @property 21 | def name(self) -> str: 22 | return "insert_cell" 23 | 24 | @property 25 | def description(self) -> str: 26 | return """Insert a cell to specified position. 27 | 28 | Args: 29 | cell_index: target index for insertion (0-based). Use -1 to append at end. 30 | cell_type: Type of cell to insert ("code" or "markdown") 31 | cell_source: Source content for the cell 32 | 33 | Returns: 34 | str: Success message and the structure of its surrounding cells (up to 5 cells above and 5 cells below)""" 35 | 36 | async def _get_jupyter_ydoc(self, serverapp: Any, file_id: str): 37 | """Get the YNotebook document if it's currently open in a collaborative session. 38 | 39 | This follows the jupyter_ai_tools pattern of accessing YDoc through the 40 | yroom_manager when the notebook is actively being edited. 41 | 42 | Args: 43 | serverapp: The Jupyter ServerApp instance 44 | file_id: The file ID for the document 45 | 46 | Returns: 47 | YNotebook instance or None if not in a collaborative session 48 | """ 49 | try: 50 | yroom_manager = serverapp.web_app.settings.get("yroom_manager") 51 | if yroom_manager is None: 52 | return None 53 | 54 | room_id = f"json:notebook:{file_id}" 55 | 56 | if yroom_manager.has_room(room_id): 57 | yroom = yroom_manager.get_room(room_id) 58 | notebook = await yroom.get_jupyter_ydoc() 59 | return notebook 60 | except Exception: 61 | # YDoc not available, will fall back to file operations 62 | pass 63 | 64 | return None 65 | 66 | async def _insert_cell_ydoc( 67 | self, 68 | serverapp: Any, 69 | notebook_path: str, 70 | cell_index: int, 71 | cell_type: Literal["code", "markdown"], 72 | cell_source: str 73 | ) -> str: 74 | """Insert cell using YDoc (collaborative editing mode). 75 | 76 | Args: 77 | serverapp: Jupyter ServerApp instance 78 | notebook_path: Path to the notebook 79 | cell_index: Index to insert at (-1 for append) 80 | cell_type: Type of cell to insert 81 | cell_source: Source content for the cell 82 | 83 | Returns: 84 | Success message with surrounding cells info 85 | """ 86 | # Get file_id from file_id_manager 87 | file_id_manager = serverapp.web_app.settings.get("file_id_manager") 88 | if file_id_manager is None: 89 | raise RuntimeError("file_id_manager not available in serverapp") 90 | 91 | file_id = file_id_manager.get_id(notebook_path) 92 | 93 | # Try to get YDoc 94 | ydoc = await self._get_jupyter_ydoc(serverapp, file_id) 95 | 96 | if ydoc: 97 | # Notebook is open in collaborative mode, use YDoc 98 | total_cells = len(ydoc.ycells) 99 | actual_index = cell_index if cell_index != -1 else total_cells 100 | 101 | if actual_index < 0 or actual_index > total_cells: 102 | raise ValueError( 103 | f"Cell index {cell_index} is out of range. Notebook has {total_cells} cells. Use -1 to append at end." 104 | ) 105 | 106 | # Create the cell 107 | cell = { 108 | "cell_type": cell_type, 109 | "source": "", 110 | } 111 | ycell = ydoc.create_ycell(cell) 112 | 113 | # Insert at the specified position 114 | if actual_index >= total_cells: 115 | ydoc.ycells.append(ycell) 116 | else: 117 | ydoc.ycells.insert(actual_index, ycell) 118 | 119 | # Write content to the cell collaboratively 120 | if cell_source: 121 | # Set the source directly on the ycell 122 | ycell["source"] = cell_source 123 | 124 | # Get surrounding cells info (simplified version for YDoc) 125 | new_total_cells = len(ydoc.ycells) 126 | surrounding_info = self._get_surrounding_cells_info_ydoc(ydoc, actual_index, new_total_cells) 127 | 128 | return f"Cell inserted successfully at index {actual_index} ({cell_type})!\n\nCurrent Surrounding Cells:\n{surrounding_info}" 129 | else: 130 | # YDoc not available, use file operations 131 | return await self._insert_cell_file(notebook_path, cell_index, cell_type, cell_source) 132 | 133 | def _get_surrounding_cells_info_ydoc(self, ydoc, center_index: int, total_cells: int) -> str: 134 | """Get info about surrounding cells from YDoc.""" 135 | lines = [] 136 | start_index = max(0, center_index - 5) 137 | end_index = min(total_cells, center_index + 6) 138 | 139 | for i in range(start_index, end_index): 140 | cell = ydoc.ycells[i] 141 | cell_type = cell.get("cell_type", "unknown") 142 | source = cell.get("source", "") 143 | if isinstance(source, list): 144 | source = "".join(source) 145 | first_line = source.split('\n')[0][:50] if source else "(empty)" 146 | marker = " <-- NEW" if i == center_index else "" 147 | lines.append(f" [{i}] {cell_type}: {first_line}{marker}") 148 | 149 | return "\n".join(lines) 150 | 151 | async def _insert_cell_file( 152 | self, 153 | notebook_path: str, 154 | cell_index: int, 155 | cell_type: Literal["code", "markdown"], 156 | cell_source: str 157 | ) -> str: 158 | """Insert cell using file operations (non-collaborative mode). 159 | 160 | Args: 161 | notebook_path: Absolute path to the notebook 162 | cell_index: Index to insert at (-1 for append) 163 | cell_type: Type of cell to insert 164 | cell_source: Source content for the cell 165 | 166 | Returns: 167 | Success message with surrounding cells info 168 | """ 169 | # Read notebook file 170 | with open(notebook_path, "r", encoding="utf-8") as f: 171 | # Read as version 4 (latest) to ensure consistency and support for cell IDs 172 | notebook = nbformat.read(f, as_version=4) 173 | 174 | # Clean any transient fields from existing outputs (kernel protocol field not in nbformat schema) 175 | self._clean_notebook_outputs(notebook) 176 | 177 | total_cells = len(notebook.cells) 178 | actual_index = cell_index if cell_index != -1 else total_cells 179 | 180 | if actual_index < 0 or actual_index > total_cells: 181 | raise ValueError( 182 | f"Cell index {cell_index} is out of range. Notebook has {total_cells} cells. Use -1 to append at end." 183 | ) 184 | 185 | # Create and insert the cell 186 | if cell_type == "code": 187 | new_cell = nbformat.v4.new_code_cell(source=cell_source or "") 188 | elif cell_type == "markdown": 189 | new_cell = nbformat.v4.new_markdown_cell(source=cell_source or "") 190 | else: 191 | raise ValueError(f"Invalid cell_type: {cell_type}. Must be 'code' or 'markdown'.") 192 | 193 | notebook.cells.insert(actual_index, new_cell) 194 | 195 | # Write back to file 196 | with open(notebook_path, "w", encoding="utf-8") as f: 197 | nbformat.write(notebook, f) 198 | 199 | # Get surrounding cells info 200 | new_total_cells = len(notebook.cells) 201 | surrounding_info = self._get_surrounding_cells_info_file(notebook, actual_index, new_total_cells) 202 | 203 | return f"Cell inserted successfully at index {actual_index} ({cell_type})!\n\nCurrent Surrounding Cells:\n{surrounding_info}" 204 | 205 | def _clean_notebook_outputs(self, notebook): 206 | """Remove transient fields from all cell outputs. 207 | 208 | The 'transient' field is part of the Jupyter kernel messaging protocol 209 | but is NOT part of the nbformat schema. This causes validation errors. 210 | 211 | Args: 212 | notebook: nbformat notebook object to clean (modified in place) 213 | """ 214 | # Clean transient fields from outputs 215 | for cell in notebook.cells: 216 | if cell.cell_type == 'code' and hasattr(cell, 'outputs'): 217 | for output in cell.outputs: 218 | if isinstance(output, dict) and 'transient' in output: 219 | del output['transient'] 220 | 221 | def _get_surrounding_cells_info_file(self, notebook, center_index: int, total_cells: int) -> str: 222 | """Get info about surrounding cells from nbformat notebook.""" 223 | lines = [] 224 | start_index = max(0, center_index - 5) 225 | end_index = min(total_cells, center_index + 6) 226 | 227 | for i in range(start_index, end_index): 228 | cell = notebook.cells[i] 229 | cell_type = cell.cell_type 230 | source = cell.source 231 | first_line = source.split('\n')[0][:50] if source else "(empty)" 232 | marker = " <-- NEW" if i == center_index else "" 233 | lines.append(f" [{i}] {cell_type}: {first_line}{marker}") 234 | 235 | return "\n".join(lines) 236 | 237 | async def _insert_cell_websocket( 238 | self, 239 | notebook_manager: NotebookManager, 240 | cell_index: int, 241 | cell_type: Literal["code", "markdown"], 242 | cell_source: str 243 | ) -> str: 244 | """Insert cell using WebSocket connection (MCP_SERVER mode). 245 | 246 | Args: 247 | notebook_manager: Notebook manager instance 248 | cell_index: Index to insert at (-1 for append) 249 | cell_type: Type of cell to insert 250 | cell_source: Source content for the cell 251 | 252 | Returns: 253 | Success message with surrounding cells info 254 | """ 255 | async with notebook_manager.get_current_connection() as notebook: 256 | actual_index = cell_index if cell_index != -1 else len(notebook) 257 | if actual_index < 0 or actual_index > len(notebook): 258 | raise ValueError(f"Cell index {cell_index} out of range") 259 | 260 | notebook.insert_cell(actual_index, cell_source, cell_type) 261 | 262 | # Get surrounding cells info 263 | new_total_cells = len(notebook) 264 | surrounding_info = get_surrounding_cells_info(notebook, actual_index, new_total_cells) 265 | 266 | return f"Cell inserted successfully at index {actual_index} ({cell_type})!\n\nCurrent Surrounding Cells:\n{surrounding_info}" 267 | 268 | async def execute( 269 | self, 270 | mode: ServerMode, 271 | server_client: Optional[JupyterServerClient] = None, 272 | kernel_client: Optional[Any] = None, 273 | contents_manager: Optional[Any] = None, 274 | kernel_manager: Optional[Any] = None, 275 | kernel_spec_manager: Optional[Any] = None, 276 | notebook_manager: Optional[NotebookManager] = None, 277 | # Tool-specific parameters 278 | cell_index: int = None, 279 | cell_type: Literal["code", "markdown"] = None, 280 | cell_source: str = None, 281 | **kwargs 282 | ) -> str: 283 | """Execute the insert_cell tool. 284 | 285 | This tool supports three modes of operation: 286 | 287 | 1. JUPYTER_SERVER mode with YDoc (collaborative): 288 | - Checks if notebook is open in a collaborative session 289 | - Uses YDoc for real-time collaborative editing 290 | - Changes are immediately visible to all connected users 291 | 292 | 2. JUPYTER_SERVER mode without YDoc (file-based): 293 | - Falls back to direct file operations using nbformat 294 | - Suitable when notebook is not actively being edited 295 | 296 | 3. MCP_SERVER mode (WebSocket): 297 | - Uses WebSocket connection to remote Jupyter server 298 | - Accesses YDoc through NbModelClient 299 | 300 | Args: 301 | mode: Server mode (MCP_SERVER or JUPYTER_SERVER) 302 | server_client: HTTP client for MCP_SERVER mode 303 | contents_manager: Direct API access for JUPYTER_SERVER mode 304 | notebook_manager: Notebook manager instance 305 | cell_index: Target index for insertion (0-based, -1 to append) 306 | cell_type: Type of cell ("code" or "markdown") 307 | cell_source: Source content for the cell 308 | **kwargs: Additional parameters 309 | 310 | Returns: 311 | Success message with surrounding cells info 312 | """ 313 | if mode == ServerMode.JUPYTER_SERVER and contents_manager is not None: 314 | # JUPYTER_SERVER mode: Try YDoc first, fall back to file operations 315 | from jupyter_mcp_server.jupyter_extension.context import get_server_context 316 | 317 | context = get_server_context() 318 | serverapp = context.serverapp 319 | notebook_path, _ = get_current_notebook_context(notebook_manager) 320 | 321 | # Resolve to absolute path 322 | if serverapp and not Path(notebook_path).is_absolute(): 323 | root_dir = serverapp.root_dir 324 | notebook_path = str(Path(root_dir) / notebook_path) 325 | 326 | if serverapp: 327 | # Try YDoc approach first 328 | return await self._insert_cell_ydoc(serverapp, notebook_path, cell_index, cell_type, cell_source) 329 | else: 330 | # Fall back to file operations 331 | return await self._insert_cell_file(notebook_path, cell_index, cell_type, cell_source) 332 | 333 | elif mode == ServerMode.MCP_SERVER and notebook_manager is not None: 334 | # MCP_SERVER mode: Use WebSocket connection 335 | return await self._insert_cell_websocket(notebook_manager, cell_index, cell_type, cell_source) 336 | else: 337 | raise ValueError(f"Invalid mode or missing required clients: mode={mode}") 338 | ``` -------------------------------------------------------------------------------- /docs/src/css/custom.css: -------------------------------------------------------------------------------- ```css 1 | /* 2 | * Copyright (c) 2023-2024 Datalayer, Inc. 3 | * 4 | * BSD 3-Clause License 5 | */ 6 | 7 | /* stylelint-disable docusaurus/copyright-header */ 8 | /** 9 | * Any CSS included here will be global. The classic template 10 | * bundles Infima by default. Infima is a CSS framework designed to 11 | * work well for content-centric websites. 12 | */ 13 | 14 | /* You can override the default Infima variables here. */ 15 | :root { 16 | --ifm-color-primary: #25c2a0; 17 | --ifm-color-primary-dark: rgb(33, 175, 144); 18 | --ifm-color-primary-darker: rgb(31, 165, 136); 19 | --ifm-color-primary-darkest: rgb(26, 136, 112); 20 | --ifm-color-primary-light: rgb(70, 203, 174); 21 | --ifm-color-primary-lighter: rgb(102, 212, 189); 22 | --ifm-color-primary-lightest: rgb(146, 224, 208); 23 | --ifm-code-font-size: 95%; 24 | } 25 | 26 | .docusaurus-highlight-code-line { 27 | background-color: rgb(72, 77, 91); 28 | display: block; 29 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 30 | padding: 0 var(--ifm-pre-padding); 31 | } 32 | 33 | .header-datalayer-io-link::before { 34 | content: ''; 35 | width: 24px; 36 | height: 24px; 37 | display: flex; 38 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' aria-hidden='true' viewBox='0 0 20 20'%3E%3Cpath fill='%232ECC71' d='M0 0h20v4H0zm0 0'/%3E%3Cpath fill='%231ABC9C' d='M0 8h20v4H0zm0 0'/%3E%3Cpath fill='%2316A085' d='M0 16h20v4H0zm0 0'/%3E%3C/svg%3E%0A") 39 | no-repeat; 40 | } 41 | 42 | .header-datalayer-io-link:hover { 43 | opacity: 0.6; 44 | } 45 | 46 | [data-theme='dark'] .header-datalayer-io-link::before { 47 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' aria-hidden='true' viewBox='0 0 20 20'%3E%3Cpath fill='%232ECC71' d='M0 0h20v4H0zm0 0'/%3E%3Cpath fill='%231ABC9C' d='M0 8h20v4H0zm0 0'/%3E%3Cpath fill='%2316A085' d='M0 16h20v4H0zm0 0'/%3E%3C/svg%3E%0A") 48 | no-repeat; 49 | } 50 | 51 | .header-github-link::before { 52 | content: ''; 53 | width: 24px; 54 | height: 24px; 55 | display: flex; 56 | background: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg viewBox='0 0 80 80' version='1.1' id='svg4' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'%3E%3Cdefs id='defs8' /%3E%3Cpath fill='%23959da5' d='M 40,0 C 17.9,0 0,17.900001 0,40 c 0,17.7 11.45,32.65 27.35,37.950001 2,0.35 2.75,-0.85 2.75,-1.9 0,-0.95 -0.05,-4.1 -0.05,-7.45 C 20,70.45 17.4,66.15 16.6,63.9 16.15,62.75 14.2,59.2 12.5,58.25 11.1,57.5 9.1,55.65 12.45,55.600001 c 3.15,-0.05 5.4,2.899999 6.15,4.1 3.6,6.05 9.35,4.35 11.65,3.3 0.35,-2.6 1.4,-4.35 2.55,-5.35 -8.9,-1 -18.2,-4.45 -18.2,-19.75 0,-4.35 1.55,-7.95 4.1,-10.75 -0.4,-1 -1.8,-5.1 0.4,-10.6 0,0 3.35,-1.05 11,4.1 3.2,-0.9 6.6,-1.35 10,-1.35 3.4,0 6.8,0.45 10,1.35 7.65,-5.2 11,-4.1 11,-4.1 2.2,5.5 0.8,9.6 0.4,10.6 2.55,2.8 4.1,6.35 4.1,10.75 0,15.35 -9.35,18.75 -18.25,19.75 1.45,1.25 2.7,3.65 2.7,7.4 0,5.349999 -0.05,9.65 -0.05,11 0,1.05 0.75,2.3 2.75,1.9 A 40.065,40.065 0 0 0 80,40 C 80,17.900001 62.1,0 40,0 Z' id='path2' style='stroke-width:5' /%3E%3C/svg%3E%0A") 57 | no-repeat; 58 | } 59 | 60 | .header-github-link:hover { 61 | opacity: 0.6; 62 | } 63 | 64 | [data-theme='dark'] .header-github-link::before { 65 | background: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8' standalone='no'%3F%3E%3Csvg viewBox='0 0 80 80' version='1.1' id='svg4' xmlns='http://www.w3.org/2000/svg' xmlns:svg='http://www.w3.org/2000/svg'%3E%3Cdefs id='defs8' /%3E%3Cpath fill='%23959da5' d='M 40,0 C 17.9,0 0,17.900001 0,40 c 0,17.7 11.45,32.65 27.35,37.950001 2,0.35 2.75,-0.85 2.75,-1.9 0,-0.95 -0.05,-4.1 -0.05,-7.45 C 20,70.45 17.4,66.15 16.6,63.9 16.15,62.75 14.2,59.2 12.5,58.25 11.1,57.5 9.1,55.65 12.45,55.600001 c 3.15,-0.05 5.4,2.899999 6.15,4.1 3.6,6.05 9.35,4.35 11.65,3.3 0.35,-2.6 1.4,-4.35 2.55,-5.35 -8.9,-1 -18.2,-4.45 -18.2,-19.75 0,-4.35 1.55,-7.95 4.1,-10.75 -0.4,-1 -1.8,-5.1 0.4,-10.6 0,0 3.35,-1.05 11,4.1 3.2,-0.9 6.6,-1.35 10,-1.35 3.4,0 6.8,0.45 10,1.35 7.65,-5.2 11,-4.1 11,-4.1 2.2,5.5 0.8,9.6 0.4,10.6 2.55,2.8 4.1,6.35 4.1,10.75 0,15.35 -9.35,18.75 -18.25,19.75 1.45,1.25 2.7,3.65 2.7,7.4 0,5.349999 -0.05,9.65 -0.05,11 0,1.05 0.75,2.3 2.75,1.9 A 40.065,40.065 0 0 0 80,40 C 80,17.900001 62.1,0 40,0 Z' id='path2' style='stroke-width:5' /%3E%3C/svg%3E%0A") 66 | no-repeat; 67 | } 68 | 69 | .header-bluesky-link::before { 70 | content: ''; 71 | width: 24px; 72 | height: 24px; 73 | display: flex; 74 | background: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22no%22%3F%3E%0A%3Csvg%0A%20%20%20width%3D%2224%22%0A%20%20%20height%3D%2224%22%0A%20%20%20viewBox%3D%220%200%202.88%202.88%22%0A%20%20%20version%3D%221.1%22%0A%20%20%20id%3D%22svg4%22%0A%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0A%20%20%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%0A%20%20%20%20%20id%3D%22defs8%22%20%2F%3E%0A%20%20%3Cpath%0A%20%20%20%20%20fill%3D%22%23959da5%22%0A%20%20%20%20%20d%3D%22M%201.44%2C1.306859%20C%201.30956%2C1.053179%200.95447995%2C0.58049901%200.62423999%2C0.34745901%200.30791999%2C0.12413901%200.18732%2C0.16277901%200.10824%2C0.19865901%200.01668%2C0.23981901%200%2C0.38045901%200%2C0.46301901%20c%200%2C0.0828%200.04536%2C0.67799999%200.07488%2C0.77747999%200.0978%2C0.32832%200.44556%2C0.4392%200.76595999%2C0.4036799%200.01632%2C-0.0024%200.033%2C-0.00468%200.0498%2C-0.00672%20-0.01656%2C0.00264%20-0.03312%2C0.0048%20-0.0498%2C0.00672%20C%200.3714%2C1.7137789%20-0.0456%2C1.884779%200.50124%2C2.493539%201.1027999%2C3.1163391%201.3256399%2C2.359979%201.44%2C1.976579%201.55436%2C2.359979%201.686%2C3.0890991%202.36796%2C2.493539%202.88%2C1.976579%202.5086%2C1.713779%202.0391599%2C1.6441789%20a%201.04892%2C1.04892%200%200%201%20-0.0498%2C-0.00672%20c%200.0168%2C0.00204%200.03348%2C0.00432%200.0498%2C0.00672%20C%202.35956%2C1.6798189%202.7073199%2C1.5688189%202.80512%2C1.240499%202.8346401%2C1.141139%202.88%2C0.54569891%202.88%2C0.46313901%20c%200%2C-0.0828%20-0.01668%2C-0.22332%20-0.10824%2C-0.26472%20-0.07908%2C-0.03576%20-0.19968%2C-0.0744%20-0.516%2C0.1488%20C%201.92552%2C0.58061901%201.5704399%2C1.053299%201.44%2C1.306859%22%0A%20%20%20%20%20style%3D%22stroke-width%3A0.12%22%0A%20%20%20%20%20id%3D%22path2%22%20%2F%3E%0A%3C%2Fsvg%3E%0A") 75 | no-repeat; 76 | } 77 | 78 | .header-bluesky-link:hover { 79 | opacity: 0.6; 80 | } 81 | 82 | [data-theme='dark'] .header-bluesky-link::before { 83 | background: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22no%22%3F%3E%0A%3Csvg%0A%20%20%20width%3D%2224%22%0A%20%20%20height%3D%2224%22%0A%20%20%20viewBox%3D%220%200%202.88%202.88%22%0A%20%20%20version%3D%221.1%22%0A%20%20%20id%3D%22svg4%22%0A%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0A%20%20%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%0A%20%20%20%20%20id%3D%22defs8%22%20%2F%3E%0A%20%20%3Cpath%0A%20%20%20%20%20fill%3D%22%23959da5%22%0A%20%20%20%20%20d%3D%22M%201.44%2C1.306859%20C%201.30956%2C1.053179%200.95447995%2C0.58049901%200.62423999%2C0.34745901%200.30791999%2C0.12413901%200.18732%2C0.16277901%200.10824%2C0.19865901%200.01668%2C0.23981901%200%2C0.38045901%200%2C0.46301901%20c%200%2C0.0828%200.04536%2C0.67799999%200.07488%2C0.77747999%200.0978%2C0.32832%200.44556%2C0.4392%200.76595999%2C0.4036799%200.01632%2C-0.0024%200.033%2C-0.00468%200.0498%2C-0.00672%20-0.01656%2C0.00264%20-0.03312%2C0.0048%20-0.0498%2C0.00672%20C%200.3714%2C1.7137789%20-0.0456%2C1.884779%200.50124%2C2.493539%201.1027999%2C3.1163391%201.3256399%2C2.359979%201.44%2C1.976579%201.55436%2C2.359979%201.686%2C3.0890991%202.36796%2C2.493539%202.88%2C1.976579%202.5086%2C1.713779%202.0391599%2C1.6441789%20a%201.04892%2C1.04892%200%200%201%20-0.0498%2C-0.00672%20c%200.0168%2C0.00204%200.03348%2C0.00432%200.0498%2C0.00672%20C%202.35956%2C1.6798189%202.7073199%2C1.5688189%202.80512%2C1.240499%202.8346401%2C1.141139%202.88%2C0.54569891%202.88%2C0.46313901%20c%200%2C-0.0828%20-0.01668%2C-0.22332%20-0.10824%2C-0.26472%20-0.07908%2C-0.03576%20-0.19968%2C-0.0744%20-0.516%2C0.1488%20C%201.92552%2C0.58061901%201.5704399%2C1.053299%201.44%2C1.306859%22%0A%20%20%20%20%20style%3D%22stroke-width%3A0.12%22%0A%20%20%20%20%20id%3D%22path2%22%20%2F%3E%0A%3C%2Fsvg%3E%0A") 84 | no-repeat; 85 | } 86 | 87 | .header-linkedin-link::before { 88 | content: ''; 89 | width: 24px; 90 | height: 24px; 91 | display: flex; 92 | background: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 18'%3E%3Cpath d='M3.94 2A2 2 0 1 1 2 0a2 2 0 0 1 1.94 2zM4 5.48H0V18h4zm6.32 0H6.34V18h3.94v-6.57c0-3.66 4.77-4 4.77 0V18H19v-7.93c0-6.17-7.06-5.94-8.72-2.91z' fill='rgb(149, 157, 165)'/%3E%3C/svg%3E") 93 | no-repeat; 94 | } 95 | 96 | .header-linkedin-link:hover { 97 | opacity: 0.6; 98 | } 99 | 100 | [data-theme='dark'] .header-linkedin-link::before { 101 | background-image: url("data:image/svg+xml,%0A%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 19 18'%3E%3Cpath d='M3.94 2A2 2 0 1 1 2 0a2 2 0 0 1 1.94 2zM4 5.48H0V18h4zm6.32 0H6.34V18h3.94v-6.57c0-3.66 4.77-4 4.77 0V18H19v-7.93c0-6.17-7.06-5.94-8.72-2.91z' fill='rgb(149, 157, 165)'/%3E%3C/svg%3E") 102 | no-repeat; 103 | } 104 | 105 | .header-x-link::before { 106 | content: ''; 107 | width: 24px; 108 | height: 24px; 109 | display: flex; 110 | background: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%201200%201227%22%20fill%3D%22rgb(149%2C%20157%2C%20165)%22%3E%3Cpath%20d%3D%22M714.163%20519.284%201160.89%200h-105.86L667.137%20450.887%20357.328%200H0l468.492%20681.821L0%201226.37h105.866l409.625-476.152%20327.181%20476.152H1200L714.137%20519.284h.026ZM569.165%20687.828l-47.468-67.894-377.686-540.24h162.604l304.797%20435.991%2047.468%2067.894%20396.2%20566.721H892.476L569.165%20687.854v-.026Z%22%20%2F%3E%3C%2Fsvg%3E") 111 | no-repeat; 112 | } 113 | 114 | .header-x-link:hover { 115 | opacity: 0.6; 116 | } 117 | 118 | [data-theme='dark'] .header-x-link::before { 119 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%231DA1F2' viewBox='0 0 20 20' aria-hidden='true'%3E%3Cpath d='M19.96 3.808a8.333 8.333 0 01-2.353.646 4.132 4.132 0 001.802-2.269 8.47 8.47 0 01-2.606.987 4.1 4.1 0 00-6.986 3.735c-3.409-.161-6.428-1.799-8.45-4.272a4.018 4.018 0 00-.555 2.063A4.1 4.1 0 002.635 8.11a4.087 4.087 0 01-1.857-.513v.05a4.102 4.102 0 003.289 4.022 4.162 4.162 0 01-1.844.07 4.114 4.114 0 003.837 2.848 8.223 8.223 0 01-5.085 1.755c-.325 0-.65-.02-.975-.056a11.662 11.662 0 006.298 1.84c7.544 0 11.665-6.246 11.665-11.654 0-.175 0-.35-.013-.525A8.278 8.278 0 0020 3.825l-.04-.017z'/%3E%3C/svg%3E%0A") 120 | no-repeat; 121 | } 122 | 123 | .header-discord-link::before { 124 | content: ''; 125 | width: 24px; 126 | height: 18px; 127 | display: flex; 128 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 127.14 96.36'%3E%3Cpath fill='rgb(149, 157, 165)' d='M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z'/%3E%3C/svg%3E%0A") 129 | no-repeat; 130 | } 131 | 132 | .header-discord-link:hover { 133 | opacity: 0.6; 134 | } 135 | 136 | [data-theme='dark'] .header-discord-link::before { 137 | background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 127.14 96.36'%3E%3Cpath fill='rgb(149, 157, 165)' d='M107.7,8.07A105.15,105.15,0,0,0,81.47,0a72.06,72.06,0,0,0-3.36,6.83A97.68,97.68,0,0,0,49,6.83,72.37,72.37,0,0,0,45.64,0,105.89,105.89,0,0,0,19.39,8.09C2.79,32.65-1.71,56.6.54,80.21h0A105.73,105.73,0,0,0,32.71,96.36,77.7,77.7,0,0,0,39.6,85.25a68.42,68.42,0,0,1-10.85-5.18c.91-.66,1.8-1.34,2.66-2a75.57,75.57,0,0,0,64.32,0c.87.71,1.76,1.39,2.66,2a68.68,68.68,0,0,1-10.87,5.19,77,77,0,0,0,6.89,11.1A105.25,105.25,0,0,0,126.6,80.22h0C129.24,52.84,122.09,29.11,107.7,8.07ZM42.45,65.69C36.18,65.69,31,60,31,53s5-12.74,11.43-12.74S54,46,53.89,53,48.84,65.69,42.45,65.69Zm42.24,0C78.41,65.69,73.25,60,73.25,53s5-12.74,11.44-12.74S96.23,46,96.12,53,91.08,65.69,84.69,65.69Z'/%3E%3C/svg%3E%0A") 138 | no-repeat; 139 | } 140 | 141 | .header-tiktok-link::before { 142 | content: ''; 143 | width: 24px; 144 | height: 24px; 145 | display: flex; 146 | background: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22no%22%3F%3E%0A%3Csvg%0A%20%20%20fill%3D%22%23959da5%22%0A%20%20%20width%3D%2224%22%0A%20%20%20height%3D%2224%22%0A%20%20%20viewBox%3D%220%200%200.72%200.72%22%0A%20%20%20xml%3Aspace%3D%22preserve%22%0A%20%20%20version%3D%221.1%22%0A%20%20%20id%3D%22svg4%22%0A%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0A%20%20%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cdefs%0A%20%20%20%20%20id%3D%22defs8%22%20%2F%3E%3Cpath%0A%20%20%20%20%20d%3D%22M%200.63539429%2C0.16867393%20A%200.1725255%2C0.17252543%200%200%201%200.49969199%2C0.01587392%20V%200%20h%20-0.1240039%20v%200.49212763%20a%200.10424241%2C0.10424236%200%200%201%20-0.1872116%2C0.0627398%20l%20-7.2e-5%2C-3.6e-5%207.2e-5%2C3.6e-5%20A%200.10420641%2C0.10420637%200%200%201%200.30304959%2C0.39252865%20V%200.26654513%20A%200.22781429%2C0.2278142%200%200%200%200.10889089%2C0.65140678%200.22785029%2C0.2278502%200%200%200%200.49969199%2C0.4921636%20V%200.24070051%20a%200.2945136%2C0.29451347%200%200%200%200.1718056%2C0.0549288%20V%200.17241745%20a%200.17389332%2C0.17389325%200%200%201%20-0.0361033%2C-0.003744%20z%22%0A%20%20%20%20%20id%3D%22path2%22%0A%20%20%20%20%20style%3D%22stroke-width%3A0.0359952%22%20%2F%3E%3C%2Fsvg%3E%0A") 147 | no-repeat; 148 | } 149 | 150 | .header-tiktok-link:hover { 151 | opacity: 0.6; 152 | } 153 | 154 | .header-youtube-link::before { 155 | content: ''; 156 | width: 24px; 157 | height: 20px; 158 | display: flex; 159 | background: url("data:image/svg+xml,%3C%3Fxml%20version%3D%221.0%22%20encoding%3D%22UTF-8%22%20standalone%3D%22no%22%3F%3E%0A%3Csvg%0A%20%20%20viewBox%3D%220%200%2024%2024%22%0A%20%20%20version%3D%221.1%22%0A%20%20%20id%3D%22svg4%22%0A%20%20%20width%3D%2224%22%0A%20%20%20height%3D%2224%22%0A%20%20%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%0A%20%20%20xmlns%3Asvg%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%0A%20%20%3Cdefs%0A%20%20%20%20%20id%3D%22defs8%22%20%2F%3E%0A%20%20%3Cpath%0A%20%20%20%20%20d%3D%22M%2023.496693%2C5.8315054%20A%203.0042862%2C3.0042862%200%200%200%2021.393692%2C3.6909515%20C%2019.516013%2C3.1652014%2011.992781%2C3.1652014%2011.992781%2C3.1652014%20A%2072.040279%2C72.040279%200%200%200%202.6043863%2C3.6659158%203.1169469%2C3.1169469%200%200%200%200.488868%2C5.8315054%2032.884416%2C32.884416%200%200%200%206.7149698e-4%2C11.677346%2032.734202%2C32.734202%200%200%200%200.488868%2C17.523186%203.0418398%2C3.0418398%200%200%200%202.6043863%2C19.663739%20c%201.9027146%2C0.525751%209.3883947%2C0.525751%209.3883947%2C0.525751%20a%2072.215529%2C72.215529%200%200%200%209.400911%2C-0.500715%203.0042862%2C3.0042862%200%200%200%202.103001%2C-2.140554%2032.083273%2C32.083273%200%200%200%200.500714%2C-5.845839%2030.042862%2C30.042862%200%200%200%20-0.500714%2C-5.8708766%20z%20M%209.6018695%2C15.320042%20V%208.0346486%20l%206.2589285%2C3.6426974%20z%22%0A%20%20%20%20%20fill%3D%22%23959da5%22%0A%20%20%20%20%20id%3D%22path2%22%0A%20%20%20%20%20style%3D%22stroke-width%3A1.25179%22%20%2F%3E%0A%3C%2Fsvg%3E%0A") 160 | no-repeat; 161 | } 162 | 163 | .header-youtube-link:hover { 164 | opacity: 0.6; 165 | } 166 | 167 | header .container { 168 | max-width: 9000px; 169 | } 170 | ``` -------------------------------------------------------------------------------- /docs/static/img/product_1.svg: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- 3 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 4 | ~ 5 | ~ BSD 3-Clause License 6 | --> 7 | 8 | <svg 9 | xmlns:dc="http://purl.org/dc/elements/1.1/" 10 | xmlns:cc="http://creativecommons.org/ns#" 11 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 12 | xmlns:svg="http://www.w3.org/2000/svg" 13 | xmlns="http://www.w3.org/2000/svg" 14 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 15 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 | viewBox="0 0 222.55631 184.29483" 17 | version="1.1" 18 | id="svg1377" 19 | sodipodi:docname="1.svg" 20 | inkscape:version="1.0.1 (c497b03c, 2020-09-10)" 21 | width="222.5563" 22 | height="184.29483"> 23 | <metadata 24 | id="metadata1381"> 25 | <rdf:RDF> 26 | <cc:Work 27 | rdf:about=""> 28 | <dc:format>image/svg+xml</dc:format> 29 | <dc:type 30 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 31 | <dc:title>Cloud_database_SVG</dc:title> 32 | </cc:Work> 33 | </rdf:RDF> 34 | </metadata> 35 | <sodipodi:namedview 36 | pagecolor="#ffffff" 37 | bordercolor="#666666" 38 | borderopacity="1" 39 | objecttolerance="10" 40 | gridtolerance="10" 41 | guidetolerance="10" 42 | inkscape:pageopacity="0" 43 | inkscape:pageshadow="2" 44 | inkscape:window-width="1349" 45 | inkscape:window-height="694" 46 | id="namedview1379" 47 | showgrid="false" 48 | inkscape:zoom="0.81391066" 49 | inkscape:cx="246.97899" 50 | inkscape:cy="87.469146" 51 | inkscape:window-x="173" 52 | inkscape:window-y="109" 53 | inkscape:window-maximized="0" 54 | inkscape:current-layer="svg1377" 55 | inkscape:document-rotation="0" 56 | fit-margin-top="0" 57 | fit-margin-left="0" 58 | fit-margin-right="0" 59 | fit-margin-bottom="0" /> 60 | <defs 61 | id="defs847"> 62 | <style 63 | id="style833">.cls-1{fill:none;}.cls-2{clip-path:url(#clip-path);}.cls-3{fill:#d3d3dd;}.cls-4{fill:#e0dee9;}.cls-5{fill:#eaeaf4;}.cls-6{fill:#2a313f;}.cls-7{fill:#9052fe;}.cls-8{opacity:0.4;isolation:isolate;}.cls-9{clip-path:url(#clip-path-3);}.cls-10{fill:#d6d8e5;}.cls-11{clip-path:url(#clip-path-4);}.cls-12{fill:#3a2c6d;}.cls-13{fill:#ffcea9;}.cls-14{fill:#f4f4f4;}.cls-15{fill:#38226d;}.cls-16{fill:#9c73ff;}.cls-17{fill:#ff7b7b;}.cls-18{fill:#ededed;}.cls-19{fill:#8c50ff;}.cls-20{fill:#bfbfbf;}.cls-21{fill:#e5e5e5;}.cls-22{clip-path:url(#clip-path-5);}</style> 64 | <clipPath 65 | id="clip-path" 66 | transform="translate(-28.33 0)"> 67 | <rect 68 | class="cls-1" 69 | width="500" 70 | height="500" 71 | id="rect835" 72 | x="0" 73 | y="0" /> 74 | </clipPath> 75 | <clipPath 76 | id="clip-path-3" 77 | transform="translate(-28.33 0)"> 78 | <rect 79 | class="cls-1" 80 | x="375.23999" 81 | y="440.37" 82 | width="36.549999" 83 | height="22.549999" 84 | id="rect838" /> 85 | </clipPath> 86 | <clipPath 87 | id="clip-path-4" 88 | transform="translate(-28.33 0)"> 89 | <rect 90 | class="cls-1" 91 | x="273.67001" 92 | y="353.35001" 93 | width="39.59" 94 | height="26.34" 95 | id="rect841" /> 96 | </clipPath> 97 | <clipPath 98 | id="clip-path-5" 99 | transform="translate(-28.33 0)"> 100 | <rect 101 | class="cls-1" 102 | x="321.47" 103 | y="473.66" 104 | width="39.59" 105 | height="26.34" 106 | id="rect844" /> 107 | </clipPath> 108 | </defs> 109 | <title 110 | id="title849">Cloud_database_SVG</title> 111 | <path 112 | class="cls-5" 113 | d="m 199.73301,51.578526 0.37501,-0.16811 c 0.49137,-0.2069 0.98275,-0.38793 1.47413,-0.55603 h 0.0905 c 0.45259,-0.15518 0.90517,-0.28449 1.29311,-0.4138 h 0.28448 a 19.396569,19.396569 0 0 1 2.14655,-0.46551 h 0.15518 c 0.55603,-0.0776 1.11207,-0.12931 1.65517,-0.15518 h 0.32328 c 0.51724,0 1.02155,0 1.52586,0 h 0.32328 c 0.38793,0 0.76293,0.0776 1.13793,0.12931 h 0.34914 c 0.43965,0.0776 0.87931,0.18104 1.2931,0.29742 h 0.15517 a 10.538802,10.538802 0 0 1 1.07328,0.375 l 0.34914,0.15517 0.94396,0.45259 0.32328,0.1681 v 0 l -44.96125,-26.13364 -0.14224,-0.0776 -0.18103,-0.0905 -0.94397,-0.45258 -0.18103,-0.0905 -0.15518,-0.0647 c -0.36207,-0.14224 -0.72413,-0.27155 -1.0862,-0.375 h -0.12931 v 0 c -0.42673,-0.11638 -0.85345,-0.21982 -1.29311,-0.29741 h -0.12931 -0.21983 c -0.375,0 -0.75,-0.10345 -1.13793,-0.12931 h -0.21983 -0.10345 c -0.50431,0 -1.00862,0 -1.52586,0 h -0.11638 -0.2069 c -0.5431,0 -1.0862,0.0776 -1.65517,0.15517 h -0.12931 a 19.396569,19.396569 0 0 0 -2.14655,0.46552 h -0.16811 -0.11638 c -0.45258,0.12931 -0.90517,0.25862 -1.2931,0.41379 h -0.0905 c -0.49138,0.16811 -0.98276,0.34914 -1.47414,0.55604 h -0.11638 l -0.25862,0.12931 c -0.45258,0.19396 -0.9181,0.40086 -1.38362,0.63362 -0.15517,0.0776 -0.32328,0.14224 -0.47845,0.23276 -0.5819,0.28448 -1.16379,0.59483 -1.74569,0.94396 l 45.03883,26.17244 c 0.5819,-0.34914 1.1638,-0.65948 1.74569,-0.94396 l 0.47845,-0.23276 c 0.46552,-0.23276 0.91811,-0.43966 1.38363,-0.63362" 114 | id="path1059" 115 | style="stroke-width:1.2931" /> 116 | <path 117 | class="cls-5" 118 | d="m 173.41834,41.906096 c 0.75,-0.3362 1.5,-0.64655 2.22414,-0.90517 h 0.10344 a 18.439671,18.439671 0 0 1 1.9138,-0.53017 q 0.5431,-0.14224 1.08621,-0.23276 a 12.788804,12.788804 0 0 1 1.2931,-0.2069 l 0.77586,-0.0776 q 0.85345,0 1.66811,0 h 0.375 a 15.168117,15.168117 0 0 1 1.91379,0.24568 l 0.46552,0.10345 a 13.745702,13.745702 0 0 1 1.42242,0.4138 l 0.43965,0.15517 a 11.340527,11.340527 0 0 1 1.69397,0.81466 l -44.90952,-26.25003 -0.14224,-0.0776 a 13.47415,13.47415 0 0 0 -1.29311,-0.63363 l -0.25862,-0.0905 -0.43965,-0.15518 -0.67242,-0.24569 -0.76293,-0.15517 -0.46552,-0.10345 h -0.24569 a 15.20691,15.20691 0 0 0 -1.56466,-0.19396 h -0.47844 c -0.40087,0 -0.81466,0 -1.29311,0 -0.14224,0 -0.28448,0 -0.43965,0 l -0.77587,0.0905 c -0.23276,0 -0.46551,0 -0.7112,0 l -0.64656,0.14224 q -0.5431,0.0905 -1.0862,0.23276 l -0.56897,0.11638 c -0.43966,0.11637 -0.89224,0.25862 -1.2931,0.41379 h -0.10345 c -0.50431,0.18103 -1.02156,0.375 -1.55173,0.59483 l -0.67241,0.29741 -0.63362,0.29741 c -0.7888,0.36207 -1.57759,0.76294 -2.37932,1.22845 -0.80172,0.46552 -1.93965,1.18966 -2.89655,1.87501 l 45.03883,26.17243 c 0.94397,-0.67241 1.9138,-1.2931 2.89656,-1.875 0.98276,-0.5819 1.59051,-0.86638 2.37931,-1.22845 l 0.63362,-0.28448" 119 | id="path1061" 120 | style="stroke-width:1.2931" /> 121 | <path 122 | class="cls-5" 123 | d="m 130.8752,29.143156 0.68534,-0.29741 c 0.67242,-0.27156 1.29311,-0.53018 2.00431,-0.76294 l 0.375,-0.14224 c 0.7888,-0.25862 1.56466,-0.50431 2.34052,-0.7112 0.43966,-0.12931 0.89224,-0.23276 1.29311,-0.33621 l 0.65948,-0.15517 c 0.63362,-0.14225 1.29311,-0.24569 1.875,-0.34914 h 0.10345 c 0.62069,-0.0905 1.29311,-0.15517 1.83621,-0.21983 h 0.5819 1.07327 a 23.379331,23.379331 0 0 1 2.52156,0 h 0.43965 c 0.65949,0 1.29311,0.12931 1.96552,0.23276 l 0.60776,0.10345 a 19.668121,19.668121 0 0 1 2.19828,0.50431 h 0.0905 a 19.875018,19.875018 0 0 1 1.97845,0.69827 l 0.55603,0.23276 c 0.55604,0.24569 1.08621,0.50431 1.61638,0.7888 l 0.51724,0.27155 v 0 l -45.0259,-26.2112301 -0.23276,-0.12931 -0.28448,-0.14224 c -0.51724,-0.28449 -1.06035,-0.54311 -1.61638,-0.7888 l -0.27155,-0.14224 -0.25862,-0.0905 a 19.875018,19.875018 0 0 0 -1.97845,-0.69826999 h -0.0647 v 0 a 19.668121,19.668121 0 0 0 -2.22414,-0.53017 h -0.25862 -0.34914 c -0.64655,-0.10345 -1.2931,-0.18104 -1.96552,-0.23276 a 2.7543128,2.7543128 0 0 1 -0.32327,0 h -0.10345 a 23.637952,23.637952 0 0 0 -2.58621,0 h -0.23276 -0.82759 -0.58189 c -0.59483,0.0647 -1.21552,0.12931 -1.82328,0.21982 v 0 h -0.0905 c -0.62069,0.0905 -1.2931,0.2069 -1.875,0.34914 -0.23276,0 -0.45259,0.10345 -0.67241,0.15517 -0.21983,0.0517 -0.80173,0.18104 -1.20259,0.29741999 h -0.12931 c -0.77587,0.2069 -1.55173,0.45259 -2.34052,0.71121 l -0.375,0.14224 c -0.65949,0.23276 -1.29311,0.49138 -2.00431,0.76293 l -0.21983,0.0776 c -0.15517,0 -0.31035,0.15517 -0.46552,0.20689 -0.71121,0.31035 -1.42241,0.63362 -2.14655,0.98276 l -0.7888,0.38793 c -0.93103,0.46552 -1.86207,0.9569 -2.7931,1.5 -1.29311,0.73707 -2.58621,1.5388 -3.80173,2.37932 -0.43966,0.29741 -0.85345,0.63362 -1.2931,0.94396 -0.80173,0.56897 -1.61639,1.1379401 -2.40518,1.7456901 -0.50431,0.38794 -0.98276,0.81466 -1.47414,1.29311 -0.71121,0.56897 -1.42241,1.13793 -2.12069,1.74569 -0.69828,0.60776 -1.00862,0.94397 -1.52586,1.42242 -0.51725,0.47844 -1.29311,1.18965 -1.95259,1.82327 l -0.0905,0.0776 q -1.44828,1.46121 -2.87069,2.98707 l -0.77586,0.86638 c -0.36207,0.40086 -0.72414,0.78879 -1.08621,1.20259 -0.36207,0.41379 -0.46552,0.56896 -0.69828,0.85345 -0.67241,0.80172 -1.2931,1.60345 -1.99138,2.4181 -0.25862,0.33621 -0.5431,0.64656 -0.81466,0.99569 -0.27155,0.34914 -0.375,0.51725 -0.56896,0.77587 -0.49138,0.63362 -0.9569,1.2931 -1.43535,1.93965 -0.29741,0.4138 -0.60776,0.80173 -0.89224,1.21552 -0.28448,0.4138 -0.38793,0.59483 -0.59483,0.87931 -0.38793,0.56897 -0.77586,1.15087 -1.15086,1.73276 -0.375,0.5819 -0.5431,0.77587 -0.80173,1.17673 l -0.69827,1.15086 -0.98276,1.60345 -0.56897,0.94397 c -0.32327,0.5431 -0.60776,1.0862 -0.9181,1.62931 l -0.81466,1.47414 -0.32327,0.5819 c -0.43966,0.82758 -0.85345,1.6681 -1.29311,2.50862 l -0.50431,1.00862 -0.10345,0.2069 c -0.60776,1.2931 -1.17672,2.50862 -1.73276,3.77586 l -0.0776,0.19397 c -0.15518,0.34914 -0.28449,0.71121 -0.43966,1.07327 -0.36207,0.87932 -0.73707,1.7457 -1.07328,2.58621 -0.0776,0.18104 -0.12931,0.34914 -0.19396,0.53018 -0.21983,0.56896 -0.4138,1.15086 -0.62069,1.71983 -0.2069,0.56896 -0.46552,1.2931 -0.67242,1.86207 -0.0776,0.25862 -0.14224,0.50431 -0.21982,0.75 -0.23276,0.7112 -0.43966,1.42241 -0.65949,2.13362 -0.21983,0.71121 -0.3362,1.04741 -0.47845,1.57759 -0.0647,0.24569 -0.11638,0.49138 -0.18103,0.75 -0.24569,0.93103 -0.46552,1.84914 -0.68535,2.78017 -0.11638,0.49138 -0.25862,0.98276 -0.36207,1.46121 -0.10344,0.47845 -0.1681,0.80173 -0.24569,1.20259 -0.45258,0.25862 -0.9181,0.49138 -1.38362,0.76293 -0.98276,0.56897 -1.93965,1.18966 -2.89655,1.82328 l -0.49138,0.32327 c -0.91811,0.62069 -1.82328,1.29311 -2.71552,1.96552 l -0.51724,0.4138 c -0.90518,0.7112 -1.79742,1.43534 -2.67673,2.2112 l -0.23276,0.2069 c -0.85345,0.75 -1.68103,1.5388 -2.50862,2.34052 l -0.25862,0.23276 -0.15517,0.1681 c -0.63363,0.62069 -1.29311,1.29311 -1.87501,1.9138 l -0.0905,0.10345 c -0.5819,0.63362 -1.1638,1.2931 -1.73276,1.91379 l -0.15518,0.18104 -0.20689,0.24569 c -0.91811,1.07327 -1.82328,2.17241 -2.70259,3.29741 l -0.0776,0.0905 -0.19396,0.27155 c -0.69828,0.90517 -1.38363,1.83621 -2.04311,2.76725 l -0.2069,0.28448 a 0.53017288,0.53017288 0 0 0 -0.0905,0.14224 c -0.64656,0.90517 -1.29311,1.83621 -1.86207,2.76724 l -0.18104,0.27156 a 4.3577625,4.3577625 0 0 0 -0.24569,0.41379 q -0.73707,1.15086 -1.43534,2.32759 a 3.375003,3.375003 0 0 1 -0.21983,0.36207 l -0.0776,0.14224 c -0.53017,0.90517 -1.03448,1.82328 -1.52586,2.74138 l -0.11638,0.21983 -0.23276,0.45258 q -0.5819,1.099144 -1.125,2.211214 l -0.23276,0.46552 a 2.5862092,2.5862092 0 0 1 -0.12931,0.27155 c -0.43966,0.94397 -0.87931,1.875 -1.2931,2.8319 v 0.0776 l -0.11638,0.31034 c -0.375,0.85345 -0.72414,1.7069 -1.06035,2.58621 l -0.1681,0.42673 c 0,0.10345 -0.0776,0.23276 -0.12931,0.34914 -0.38794,1.00862 -0.75,2.03017 -1.09914,3.05172 v 0 l -0.0647,0.18104 c -0.34914,1.06034 -0.65948,2.12069 -0.96983,3.1681 a 3.6206929,3.6206929 0 0 1 -0.11638,0.40087 1.7586222,1.7586222 0 0 1 0,0.21982 c -0.3362,1.21552 -0.63362,2.44397 -0.90517,3.67242 l -0.0776,0.32328 c 0,0.1681 -0.0647,0.32327 -0.0905,0.47844 -0.10345,0.47845 -0.19397,0.96983 -0.27155,1.44828 -0.0776,0.47845 -0.12932,0.65949 -0.18104,0.99569 -0.0517,0.33621 -0.15517,0.98276 -0.23276,1.48707 0,0.23276 -0.0647,0.46552 -0.10345,0.69828 a 0.91810426,0.91810426 0 0 1 0,0.18103 c -0.12931,1.04742 -0.23276,2.10776 -0.31034,3.14225 v 0.67241 c 0,1.07328 -0.10345,2.14656 -0.10345,3.19397 0,7.97846 1.96552,14.3276 5.31466,18.76295 a 18.607775,18.607775 0 0 0 5.40518,4.83621 l 45.03883,26.15951 c -6.59483,-3.87932 -10.69398,-11.98708 -10.71984,-23.58623 0,-1.04742 0,-2.12069 0.10345,-3.19397 v -0.65948 c 0.0776,-1.04742 0.18104,-2.10776 0.31035,-3.15518 l 0.11638,-0.87931 c 0.0776,-0.50431 0.14224,-0.99569 0.23275,-1.48707 0.0905,-0.49138 0.11638,-0.65948 0.16811,-0.99569 0.0517,-0.33621 0.18103,-0.96983 0.28448,-1.44828 0,-0.27155 0.11638,-0.53017 0.1681,-0.80172 0.27156,-1.21552 0.56897,-2.44397 0.90518,-3.67242 0,-0.2069 0.11638,-0.41379 0.1681,-0.62069 0.31035,-1.06035 0.62069,-2.12069 0.96983,-3.16811 l 0.0776,-0.23276 c 0.3362,-1.03448 0.7112,-2.0431 1.09914,-3.06465 0.0905,-0.25862 0.19396,-0.51725 0.29741,-0.77587 0.33621,-0.85345 0.68534,-1.71983 1.04741,-2.58621 0.0647,-0.12931 0.11638,-0.25862 0.16811,-0.38793 0.41379,-0.95689 0.85345,-1.88793 1.2931,-2.8319 l 0.36207,-0.73707 q 0.54311,-1.09913 1.125,-2.2112 l 0.34914,-0.67242 c 0.49138,-0.9181 0.99569,-1.83621 1.52586,-2.72845 l 0.31035,-0.51724 c 0.45259,-0.78879 0.93103,-1.56466 1.42241,-2.34052 l 0.42673,-0.67241 c 0.59483,-0.93104 1.21552,-1.86208 1.86207,-2.78018 l 0.28448,-0.40086 c 0.67242,-0.94397 1.35776,-1.875 2.05604,-2.78018 l 0.27155,-0.36207 c 0.87931,-1.125 1.78448,-2.22414 2.70259,-3.29741 l 0.36207,-0.42673 c 0.56896,-0.64655 1.15086,-1.2931 1.73276,-1.90086 l 0.0905,-0.11638 c 0.62069,-0.64655 1.2931,-1.29311 1.875,-1.9138 l 0.41379,-0.40086 c 0.82759,-0.80172 1.66811,-1.59052 2.50862,-2.34052 l 0.23276,-0.20689 q 1.29311,-1.15087 2.67673,-2.211214 l 0.51724,-0.4138 c 0.89224,-0.68534 1.79742,-1.2931 2.71552,-1.96552 l 0.47845,-0.32327 c 0.9569,-0.63362 1.92673,-1.29311 2.89655,-1.81035 l 1.39656,-0.77586 c 0.18103,-0.87931 0.40086,-1.77155 0.60776,-2.6638 0.20689,-0.89224 0.43965,-1.84914 0.68534,-2.78017 0.24569,-0.93104 0.42673,-1.55173 0.65948,-2.32759 0.23276,-0.77586 0.42673,-1.42241 0.65949,-2.13362 0.23276,-0.71121 0.5819,-1.73276 0.89224,-2.58621 0.2069,-0.5819 0.40086,-1.16379 0.62069,-1.73276 0.40086,-1.06035 0.82759,-2.10776 1.2931,-3.15518 0.15518,-0.36207 0.28449,-0.73707 0.43966,-1.0862 0.60776,-1.40949 1.29311,-2.79311 1.9138,-4.17673 l 0.51724,-1.00862 c 0.50431,-1.03449 1.03448,-2.06897 1.59052,-3.09052 0.27155,-0.49138 0.5431,-0.98276 0.82758,-1.47414 q 0.72414,-1.29311 1.47414,-2.58621 c 0.32328,-0.54311 0.64655,-1.08621 0.98276,-1.61638 0.49138,-0.7888 0.98276,-1.56466 1.5,-2.32759 0.51724,-0.76293 0.76293,-1.16379 1.15087,-1.73276 0.38793,-0.56897 0.85344,-1.3319 1.38362,-1.97845 0.53017,-0.64655 0.94396,-1.29311 1.43534,-1.95259 0.49138,-0.65948 0.91811,-1.17672 1.38363,-1.75862 0.65948,-0.81466 1.2931,-1.61638 1.99138,-2.41811 0.69827,-0.80172 1.17672,-1.38362 1.78448,-2.05603 0.25862,-0.29742 0.51724,-0.5819 0.78879,-0.86638 q 1.43535,-1.57759 2.94828,-3.06466 c 0.64656,-0.63362 1.29311,-1.29311 1.97845,-1.84914 0.68535,-0.55604 0.98276,-0.94397 1.5,-1.38362 0.51725,-0.43966 1.42242,-1.18966 2.13363,-1.78449 0.49138,-0.40086 0.96983,-0.81465 1.46121,-1.20258 0.78879,-0.60776 1.60345,-1.17673 2.4181,-1.75863 l 1.29311,-0.93103 c 1.2931,-0.84052 2.5862,-1.64224 3.80172,-2.37931 0.93104,-0.54311 1.86207,-1.03449 2.79311,-1.50001 l 0.78879,-0.38793 c 0.72414,-0.34913 1.43535,-0.67241 2.14656,-0.96983" 124 | id="path1063" 125 | style="stroke-width:1.2931" /> 126 | <path 127 | class="cls-3" 128 | d="m 125.14674,32.000916 c 20.3664,-11.75432 37.50004,-5.59914 42.41383,13.29312 a 33.426754,33.426754 0 0 1 2.89656,-1.875 c 12.84053,-7.40949 23.50864,-2.84483 25.66812,10.00862 14.79312,-8.56035 26.43106,-1.84913 26.43106,14.61209 0,15.28449 -9.91811,33.620724 -22.81036,42.866414 -1.06035,0.75 -2.06897,1.42242 -3.10346,2.01725 a 30.750027,30.750027 0 0 1 -2.97414,1.51293 l -111.90527,64.8104 c -20.31467,11.72846 -36.82762,2.32759 -36.89227,-21.02588 -0.0647,-23.35347 16.33191,-51.72418 36.64658,-63.478504 0.47845,-0.28449 0.93104,-0.51725 1.39656,-0.77587 4.81035,-24.40088 21.85346,-50.22418 42.18107,-61.96557" 129 | id="path1065" 130 | style="stroke-width:1.2931" /> 131 | </svg> 132 | ``` -------------------------------------------------------------------------------- /docs/static/img/feature_3.svg: -------------------------------------------------------------------------------- ``` 1 | <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 | <!-- 3 | ~ Copyright (c) 2023-2024 Datalayer, Inc. 4 | ~ 5 | ~ BSD 3-Clause License 6 | --> 7 | 8 | <svg 9 | xmlns:dc="http://purl.org/dc/elements/1.1/" 10 | xmlns:cc="http://creativecommons.org/ns#" 11 | xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" 12 | xmlns:svg="http://www.w3.org/2000/svg" 13 | xmlns="http://www.w3.org/2000/svg" 14 | xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" 15 | xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" 16 | viewBox="0 0 302.65826 398.12268" 17 | version="1.1" 18 | id="svg1054" 19 | sodipodi:docname="6.svg" 20 | inkscape:version="1.0.1 (c497b03c, 2020-09-10)" 21 | width="302.65826" 22 | height="398.12268"> 23 | <metadata 24 | id="metadata1058"> 25 | <rdf:RDF> 26 | <cc:Work 27 | rdf:about=""> 28 | <dc:format>image/svg+xml</dc:format> 29 | <dc:type 30 | rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> 31 | <dc:title>Web_SVG</dc:title> 32 | </cc:Work> 33 | </rdf:RDF> 34 | </metadata> 35 | <sodipodi:namedview 36 | pagecolor="#ffffff" 37 | bordercolor="#666666" 38 | borderopacity="1" 39 | objecttolerance="10" 40 | gridtolerance="10" 41 | guidetolerance="10" 42 | inkscape:pageopacity="0" 43 | inkscape:pageshadow="2" 44 | inkscape:window-width="1256" 45 | inkscape:window-height="607" 46 | id="namedview1056" 47 | showgrid="false" 48 | inkscape:zoom="0.83484846" 49 | inkscape:cx="105.00162" 50 | inkscape:cy="149.95736" 51 | inkscape:window-x="0" 52 | inkscape:window-y="25" 53 | inkscape:window-maximized="0" 54 | inkscape:current-layer="svg1054" 55 | inkscape:document-rotation="0" 56 | fit-margin-top="0" 57 | fit-margin-left="0" 58 | fit-margin-right="0" 59 | fit-margin-bottom="0" /> 60 | <defs 61 | id="defs843"> 62 | <style 63 | id="style833">.cls-1{fill:none;}.cls-2,.cls-9{fill:#d6d8e5;}.cls-2{opacity:0.4;isolation:isolate;}.cls-3{fill:#d5d6e0;}.cls-4{fill:#e9eaf4;}.cls-5{fill:url(#Безымянный_градиент_15);}.cls-6{clip-path:url(#clip-path);}.cls-7{fill:#8c50ff;}.cls-8{opacity:0.05;}.cls-9{opacity:0.4;}.cls-10{fill:#c5c7d3;}.cls-11{fill:#38226d;}.cls-12{fill:#9c73ff;}.cls-13{fill:#ffcea9;}.cls-14{fill:#ededed;}.cls-15{fill:#e5e5e5;}.cls-16{fill:#f4f4f4;}.cls-17{fill:#bfbfbf;}.cls-18{fill:#3a2c6d;}.cls-19{fill:#dceeff;}.cls-20{fill:#dbdbdb;}.cls-21{fill:#1e3779;}.cls-22{fill:#031f60;}</style> 64 | <linearGradient 65 | id="Безымянный_градиент_15" 66 | x1="235.62" 67 | y1="356.16" 68 | x2="235.62" 69 | y2="256.92999" 70 | gradientUnits="userSpaceOnUse"> 71 | <stop 72 | offset="0" 73 | stop-color="#e9eaf4" 74 | id="stop835" /> 75 | <stop 76 | offset="0.63" 77 | stop-color="#e9eaf4" 78 | stop-opacity="0" 79 | id="stop837" /> 80 | </linearGradient> 81 | <clipPath 82 | id="clip-path"> 83 | <circle 84 | class="cls-1" 85 | cx="233.41" 86 | cy="151.32001" 87 | r="151.32001" 88 | id="circle840" /> 89 | </clipPath> 90 | </defs> 91 | <title 92 | id="title845">Web_SVG</title> 93 | <path 94 | class="cls-2" 95 | d="m 146.09827,397.27001 -47.08,-27.18 c -2.19,-1.27 -1.92,-3.48 0.62,-4.94 l 45.84,-26.47 c 2.54,-1.46 6.37,-1.62 8.56,-0.36 l 47.07,27.18 c 2.2,1.27 1.92,3.48 -0.61,4.94 l -45.85,26.47 c -2.53,1.47 -6.36,1.62 -8.55,0.36 z" 96 | id="path847" /> 97 | <path 98 | class="cls-3" 99 | d="m 205.92827,362.37001 -2.32,0.71 -46.23,-26.7 c -2.19,-1.26 -6,-1.1 -8.55,0.36 l -45.85,26.47 -0.11,0.07 -2,-0.61 v 3 c 0,0.11 0,0.22 0,0.34 v 0.24 0 a 2.68,2.68 0 0 0 1.44,1.86 l 47.08,27.18 c 2.19,1.26 6,1.11 8.56,-0.36 l 45.98,-26.43 a 3.62,3.62 0 0 0 2.08,-2.64 v 0 z" 100 | id="path851" /> 101 | <path 102 | class="cls-4" 103 | d="m 149.47827,392.14001 -47.08,-27.14 c -2.19,-1.26 -1.91,-3.47 0.62,-4.93 l 45.85,-26.47 c 2.53,-1.47 6.36,-1.63 8.55,-0.36 l 47.08,27.18 c 2.19,1.27 1.92,3.48 -0.62,4.94 l -45.84,26.47 c -2.54,1.42 -6.37,1.58 -8.56,0.31 z" 104 | id="path853" /> 105 | <path 106 | class="cls-3" 107 | d="m 173.30827,360.25001 v -4 l -5.7,1.5 -12.68,-7.35 a 3.58,3.58 0 0 0 -3.24,0.14 l -12.33,7.12 -5.83,-1.54 v 4.31 0 1 a 0.48,0.48 0 0 0 0,0.12 v 0.1 0 a 1,1 0 0 0 0.55,0.7 l 17.85,10.39 a 3.61,3.61 0 0 0 3.24,-0.13 l 17.38,-10 a 1.36,1.36 0 0 0 0.78,-1 v 0 -1.32 z" 108 | id="path855" /> 109 | <path 110 | class="cls-4" 111 | d="m 151.92827,367.33001 -17.87,-10.33 c -0.83,-0.48 -0.73,-1.32 0.23,-1.87 l 17.38,-10 a 3.58,3.58 0 0 1 3.26,-0.13 l 17.84,10.3 c 0.83,0.48 0.73,1.32 -0.23,1.88 l -17.38,10 a 3.61,3.61 0 0 1 -3.23,0.15 z" 112 | id="path857" /> 113 | <polygon 114 | class="cls-5" 115 | points="129.08,260.92 342.17,256.93 235.48,356.16 " 116 | id="polygon859" 117 | style="fill:url(#%D0%91%D0%B5%D0%B7%D1%8B%D0%BC%D1%8F%D0%BD%D0%BD%D1%8B%D0%B9_%D0%B3%D1%80%D0%B0%D0%B4%D0%B8%D0%B5%D0%BD%D1%82_15)" 118 | transform="translate(-82.071732)" /> 119 | <circle 120 | class="cls-4" 121 | cx="151.33827" 122 | cy="151.32001" 123 | r="151.32001" 124 | id="circle861" /> 125 | <g 126 | class="cls-6" 127 | clip-path="url(#clip-path)" 128 | id="g865" 129 | transform="translate(-82.071732)"> 130 | <path 131 | class="cls-7" 132 | d="m 193.61,61.31 0.51,-1.79 v -3.88 a 9.1,9.1 0 0 1 -2.17,0 c -0.38,-0.17 -0.12,-1.23 -0.12,-1.23 l 1.78,-1.53 -3.57,0.76 -1.28,2 -4,2.47 1.79,1 1.15,0.76 1.72,0.26 V 59 c 0,0 2,-0.51 2.49,0.25 l -0.25,1.41 z m -44.22,55.25 a 4,4 0 0 0 -3.28,0 2.7,2.7 0 0 1 -2.56,2.73 l 2.92,1.63 2.92,-1.09 3.82,-1.27 3.27,2.36 c 2.46,-3 0,-4.73 0,-4.73 z m 62.1,55.63 -2.83,-0.6 -4.92,-3.13 h -4.32 l -3.73,-1.93 h -1.49 c 0,0 0.3,-2.09 -0.15,-2.24 -0.45,-0.15 -3.72,-2.09 -3.72,-2.09 l -2.54,0.9 v -1.79 L 186,160.56 h -1.49 l 0.15,-1.49 c 0,0 1.34,-2.65 0.6,-3.11 -0.74,-0.46 -3.43,-6.43 -3.43,-6.43 l -4,-2.24 h -4.32 l -3.73,-3.57 -3.72,-3 -2.09,-4.17 -2.24,-1.49 c 0,0 -2.53,1.64 -3.13,1.49 -0.6,-0.15 -4.32,-1.49 -4.32,-1.49 l -3.13,-0.89 -3.91,1.83 v 4 l -1.49,-1 v -1.5 l 1.34,-2.53 0.6,-1.79 -1.34,-0.59 -1.79,2.08 -4.47,1.33 -0.9,1.94 -2.09,1.94 -0.59,1.64 -3.28,-2.68 a 25.86,25.86 0 0 1 -3.43,1.49 22.67,22.67 0 0 1 -3.43,-2.24 l -1.64,-3.13 0.9,-4.17 0.74,-4.33 -2.83,-1.79 h -3.88 l -3,-0.89 1.4,-2.24 1.64,-3.28 1.49,-2.38 c 0,0 0.42,-1.09 0.9,-1.34 1.9,-1 0,-1.94 0,-1.94 0,0 -4,-0.3 -4.62,0 -0.62,0.3 -2.24,1.19 -2.24,1.19 v 3.43 l -3,1.34 -4.62,1 -3.13,-3.14 c -0.45,-0.44 -1.49,-5.36 -1.49,-5.36 l 1.93,-1.79 0.9,-1.49 0.15,-3.58 1.93,-1.64 0.75,-1.94 2.22,-1.62 2.13,-1.95 1.94,-0.59 2.68,0.59 c 0.15,-1 5.07,-0.59 5.07,-0.59 l 1.94,-0.1 -0.15,-1.09 c 0.9,-0.75 3.88,0 3.88,0 l 4,-0.34 c 0.88,0.19 1.62,2 1.62,2 l -1.79,1.64 1.2,3.57 1.19,1.64 -0.45,1.64 2.87,-0.42 0.14,-2.84 0.15,-3.43 -0.44,-3.76 2.68,-5.78 10.88,-7 -0.15,-2.54 0.74,-3.65 1.94,0.67 3.43,-4.07 4.47,-1.48 2.68,-1.82 1.34,-2.69 4.92,-1.49 4.92,-2.53 -2.26,2.41 -0.3,2.09 2.68,-0.6 2.39,-1.34 2.08,-0.59 1.94,-0.6 0.6,-1.34 h -1.34 l -2.09,-0.75 c -1.34,-1 -3,-2.23 -3,-2.23 a 1.66,1.66 0 0 1 0.44,-1.35 l 1.35,-1.21 c 0,0 0.59,-0.72 0.15,-0.72 -0.44,0 -3.73,-0.72 -3.73,-0.72 l -3.58,1.44 H 168 l 2.54,-2.21 h 2.83 l 3.13,-0.75 h 6.5 l 3.58,-0.75 2.55,-1.37 3.13,-0.32 0.9,-3.35 -2.09,-1.79 -3,-1.55 -0.6,-3.52 -1.64,-3.58 h -1.49 l -0.15,1.94 h -2.38 l -0.6,1.19 -0.89,0.6 -1.49,0.3 -1.35,-1.35 1.79,-1.93 -2.08,-1.94 -2.54,-1.34 H 172 l -1.94,0.59 -2.68,3.13 -2.09,1.79 0.6,1.64 -0.76,2.49 0.45,1 -2.54,0.9 -2.83,0.59 -1.19,-0.3 -0.9,1.2 1.64,3.35 -1.64,1.12 h -2.23 l -1.89,-2.43 -0.45,-1.38 1.34,-0.74 -1,-1.79 -3.13,-0.15 -4.17,-2.59 h -1.94 l -0.45,-1.59 -1.34,-0.59 1.94,-2.24 2.83,-1.64 4.92,-1.79 1.79,-0.59 3.87,-0.9 2.6,-1.44 2.38,-2.09 h 3.13 l 3.73,-0.94 h 2.39 l 1,-2.78 h -4.77 l -1.79,1.37 h -3.13 a 5.39,5.39 0 0 0 -2.39,-1.37 v -1.1 L 163,24.6 165.13,23 a 12.52,12.52 0 0 0 1.79,-3.13 l 4.32,1.49 3.27,-0.17 2.25,-0.13 2.83,0.15 1.64,0.6 C 180.78,23 179,22.11 179,22.11 l -5.37,0.59 -2.39,1.3 0.75,-1.64 -2.54,0.15 -3,1.93 2.09,1.05 h 4.92 l 2.68,1 2.24,-0.89 2.38,0.89 2.84,0.9 -3.43,1.07 -3,1.41 -3,0.65 -2.38,0.89 1,2.09 6.26,-0.15 3.95,2.43 h 3.28 l 0.15,-1.34 0.74,-1.34 1.5,-0.3 -1.2,-1.64 -0.44,-1.24 h 1.78 l 1.5,1.09 c 1.19,0.9 3.72,-1.09 3.72,-1.09 l 3.73,-1.41 -4.62,-1.37 -1.49,-1.49 c 0.44,-0.9 -4.92,-2.24 -4.92,-2.24 l -4.18,-2.53 -1,-1.05 H 178 l 2.83,-1.19 7,0.74 3.58,-0.74 8.2,-2.09 5.51,-1.34 4,-1.49 -4.63,-0.72 c 0,0 -3.28,0.9 -3.73,0.9 -0.45,0 -4.32,0.3 -4.77,0.3 h -10.44 l -3.27,0.74 3,0.75 -1.79,1 h -3.13 l -1.79,0.6 2.38,-1.79 -2.53,-0.6 -3.13,0.75 -3.88,2.23 1.79,0.6 2.54,-1 2.08,0.44 -2.23,0.6 -0.9,1.19 1.49,0.45 -4,0.3 -7,-1.19 -2.83,3.42 -5.67,4 1.49,1.19 -1,0.63 -1.34,0.71 -3.88,-0.71 -5.36,1.41 -9.84,2.43 -32.89,31.65 -13.12,17.1 -7.6,45.62 13.41,-4.47 2.09,2.23 3.58,0.6 2.39,1.49 2.53,-1 2.09,2.09 4.62,2.53 4.47,0.6 1.34,4.47 c 0,0 3.13,2.83 3,3.43 -0.13,0.6 1,3.28 1.64,3.28 a 30.41,30.41 0 0 1 4.33,1.94 l 2.53,0.29 1,-2.23 h 2.24 v 2.23 l 0.9,1.2 0.29,3.42 0.9,2.54 -1.49,1.64 -2.39,3 -3,4.15 0.3,4 -0.75,2.68 2.24,0.75 -1.94,2.68 a 32.55,32.55 0 0 1 1,3.43 30.41,30.41 0 0 0 1.64,3.73 l 2.09,1.79 0.9,3 5.07,10.43 3.57,3 5.22,2.68 c 0,0 3.28,3.58 3.43,4 0.15,0.42 1.79,7 1.79,7 l -0.07,3.68 1,3.87 0.3,4.33 0.45,6 1.49,4.77 1.34,6.26 -1.64,2.09 1,3.09 1.2,4.51 2.08,2.24 -0.14,3.58 0.44,3.57 9.25,9.84 1.78,1.94 v -4.62 l 3,-1.73 V 260 l -3,-1.34 v -2.09 l 1.47,-1.34 1.48,-2.39 v -1.94 c 1.23,-0.44 0,-1.79 0,-1.79 h -2.05 l -0.15,-1.93 h 1.94 l 0.8,1.19 1.58,-1.19 -1.19,-2.39 c 0.3,-0.74 2.39,0 2.39,0 l 3.57,-2.09 1.49,-1.78 v -2.39 l -1.7,-1.53 -1.5,-2.39 h 1.79 c 0,0 2.69,1.35 3.58,0 0.89,-1.35 2.24,-3.13 2.24,-3.13 l 1.64,-3.87 c 0,0 3.13,-5.67 3.43,-6.12 0.3,-0.45 0,-6 0,-6 l 4.92,-4 h 3.28 l 2.68,-4.48 2.09,-2.94 v -5 l 0.74,-4.77 -1.17,-4.66 4.17,-5.52 3.13,-3 -1.19,-5.51 z m 33.22,-117.14 1,0.59 1.36,-1.18 H 249 l 1.78,-0.6 0.8,-1.1 -0.38,-1.52 0.76,-0.81 c 0.09,-1.23 -1.86,-2.12 -1.86,-2.12 H 249 l -1.36,1.57 h -1.44 l -0.51,1.78 0.21,1.34 -1.19,0.74 z m 9,-11.27 -1.52,0.68 v 2.29 l 0.17,0.59 a 6.36,6.36 0 0 1 0.17,1.44 l 1.1,0.76 1.1,0.68 0.51,1 c 0,0 -0.17,1 -0.76,1 l -1.36,0.43 0.51,0.93 c 0,0 -1,0.51 -1.1,0.93 l 0.93,0.68 -0.85,0.85 h -1.86 v 1 h 3.9 l 1.61,-1 2.37,0.42 c 0,0 1.95,-0.42 2.37,-0.42 l -0.34,-0.43 0.6,-0.76 0.93,-1.1 -1.19,-0.48 -1.35,-0.42 0.17,-1.44 -1.28,-1 -1.94,-1.06 -0.17,-1.95 0.17,-1.69 h -2 l -0.42,-0.85 1.18,-1.1 z m 11.1,7.29 v -0.64 h 1.36 l 0.76,-0.55 v -2.12 l 1,-0.51 -0.34,-1.69 L 266,45 263.63,45.76 V 47 l -1.44,0.17 -0.34,1.14 1,0.47 -1.61,0.93 -0.43,0.72 1.36,1.23 z m 85.4,51.37 5.6,3 2.89,-1.12 1.92,2.72 5.13,1.13 2.24,1.12 6.25,-16.5 L 372.45,84 344.45,38.18 336.09,32 H 326 l -4.49,0.64 -1.12,2.25 L 318,34 316.87,32 h -2.25 l 1,1 0.81,1.61 h -3.53 v 1.99 l -2.52,-0.37 -1.53,-0.23 -0.86,0.43 0.8,0.49 1.11,0.25 -1,0.37 -1.34,-0.54 -1.67,-0.56 -0.5,-1.11 -1.23,-1 -1.42,-0.19 -1,-1.17 2.18,0.19 1.92,0.86 1.6,0.19 c 0,0 1,-0.25 1.11,-0.07 a 12.12,12.12 0 0 0 2.35,0.5 l 1.3,-1 L 310,32 306.1,30.85 c 0,0 -6.55,-0.9 -6.86,-0.88 a 28.82,28.82 0 0 1 -2.78,-0.85 l -2.28,-0.56 h -5 l -7.6,1.41 -4.2,2.67 -5,4.51 c 0,0 -4.32,0.62 -4.57,0.68 a 20.44,20.44 0 0 0 -2,1.91 l 1,2.17 -0.25,1 0.68,0.55 0.5,1 0.68,1 2.53,-0.37 2.72,-1.54 2.1,1.11 1.61,2.78 0.3,1.67 1.92,0.37 1.3,-1.11 1.36,-0.31 0.74,-2.23 0.12,-1.73 1.86,-0.55 v -1.14 l -0.68,-0.49 -0.87,-0.56 -0.31,-2 2.83,-1.79 c 0,0 1.5,-1.36 1.5,-1.6 0,-0.24 0.74,-1.06 0.74,-1.06 0,0 1.18,0.07 1.55,0.13 0.37,0.06 2.34,0.74 2.34,0.74 l 0.75,0.56 -2.29,1.11 -1.35,1.36 -1,0.68 0.93,2.16 2.41,1.24 2.16,-0.44 3.43,-0.17 c 0,0 3.93,0.79 4.11,0.92 0.18,0.13 -1.13,0.55 -1.13,0.55 l -1.53,0.43 -0.86,0.56 -1.46,-0.74 a 3.78,3.78 0 0 0 -2,-0.68 4.09,4.09 0 0 0 -1.67,0.74 l 0.93,1.42 1.29,0.87 c 0,0 -0.55,1.6 -0.92,1.54 -0.37,-0.06 -1,-1.17 -1,-1.17 l -1.43,-0.56 -0.92,0.56 0.12,1.36 -0.4,1.92 -1.54,1 -1.12,0.87 -1.48,-0.87 -1.91,0.56 -3.34,-0.19 -0.74,0.68 -1.18,-0.49 c 0,0 -2.9,0.12 -3.09,0.18 a 10.62,10.62 0 0 1 -1.91,-0.86 l -0.74,-1.67 0.92,-1.11 -0.43,-0.5 -0.37,-0.92 0.62,-0.75 -0.93,-0.24 -1.54,1.54 -0.07,1.49 c 0,0 0.81,1.42 0.87,1.6 a 10.91,10.91 0 0 1 0.06,1.3 l -1.54,0.62 -1.67,0.49 0.31,0.62 1.73,0.31 -0.56,1 -1,0.06 -0.49,1.24 -0.31,0.68 -1.36,-0.37 0.06,-0.87 0.06,-1.11 -0.55,-0.56 -0.81,0.87 -0.86,1.11 -2,0.37 -0.93,0.74 -0.62,1.36 -1.48,0.68 -1.17,0.74 -1.8,-0.92 -0.24,0.43 0.37,0.92 -0.43,0.5 -2.35,-0.19 -1.3,-0.18 -0.19,1.73 2.29,0.37 1.11,1 c 0,0 0.87,1.05 1.05,1.05 0.18,0 1.43,0.31 1.43,0.31 l -0.5,2.72 -1.11,2.53 -4.64,-0.06 -5.06,-0.12 -1.61,0.61 -0.5,1.42 0.44,1.49 -0.9,2.79 -0.37,1.49 A 7,7 0 0 0 245,79 c 0.13,0.18 0.87,1.48 0.87,1.48 l -0.56,2.1 1.67,0.5 2.78,0.18 1.48,0.81 1.12,0.18 1.85,-0.8 2.78,-0.37 2.6,-2.17 -0.43,-2.65 1.6,-2 3.4,-2.16 1.48,-0.5 -0.18,-2.22 2,-1.18 2,0.68 1.36,-0.74 4.18,-1.28 4.14,3.84 5.68,3.89 c 0,0 1.18,2.35 1.24,2.53 a 7.8,7.8 0 0 1 -0.87,1.61 H 282 L 280.8,81 c 0,0 0.37,0.92 0.61,0.92 0.24,0 2.79,1.18 2.79,1.18 l 0.8,0.62 0.43,-0.13 -0.06,-1.17 0.25,-1.11 0.86,0.12 a 7.77,7.77 0 0 0 0.93,-1.24 17.93,17.93 0 0 1 0.86,-1.6 l -0.68,-1.3 0.38,-0.87 1,0.13 0.12,1.29 h 0.37 l -0.12,-1.6 -1,-0.62 -1.34,-0.89 -0.93,-1.35 -1.61,-0.44 -1.85,-1.3 -0.43,-1.48 -1.73,-0.86 -0.45,-1.36 0.5,-1.42 1.11,-0.19 0.06,1.36 1.11,0.37 0.75,-0.43 1.11,2 2.28,1.29 1.8,1 1.11,0.13 1.85,2 0.44,2.59 3.46,3.59 0.49,2.53 2.47,1.73 0.68,-0.56 -0.37,-1.6 1.36,-0.74 -0.43,-1.12 -1.37,-0.47 -0.56,-1.11 0.25,-1.8 -0.8,-0.92 0.74,0.12 0.55,0.68 0.87,-1.17 1.48,0.06 1.49,0.62 1.11,1.52 1.11,1.73 c 0,0 -0.74,0.43 -0.8,0.62 a 5.76,5.76 0 0 0 0.62,1.36 l 1.42,1.29 1.11,1.56 2.22,-0.06 0.25,1 1.61,0.18 1.54,-0.37 0.44,-1 0.68,0.93 1.79,0.62 1.54,-0.12 1.3,-1.43 1.17,0.5 0.87,0.55 -0.31,1.24 0.77,2.54 -1.12,2.57 v 3.36 H 313 l -2.56,1 -4.15,-2.07 -2.72,0.32 -4.65,-1.68 h -3.68 l -1.12,1 0.64,1.76 -1.44,1.44 -3.69,-2.24 -2.88,0.32 -1.12,-1.92 -4.33,-1.28 -3,-1.45 -2.3,-1.13 2.72,-3.2 -0.8,-2.72 -1.92,-1.13 -11.38,1.29 -4.48,1.76 -2.89,0.16 -3,1.12 -3.68,-0.8 -2.41,2.88 -2.88,1.92 -2.6,5.45 0.68,2.4 -3.36,3.21 -3.36,1.28 -1.6,3 c 0,0 -3.53,7.85 -3.85,8.49 -0.32,0.64 0,4.65 0,4.65 l 1.44,4.64 -3.36,7.85 1.28,3.85 1.76,3.52 4.65,6.57 -0.14,3.3 9.58,7.29 6.42,-2.58 6.18,1.96 7.68,-4.16 c 0,0 2.57,0 3.37,1.44 0.8,1.44 1.28,2.72 4.16,2.72 2.88,0 4.33,3.21 4.33,3.21 l -1.28,3.68 0.64,1.92 -1.28,3 3,6.24 2.24,4.81 2.09,4.65 0.48,5.92 -0.48,3.37 -3.69,6.41 1.12,9.93 1.28,2.24 -0.48,1.6 2.25,2.41 1.44,5.6 -1.28,5.29 1.92,4 c 0,0 1.76,4.33 1.92,4.81 0.16,0.48 1.28,2.08 1.12,2.88 -0.16,0.8 -0.64,2.89 -0.64,2.89 l 0.64,3.2 h 8.17 l 3.85,-1.28 9.45,-9.61 a 33.38,33.38 0 0 0 2.4,-3.21 c 0.16,-0.48 1.44,-6.4 1.44,-6.4 l 6.73,-2.73 v -5.23 l -1.44,-4.17 3.52,-3.36 9.46,-5.45 0.8,-4.17 v -7.2 l -1.77,-5.45 0.48,-3.36 -1.64,-1.45 3.05,-4.48 1.12,-5.93 4.48,-2.72 1,-2.73 8.17,-8.47 2.57,-8.06 2.4,-4.45 v -4.81 l -2.24,-0.8 -2.57,2.08 -3.52,0.32 -3.68,1 -3.05,-1.93 0.32,-2.4 -3.84,-4.32 -3.85,-2.25 -1.76,-5.76 -3.76,-3.58 -0.32,-4.48 -2.73,-4 -3.36,-7.52 -3.53,-3.52 -0.32,-2.89 1.45,0.65 1.6,1.28 0.64,2.4 c 1.28,0 1.76,-2.4 1.76,-2.4 l 1.12,2.4 1.12,3 3.53,5.29 1.92,0.32 2.24,4.65 -0.16,2.08 6.41,6.57 1.12,6.08 1.12,2.73 2.09,2.08 9.61,-2.56 1.76,-1.61 4.16,-1.28 v -1.92 l 4.81,-3.2 2,-3.72 1.16,-1.89 1.61,-2.24 -2.65,-3.65 -3.7,-3.13 -1.18,-3 -1.36,0.32 -1,2.88 -3.68,1 -1.92,-1.28 -0.64,-2.72 -1.28,0.32 -1.93,-3.53 -1.76,-1 -2.24,-3.68 1.6,-1.77 z m -75,-23.58 0.34,-1.53 v -2.2 a 2.47,2.47 0 0 0 -1.23,-0.59 1.34,1.34 0 0 0 -0.89,0.34 v 2.45 a 4.55,4.55 0 0 1 0,1.53 z m 0,-6.61 a 1,1 0 0 0 -1.19,-1.1 l -0.59,1.69 0.59,1.27 h 1.19 z m -43.7,-43.74 h 6 l 3.27,-4.71 h 7.53 l 1.21,-3 4.63,-2.45 c 2.19,-1.91 0,-5.37 0,-5.37 l -9.44,-1.18 -9.38,1.2 -13.64,-2.54 -8.41,3.82 c 0,0 -6.05,3.27 -6.87,3 a 29.17,29.17 0 0 0 -4.91,0 c 0,0 3,2.72 4.36,3 a 9.87,9.87 0 0 0 3,0 c 0,0 0,2.66 0.55,3.27 1.91,2.19 -2,3.82 -2,3.82 v 2.58 c 0,0 0.43,4.87 1.24,4.65 0.81,-0.22 2.14,2.59 2.14,2.59 l 2.51,2.73 h 4.59 l 5.73,-6.82 5.64,-2.45 z m 117.85,163.14 -0.74,-2 -0.85,0.84 -0.74,1.7 -1.69,1 -0.74,2.23 -1.06,2.64 -4.13,1.27 -1.8,3.07 -0.42,5.08 -1.48,2.52 -1.69,1.8 0.63,3 1.17,1.38 -1.17,1.58 1.17,1 h 3.91 l 2.22,-3.28 c 0,0 1.8,-3.81 1.8,-4.13 0,-0.32 0.85,-3.81 0.85,-3.81 l 3.06,-4 V 198 h 2.12 l -0.63,-3.92 z m -213.61,-81.15 -4.64,-1.58 c 0,0 -1.29,0.18 -2.8,0.54 a 16.83,16.83 0 0 0 -3.24,1 l 6.58,2.24 h 2.66 l 4.44,3.82 h 3 c 0,0 2.45,-1.09 1.63,-3 l -3.81,-1.37 z" 133 | id="path863" /> 134 | </g> 135 | <path 136 | class="cls-8" 137 | d="M 166.04827,269.00001 A 151.41,151.41 0 0 1 21.048268,74.340006 151.34,151.34 0 1 0 296.34827,194.65001 a 151.23,151.23 0 0 1 -130.3,74.35 z" 138 | id="path867" /> 139 | <ellipse 140 | class="cls-3" 141 | cx="153.54826" 142 | cy="356.16" 143 | rx="4.0900002" 144 | ry="2.3599999" 145 | id="ellipse1050" /> 146 | </svg> 147 | ```