import asyncio import base64 import binascii import json import logging from pathlib import Path from ..model_download import DownloadError, download_model from ..model_registry import model_root from .audio import ( decode_pcm, json_response, normalize_stems, parse_int, validate_common_options, zip_response, ) from .config import ServerConfig from .errors import APIError from .models import ( catalog_model_card, catalog_model_detail, filter_catalog_models, parse_include_files, parse_local_filter, parse_supported_filter, ) from .state import DEFAULT_ENDPOINT, InferenceParameterError, close_loaded_model, load_model, load_state, model_card from .webui import register_webui_routes try: from fastapi import FastAPI, Request from fastapi.responses import JSONResponse, Response except ImportError as exc: # pragma: no cover - exercised only without optional deps. raise RuntimeError("Install server dependencies with `pip install pymss[server]` or `uv sync --extra server`.") from exc def _error_response(exc): """Implement the error response helper. Args: exc (Any): Exc value. Returns: Any: Computed result.""" return JSONResponse( status_code=exc.status_code, content={ "error": { "message": exc.message, "type": exc.error_type, "param": exc.param, "code": exc.code, } }, ) def _check_auth(request, state): """Check auth. Args: request (Request): Incoming FastAPI request. state (Any): State value. Returns: None: This callable completes for its side effects.""" if not state.config.api_key: return expected = f"Bearer {state.config.api_key}" if request.headers.get("authorization") != expected: raise APIError(401, "invalid_api_key", "Invalid or missing API key.") def _content_type(request): """Implement the content type helper. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" return request.headers.get("content-type", "").split(";", 1)[0].strip().lower() async def _read_body(request, state): """Read body. Args: request (Request): Incoming FastAPI request. state (Any): State value. Returns: Any: Computed result.""" max_request_bytes = state.config.max_request_bytes content_length = request.headers.get("content-length") if content_length is not None: try: declared_length = int(content_length) except ValueError: raise APIError(400, "invalid_request", "Invalid Content-Length header.") if declared_length < 0: raise APIError(400, "invalid_request", "Invalid Content-Length header.") if declared_length > max_request_bytes: raise APIError(413, "request_too_large", "Request body is too large.") chunks = [] total = 0 async for chunk in request.stream(): total += len(chunk) if total > max_request_bytes: raise APIError(413, "request_too_large", "Request body is too large.") chunks.append(chunk) return b"".join(chunks) def _require_request_model(model): """Implement the require request model helper. Args: model (str): Model value. Returns: Any: Computed result.""" if not model: raise APIError(400, "invalid_model", "The 'model' field is required.", param="model") return str(model) def _require_loaded_for_inference(state): """Implement the require loaded for inference helper. Args: state (Any): State value. Returns: Any: Computed result.""" if state.model_loading: raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") loaded = state.loaded if loaded is None: raise APIError(503, "model_not_loaded", "No model is currently loaded.", param="model") return loaded def _require_model_id(loaded, model): """Implement the require model id helper. Args: loaded (LoadedModel): Loaded value. model (str): Model value. Returns: Any: Computed result.""" model = _require_request_model(model) if not loaded.is_model_id(model): raise APIError(404, "model_not_found", f"Model {model!r} is not loaded by this process.", param="model") return model def _validate_download_source(source, endpoint, *, source_required=False): """Validate download source. Args: source (str): Download source name. endpoint (str | None): Optional custom download endpoint. source_required (Any, optional): Source required value. Defaults to False. Returns: None: This callable completes for its side effects.""" if source_required and not source: raise APIError(400, "invalid_download_source", "The 'source' field is required.", param="source") if source is not None and source not in {"modelscope", "huggingface", "hf-mirror"}: raise APIError( 400, "invalid_download_source", "source must be one of: modelscope, huggingface, hf-mirror.", param="source", ) if endpoint is not DEFAULT_ENDPOINT and endpoint is not None and not isinstance(endpoint, str): raise APIError(400, "invalid_download_source", "endpoint must be a string or null.", param="endpoint") def _effective_download_source(state, source, endpoint): """Implement the effective download source helper. Args: state (Any): State value. source (str): Download source name. endpoint (str | None): Optional custom download endpoint. Returns: Any: Computed result.""" effective_source = source or state.config.source effective_endpoint = state.config.endpoint if endpoint is DEFAULT_ENDPOINT else endpoint return effective_source, effective_endpoint def _query_endpoint(query_params): """Implement the query endpoint helper. Args: query_params (Any): Query params value. Returns: Any: Computed result.""" return query_params["endpoint"] if "endpoint" in query_params else DEFAULT_ENDPOINT def _parse_bool_field(value, default, param): """Parse bool field. Args: value (Any): Value value. default (Any): Default value. param (str | None): Param value. Returns: Any: Parsed value.""" if value is None: return default if isinstance(value, bool): return value if isinstance(value, str): lowered = value.strip().lower() if lowered in {"1", "true", "yes", "on"}: return True if lowered in {"0", "false", "no", "off"}: return False raise APIError(400, "invalid_request", f"{param} must be a boolean.", param=param) def _parse_json_request_body(body, loaded, state): """Parse json request body. Args: body (Any): Body value. loaded (LoadedModel): Loaded value. state (Any): State value. Returns: Any: Parsed value.""" try: payload = json.loads(body.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): raise APIError(400, "invalid_request", "Request body must be valid JSON.") if not isinstance(payload, dict): raise APIError(400, "invalid_request", "JSON request body must be an object.") model = _require_model_id(loaded, payload.get("model")) input_data = payload.get("input") if not isinstance(input_data, dict): raise APIError(400, "invalid_request", "The 'input' object is required.", param="input") audio_format = str(input_data.get("format", "")).lower() sample_rate = parse_int(input_data.get("sample_rate"), "input.sample_rate") channels = parse_int(input_data.get("channels"), "input.channels") encoded = input_data.get("data") if not isinstance(encoded, str): raise APIError(400, "invalid_request", "input.data must be a base64 string.", param="input.data") try: raw = base64.b64decode(encoded, validate=True) except binascii.Error: raise APIError(400, "invalid_base64", "input.data must be valid base64.", param="input.data") stems = normalize_stems(payload.get("stems"), loaded.instruments) response_format = str(payload.get("response_format", "json")).lower() output_audio_format = str(payload.get("output_audio_format", "pcm_f32le")).lower() validate_common_options(response_format, output_audio_format) mix, seconds = decode_pcm( raw, audio_format, sample_rate, channels, loaded.sample_rate, state.config.max_audio_seconds, ) return model, mix, stems, response_format, output_audio_format, seconds def _parse_binary_request_body(request, body, loaded, state): """Parse binary request body. Args: request (Request): Incoming FastAPI request. body (Any): Body value. loaded (LoadedModel): Loaded value. state (Any): State value. Returns: Any: Parsed value.""" params = request.query_params model = _require_model_id(loaded, params.get("model")) audio_format = str(params.get("format", "")).lower() if not audio_format or params.get("sample_rate") is None or params.get("channels") is None: raise APIError(400, "missing_audio_metadata", "format, sample_rate, and channels are required.") sample_rate = parse_int(params.get("sample_rate"), "sample_rate", code="invalid_query_parameter") channels = parse_int(params.get("channels"), "channels", code="invalid_query_parameter") stems = normalize_stems(params.get("stems"), loaded.instruments) response_format = str(params.get("response_format", "json")).lower() output_audio_format = str(params.get("output_audio_format", "pcm_f32le")).lower() validate_common_options(response_format, output_audio_format) mix, seconds = decode_pcm( body, audio_format, sample_rate, channels, loaded.sample_rate, state.config.max_audio_seconds, ) return model, mix, stems, response_format, output_audio_format, seconds async def _parse_request(request, state, loaded, body=None): """Parse request. Args: request (Request): Incoming FastAPI request. state (Any): State value. loaded (LoadedModel): Loaded value. body (Any, optional): Body value. Defaults to None. Returns: Any: Parsed value.""" if body is None: body = await _read_body(request, state) content_type = _content_type(request) if content_type == "application/json": return _parse_json_request_body(body, loaded, state) if content_type == "application/octet-stream": return _parse_binary_request_body(request, body, loaded, state) raise APIError(415, "unsupported_content_type", "Content-Type must be application/json or application/octet-stream.") def _run_separation_sync(loaded, mix, stems): """Run separation sync. Args: loaded (LoadedModel): Loaded value. mix (np.ndarray): Mix value. stems (Sequence[str] | None): Requested output stem names. Returns: Any: Computed result.""" if loaded.separator.model_type == "vr": return loaded.separator.separate(mix, pbar=False) return loaded.separator.separate(mix, pbar=False, stems=stems) async def _run_separation(state, loaded, model, mix, stems): """Run separation. Args: state (Any): State value. loaded (LoadedModel): Loaded value. model (str): Model value. mix (np.ndarray): Mix value. stems (Sequence[str] | None): Requested output stem names. Returns: Any: Computed result.""" if state.model_loading: raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") acquired = await state.limiter.acquire() if not acquired: raise APIError(429, "server_overloaded", "Inference queue is full.") try: if state.model_loading: raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") async with state.inference_lock: if state.model_loading: raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") if state.loaded is not loaded: raise APIError(404, "model_not_found", f"Model {model!r} is not loaded by this process.", param="model") task = asyncio.to_thread(_run_separation_sync, loaded, mix, stems) if state.config.request_timeout_seconds: try: return await asyncio.wait_for(task, timeout=state.config.request_timeout_seconds) except asyncio.TimeoutError: raise APIError(504, "separation_timeout", "Separation request timed out.") return await task finally: await state.limiter.release() def _parse_load_payload(payload): """Parse load payload. Args: payload (Any): Payload value. Returns: Any: Parsed value.""" if not isinstance(payload, dict): raise APIError(400, "invalid_request", "JSON request body must be an object.") model = payload.get("model") if not model: raise APIError(400, "invalid_model", "The 'model' field is required.", param="model") inference_params = payload.get("inference_params") if inference_params is None: inference_params = None elif not isinstance(inference_params, dict): raise APIError(400, "invalid_inference_parameter", "inference_params must be an object.", param="inference_params") source = payload.get("source") endpoint = payload["endpoint"] if "endpoint" in payload else DEFAULT_ENDPOINT _validate_download_source(source, endpoint) return str(model), source, endpoint, inference_params async def _load_or_switch_model(state, model, source, endpoint, inference_params): """Load or switch model. Args: state (Any): State value. model (str): Model value. source (str): Download source name. endpoint (str | None): Optional custom download endpoint. inference_params (dict | None): Inference params value. Returns: Any: Computed result.""" async with state.operation_lock: if state.model_lock.locked(): raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") if state.download_lock.locked(): raise APIError(409, "model_download_in_progress", "A model download operation is in progress.") await state.model_lock.acquire() try: previous_loaded = state.loaded is not None old_loaded = state.loaded state.loaded = None state.model_loading = True state.model_loading_target = model effective_source, effective_endpoint = _effective_download_source(state, source, endpoint) if old_loaded is not None: async with state.inference_lock: try: await asyncio.to_thread(close_loaded_model, old_loaded) except Exception as exc: state.logger.exception("Model unload failed") raise APIError(500, "model_unload_failed", str(exc), error_type="server_error") try: loaded = await asyncio.to_thread( load_model, state.config, model, effective_source, effective_endpoint, inference_params, ) except InferenceParameterError as exc: raise APIError(400, "invalid_inference_parameter", str(exc), param="inference_params") except ValueError as exc: raise APIError(400, "invalid_model", str(exc), param="model") except KeyError as exc: raise APIError(404, "model_not_found", str(exc), param="model") except Exception as exc: state.logger.exception("Model load failed") raise APIError(500, "model_load_failed", str(exc), error_type="server_error") state.loaded = loaded return previous_loaded, loaded finally: state.model_loading = False state.model_loading_target = None state.model_lock.release() def _parse_download_payload(payload): """Parse download payload. Args: payload (Any): Payload value. Returns: Any: Parsed value.""" if not isinstance(payload, dict): raise APIError(400, "invalid_request", "JSON request body must be an object.") model = payload.get("model") if not model: raise APIError(400, "invalid_model", "The 'model' field is required.", param="model") source = payload.get("source") endpoint = payload["endpoint"] if "endpoint" in payload else DEFAULT_ENDPOINT _validate_download_source(source, endpoint) force = _parse_bool_field(payload.get("force"), False, "force") verify = _parse_bool_field(payload.get("verify"), True, "verify") timeout = payload.get("timeout_seconds", 30) try: timeout = float(timeout) except (TypeError, ValueError): raise APIError(400, "invalid_request", "timeout_seconds must be a number.", param="timeout_seconds") if timeout <= 0: raise APIError(400, "invalid_request", "timeout_seconds must be greater than 0.", param="timeout_seconds") return str(model), source, endpoint, force, verify, timeout def _download_result_relpaths(paths, model_dir): """Download result relpaths. Args: paths (Any): Paths value. model_dir (str | os.PathLike | None): Local model cache directory. Uses the package default when None. Returns: Any: Computed result.""" root = model_root(model_dir).resolve() relpaths = [] for path in paths: try: relpaths.append(Path(path).resolve().relative_to(root).as_posix()) except (OSError, ValueError): relpaths.append("") return relpaths async def _download_model_to_local(state, model, source, endpoint, force, verify, timeout): """Download model to local. Args: state (Any): State value. model (str): Model value. source (str): Download source name. endpoint (str | None): Optional custom download endpoint. force (bool): Whether to overwrite or redownload existing files. verify (bool): Whether to validate downloads with available metadata. timeout (int): Network timeout in seconds. Returns: Any: Computed result.""" async with state.operation_lock: if state.model_lock.locked(): raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") if state.download_lock.locked(): raise APIError(409, "model_download_in_progress", "A model download operation is in progress.") await state.download_lock.acquire() try: state.model_downloading = True state.model_downloading_target = model effective_source, effective_endpoint = _effective_download_source(state, source, endpoint) try: result = await asyncio.to_thread( download_model, model, model_dir=state.config.model_dir, source=effective_source, endpoint=effective_endpoint, verify=verify, force=force, timeout=timeout, ) except KeyError as exc: raise APIError(404, "model_not_found", str(exc), param="model") except (DownloadError, OSError) as exc: state.logger.exception("Model download failed") raise APIError(500, "model_download_failed", str(exc), error_type="server_error") except Exception as exc: state.logger.exception("Model download failed") raise APIError(500, "model_download_failed", str(exc), error_type="server_error") entry = result["entry"] return { "object": "model.download", "model": catalog_model_card( entry, model_dir=state.config.model_dir, source=effective_source, endpoint=effective_endpoint, include_files=False, ), "source": effective_source, "endpoint": effective_endpoint, "downloaded": _download_result_relpaths(result.get("downloaded", []), state.config.model_dir), "skipped": _download_result_relpaths(result.get("skipped", []), state.config.model_dir), } finally: state.model_downloading = False state.model_downloading_target = None state.download_lock.release() def _download_source_response(state): """Download source response. Args: state (Any): State value. Returns: Any: Computed result.""" return { "object": "download.source", "source": state.config.source, "endpoint": state.config.endpoint, } def _server_info_response(state): """Implement the server info response helper. Args: state (Any): State value. Returns: Any: Computed result.""" return { "object": "server.info", "webui": { "enabled": bool(state.config.webui), "path": "/ui/" if state.config.webui else None, }, "auth": { "api_key_required": bool(state.config.api_key), }, "limits": { "max_audio_seconds": state.config.max_audio_seconds, "max_request_bytes": state.config.max_request_bytes, "max_queue_size": state.config.max_queue_size, "request_timeout_seconds": state.config.request_timeout_seconds, }, "download_source": { "source": state.config.source, "endpoint": state.config.endpoint, }, } async def _update_download_source(state, source, endpoint): """Update download source. Args: state (Any): State value. source (str): Download source name. endpoint (str | None): Optional custom download endpoint. Returns: Any: Computed result.""" _validate_download_source(source, endpoint, source_required=True) async with state.operation_lock: if state.model_lock.locked(): raise APIError(409, "model_operation_in_progress", "A model load or switch operation is in progress.") if state.download_lock.locked(): raise APIError(409, "model_download_in_progress", "A model download operation is in progress.") await state.download_lock.acquire() try: state.config.source = source if endpoint is not DEFAULT_ENDPOINT: state.config.endpoint = endpoint return _download_source_response(state) finally: state.download_lock.release() def create_app(config): """Create the FastAPI application. Args: config (AttrDict | dict): Loaded pymss configuration. Returns: FastAPI: Configured application instance. Example: >>> app = create_app()""" state = load_state(config) app = FastAPI(title="pymss server", version="1") app.state.pymss_state = state if config.webui: register_webui_routes(app) @app.exception_handler(APIError) async def handle_api_error(_request, exc): """Implement the handle api error helper. Args: _request (Any): request value. exc (Any): Exc value. Returns: Any: Computed result.""" return _error_response(exc) @app.get("/health") async def health(): """Implement the health helper. Args: None: This callable does not accept user-provided arguments. Returns: Any: Computed result.""" loaded = state.loaded return { "status": "ok", "model_loaded": loaded is not None, "model_loading": state.model_loading, "model": loaded.model_id if loaded is not None else None, "device": loaded.device if loaded is not None else None, } @app.get("/v1/models") async def list_models(request: Request): """List model catalog entries. Args: request (Request): Incoming FastAPI request. Returns: list[ModelEntry]: Matching catalog entries. Example: >>> models = list_models(supported=True) >>> models[0].name""" _check_auth(request, state) loaded = state.loaded return { "object": "list", "data": [] if loaded is None else [model_card(loaded)], } @app.get("/v1/models/{model}") async def get_model(model: str, request: Request): """Return model. Args: model (str): Model value. request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) loaded = state.loaded if loaded is None or not loaded.is_model_id(model): raise APIError(404, "model_not_found", f"Model {model!r} is not loaded by this process.", param="model") return model_card(loaded) @app.get("/v1/catalog/models") async def list_catalog_models(request: Request): """Implement the list catalog models helper. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) try: supported = parse_supported_filter(request.query_params.get("supported")) local = parse_local_filter(request.query_params.get("local")) include_files = parse_include_files(request.query_params.get("include_files")) except ValueError as exc: raise APIError(400, "invalid_request", str(exc)) source, endpoint = _effective_download_source( state, request.query_params.get("source"), _query_endpoint(request.query_params), ) _validate_download_source(source, endpoint) entries = filter_catalog_models( category=request.query_params.get("category"), supported=supported, local=local, q=request.query_params.get("q"), model_dir=state.config.model_dir, ) return { "object": "list", "data": [ catalog_model_card( entry, model_dir=state.config.model_dir, source=source, endpoint=endpoint, include_files=include_files, ) for entry in entries ], "pymss": { "source": source, "endpoint": endpoint, "total": len(entries), }, } @app.get("/v1/catalog/models/{model}") async def get_catalog_model(model: str, request: Request): """Return catalog model. Args: model (str): Model value. request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) source, endpoint = _effective_download_source( state, request.query_params.get("source"), _query_endpoint(request.query_params), ) _validate_download_source(source, endpoint) try: return catalog_model_detail(model, model_dir=state.config.model_dir, source=source, endpoint=endpoint) except KeyError as exc: raise APIError(404, "model_not_found", str(exc), param="model") @app.post("/v1/models/load") async def load_model_endpoint(request: Request): """Load model endpoint. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) body = await _read_body(request, state) try: payload = json.loads(body.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): raise APIError(400, "invalid_request", "Request body must be valid JSON.") model, source, endpoint, inference_params = _parse_load_payload(payload) previous_loaded, loaded = await _load_or_switch_model(state, model, source, endpoint, inference_params) return { "object": "model.load", "previous_model_loaded": previous_loaded, "model_loaded": True, "model": model_card(loaded), } @app.post("/v1/models/download") async def download_model_endpoint(request: Request): """Download model endpoint. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) body = await _read_body(request, state) try: payload = json.loads(body.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): raise APIError(400, "invalid_request", "Request body must be valid JSON.") model, source, endpoint, force, verify, timeout = _parse_download_payload(payload) return await _download_model_to_local(state, model, source, endpoint, force, verify, timeout) @app.get("/v1/download-source") async def get_download_source(request: Request): """Return download source. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) return _download_source_response(state) @app.post("/v1/download-source") async def update_download_source(request: Request): """Update download source. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) body = await _read_body(request, state) try: payload = json.loads(body.decode("utf-8")) except (UnicodeDecodeError, json.JSONDecodeError): raise APIError(400, "invalid_request", "Request body must be valid JSON.") if not isinstance(payload, dict): raise APIError(400, "invalid_request", "JSON request body must be an object.") endpoint = payload["endpoint"] if "endpoint" in payload else DEFAULT_ENDPOINT return await _update_download_source(state, payload.get("source"), endpoint) @app.get("/v1/server/info") async def get_server_info(request: Request): """Return server info. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) return _server_info_response(state) @app.post("/v1/audio/separations") async def separate_audio(request: Request): """Separate audio. Args: request (Request): Incoming FastAPI request. Returns: Any: Computed result.""" _check_auth(request, state) body = await _read_body(request, state) loaded = _require_loaded_for_inference(state) model, mix, stems, response_format, output_audio_format, input_seconds = await _parse_request( request, state, loaded, body, ) try: results = await _run_separation(state, loaded, model, mix, stems) except APIError: raise except Exception as exc: state.logger.exception("Separation failed") raise APIError(500, "separation_failed", str(exc), error_type="server_error") try: if response_format == "json": return json_response(loaded, model, results, stems, input_seconds) content = zip_response(loaded, model, results, stems, input_seconds, output_audio_format) return Response(content=content, media_type="application/zip") except APIError: raise except Exception as exc: state.logger.exception("Encoding separation response failed") raise APIError(500, "separation_failed", str(exc), error_type="server_error") return app def _server_display_host(host): """Implement the server display host helper. Args: host (str): Host value. Returns: Any: Computed result.""" if host in {"0.0.0.0", "::"}: return "127.0.0.1" if ":" in host and not host.startswith("["): return f"[{host}]" return host def _server_url(config, path="/"): """Implement the server url helper. Args: config (AttrDict | dict): Loaded pymss configuration. path (str | os.PathLike, optional): File system path. Defaults to '/'. Returns: Any: Computed result.""" normalized_path = path if path.startswith("/") else f"/{path}" return f"http://{_server_display_host(config.host)}:{config.port}{normalized_path}" def _log_webui_url(config): """Implement the log webui url helper. Args: config (AttrDict | dict): Loaded pymss configuration. Returns: None: This callable completes for its side effects.""" if not config.webui: return logging.getLogger("uvicorn.error").info("WebUI available at %s", _server_url(config, "/ui/")) def _create_uvicorn_server(uvicorn, app, config): """Create uvicorn server. Args: uvicorn (Any): Uvicorn value. app (FastAPI): App value. config (AttrDict | dict): Loaded pymss configuration. Returns: Any: Computed result.""" class PymssUvicornServer(uvicorn.Server): """Represent PymssUvicornServer.""" def _log_started_message(self, listeners): """Implement the log started message helper. Args: listeners (Any): Listeners value. Returns: None: This callable completes for its side effects.""" super()._log_started_message(listeners) _log_webui_url(config) uvicorn_config = uvicorn.Config(app, host=config.host, port=config.port) return PymssUvicornServer(uvicorn_config) def run_server(config): """Run the pymss HTTP server. Args: config (AttrDict | dict): Loaded pymss configuration. Returns: None: Runs until the server stops. Example: >>> run_server()""" try: import uvicorn except ImportError as exc: # pragma: no cover - exercised only without optional deps. raise RuntimeError("Install server dependencies with `pip install pymss[server]` or `uv sync --extra server`.") from exc app = create_app(config) _create_uvicorn_server(uvicorn, app, config).run()