From 4890ea13af8e817b78f4d1e9e306bd3232bcbce0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:08:17 +0000 Subject: [PATCH 01/14] Initial plan From ebbc65bbbdc464adc985345100c68a3142495aa8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:12:31 +0000 Subject: [PATCH 02/14] Add FastAPI admin service with /services endpoint - Create admin service with Poetry project structure - Add FastAPI application with /services endpoint - Configure nginx to route /services to admin service - Update custom_startup.sh to start admin service - Add comprehensive tests for the service Co-authored-by: prasadtalasila <9206466+prasadtalasila@users.noreply.github.com> --- workspaces/Dockerfile.ubuntu.noble.gnome | 4 +- workspaces/src/admin/README.md | 33 +++++ workspaces/src/admin/pyproject.toml | 25 ++++ workspaces/src/admin/src/admin/__init__.py | 1 + workspaces/src/admin/src/admin/main.py | 78 +++++++++++ .../admin/src/admin/services_template.json | 22 ++++ workspaces/src/admin/tests/__init__.py | 1 + workspaces/src/admin/tests/test_main.py | 123 ++++++++++++++++++ workspaces/src/install/admin/install_admin.sh | 20 +++ workspaces/src/startup/configure_nginx.py | 9 ++ workspaces/src/startup/custom_startup.sh | 12 ++ workspaces/src/startup/nginx.conf | 14 ++ 12 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 workspaces/src/admin/README.md create mode 100644 workspaces/src/admin/pyproject.toml create mode 100644 workspaces/src/admin/src/admin/__init__.py create mode 100644 workspaces/src/admin/src/admin/main.py create mode 100644 workspaces/src/admin/src/admin/services_template.json create mode 100644 workspaces/src/admin/tests/__init__.py create mode 100644 workspaces/src/admin/tests/test_main.py create mode 100755 workspaces/src/install/admin/install_admin.sh diff --git a/workspaces/Dockerfile.ubuntu.noble.gnome b/workspaces/Dockerfile.ubuntu.noble.gnome index 0be1902..78b86b6 100644 --- a/workspaces/Dockerfile.ubuntu.noble.gnome +++ b/workspaces/Dockerfile.ubuntu.noble.gnome @@ -1,7 +1,8 @@ FROM kasmweb/core-ubuntu-noble:1.18.0 AS configure USER root -ENV CODE_SERVER_PORT=8054 \ +ENV ADMIN_SERVER_PORT=8091 \ + CODE_SERVER_PORT=8054 \ HOME=/home/kasm-default-profile \ INST_DIR=${STARTUPDIR}/install \ JUPYTER_SERVER_PORT=8090 \ @@ -23,6 +24,7 @@ RUN bash ${INST_DIR}/firefox/install_firefox.sh && \ bash ${INST_DIR}/nginx/install_nginx.sh && \ bash ${INST_DIR}/vscode/install_vscode_server.sh && \ bash ${INST_DIR}/jupyter/install_jupyter.sh && \ + bash ${INST_DIR}/admin/install_admin.sh && \ bash ${INST_DIR}/dtaas_cleanup.sh COPY ./src/startup/ ${STARTUPDIR} diff --git a/workspaces/src/admin/README.md b/workspaces/src/admin/README.md new file mode 100644 index 0000000..ff3548b --- /dev/null +++ b/workspaces/src/admin/README.md @@ -0,0 +1,33 @@ +# Admin Service + +FastAPI service for workspace service discovery and management. + +## Features + +- `/services` endpoint - Returns JSON list of available workspace services +- Environment-aware configuration (uses MAIN_USER environment variable) + +## Running + +The service is automatically started when the workspace container starts. +It runs on port 8091 and is accessible via the nginx reverse proxy at `/services`. + +## Development + +Install dependencies: + +```bash +poetry install +``` + +Run tests: + +```bash +poetry run pytest +``` + +Run the service locally: + +```bash +poetry run uvicorn admin.main:app --host 0.0.0.0 --port 8091 +``` diff --git a/workspaces/src/admin/pyproject.toml b/workspaces/src/admin/pyproject.toml new file mode 100644 index 0000000..2f4ce59 --- /dev/null +++ b/workspaces/src/admin/pyproject.toml @@ -0,0 +1,25 @@ +[tool.poetry] +name = "admin" +version = "0.1.0" +description = "Admin service for DTaaS workspace service discovery" +authors = ["INTO-CPS Association"] +readme = "README.md" +packages = [{include = "admin", from = "src"}] + +[tool.poetry.dependencies] +python = "^3.12" +fastapi = "^0.115.0" +uvicorn = {extras = ["standard"], version = "^0.32.0"} + +[tool.poetry.group.dev.dependencies] +pytest = "^8.3.0" +httpx = "^0.28.0" +pytest-asyncio = "^0.24.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" + +[tool.pytest.ini_options] +asyncio_mode = "auto" +testpaths = ["tests"] diff --git a/workspaces/src/admin/src/admin/__init__.py b/workspaces/src/admin/src/admin/__init__.py new file mode 100644 index 0000000..b519efb --- /dev/null +++ b/workspaces/src/admin/src/admin/__init__.py @@ -0,0 +1 @@ +"""Admin service package.""" diff --git a/workspaces/src/admin/src/admin/main.py b/workspaces/src/admin/src/admin/main.py new file mode 100644 index 0000000..7ed3b66 --- /dev/null +++ b/workspaces/src/admin/src/admin/main.py @@ -0,0 +1,78 @@ +""" +FastAPI application for workspace service discovery. + +This service provides a /services endpoint that returns a JSON object +containing information about all available services in the workspace. +""" + +import json +import os +from pathlib import Path +from typing import Dict, Any + +from fastapi import FastAPI +from fastapi.responses import JSONResponse + +app = FastAPI( + title="Workspace Admin Service", + description="Service discovery and management for DTaaS workspace", + version="0.1.0" +) + +# Path to services template +SERVICES_TEMPLATE_PATH = Path(__file__).parent / "services_template.json" + + +def load_services() -> Dict[str, Any]: + """ + Load services from template and substitute environment variables. + + Returns: + Dictionary containing service information with environment variables substituted. + """ + # Read the services template + with open(SERVICES_TEMPLATE_PATH, 'r', encoding='utf-8') as f: + services = json.load(f) + + # Get MAIN_USER from environment, default to 'dtaas-user' + main_user = os.getenv('MAIN_USER', 'dtaas-user') + + # Substitute {MAIN_USER} in endpoint values + for service_id, service_info in services.items(): + if 'endpoint' in service_info: + service_info['endpoint'] = service_info['endpoint'].replace( + '{MAIN_USER}', main_user + ) + + return services + + +@app.get("/") +async def root() -> Dict[str, str]: + """Root endpoint providing service information.""" + return { + "service": "Workspace Admin Service", + "version": "0.1.0", + "endpoints": { + "/services": "Get list of available workspace services", + "/health": "Health check endpoint" + } + } + + +@app.get("/services") +async def get_services() -> JSONResponse: + """ + Get list of available workspace services. + + Returns: + JSONResponse containing service information. + """ + services = load_services() + return JSONResponse(content=services) + + +@app.get("/health") +async def health_check() -> Dict[str, str]: + """Health check endpoint.""" + return {"status": "healthy"} diff --git a/workspaces/src/admin/src/admin/services_template.json b/workspaces/src/admin/src/admin/services_template.json new file mode 100644 index 0000000..c076735 --- /dev/null +++ b/workspaces/src/admin/src/admin/services_template.json @@ -0,0 +1,22 @@ +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path={MAIN_USER}%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} diff --git a/workspaces/src/admin/tests/__init__.py b/workspaces/src/admin/tests/__init__.py new file mode 100644 index 0000000..46816dd --- /dev/null +++ b/workspaces/src/admin/tests/__init__.py @@ -0,0 +1 @@ +"""Tests package.""" diff --git a/workspaces/src/admin/tests/test_main.py b/workspaces/src/admin/tests/test_main.py new file mode 100644 index 0000000..ea2ce64 --- /dev/null +++ b/workspaces/src/admin/tests/test_main.py @@ -0,0 +1,123 @@ +""" +Unit tests for the admin service. + +Tests the /services endpoint and service discovery functionality. +""" + +import os +import json +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +from admin.main import app, load_services, SERVICES_TEMPLATE_PATH + + +@pytest.fixture +def client(): + """Create a test client for the FastAPI app.""" + return TestClient(app) + + +@pytest.fixture +def mock_main_user(monkeypatch): + """Set up mock MAIN_USER environment variable.""" + monkeypatch.setenv('MAIN_USER', 'testuser') + + +def test_root_endpoint(client): + """Test the root endpoint returns service information.""" + response = client.get("/") + assert response.status_code == 200 + data = response.json() + assert data["service"] == "Workspace Admin Service" + assert "endpoints" in data + assert "/services" in data["endpoints"] + + +def test_health_check(client): + """Test the health check endpoint.""" + response = client.get("/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + + +def test_services_endpoint(client, mock_main_user): + """Test the /services endpoint returns service list.""" + response = client.get("/services") + assert response.status_code == 200 + + services = response.json() + + # Check that we have the expected services + assert "desktop" in services + assert "vscode" in services + assert "notebook" in services + assert "lab" in services + + # Check desktop service structure + desktop = services["desktop"] + assert "name" in desktop + assert "description" in desktop + assert "endpoint" in desktop + assert desktop["name"] == "Desktop" + + # Check that MAIN_USER is substituted correctly + assert "testuser" in desktop["endpoint"] + assert "{MAIN_USER}" not in desktop["endpoint"] + + +def test_services_endpoint_default_user(client, monkeypatch): + """Test /services endpoint with default MAIN_USER.""" + # Remove MAIN_USER from environment + monkeypatch.delenv('MAIN_USER', raising=False) + + response = client.get("/services") + assert response.status_code == 200 + + services = response.json() + desktop = services["desktop"] + + # Should use default 'dtaas-user' + assert "dtaas-user" in desktop["endpoint"] + + +def test_load_services_substitutes_main_user(mock_main_user): + """Test that load_services correctly substitutes MAIN_USER.""" + services = load_services() + + # Check that {MAIN_USER} is replaced with 'testuser' + desktop_endpoint = services["desktop"]["endpoint"] + assert "testuser" in desktop_endpoint + assert "{MAIN_USER}" not in desktop_endpoint + + +def test_load_services_preserves_structure(): + """Test that load_services preserves the service structure.""" + services = load_services() + + # Check all required services exist + required_services = ["desktop", "vscode", "notebook", "lab"] + for service_id in required_services: + assert service_id in services + assert "name" in services[service_id] + assert "description" in services[service_id] + assert "endpoint" in services[service_id] + + +def test_services_template_file_exists(): + """Test that the services template file exists.""" + assert SERVICES_TEMPLATE_PATH.exists() + assert SERVICES_TEMPLATE_PATH.is_file() + + +def test_services_template_valid_json(): + """Test that the services template is valid JSON.""" + with open(SERVICES_TEMPLATE_PATH, 'r', encoding='utf-8') as f: + services = json.load(f) + + # Should not raise an exception + assert isinstance(services, dict) + assert len(services) > 0 diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh new file mode 100755 index 0000000..b6a6263 --- /dev/null +++ b/workspaces/src/install/admin/install_admin.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +set -e + +echo "Installing Admin Service" + +# Install Poetry +# shellcheck disable=SC2312 +curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5 +export PATH="/root/.local/bin:${PATH}" + +# Copy admin service to /opt/admin +mkdir -p /opt/admin +cp -r "${INST_DIR}/../../admin" /opt/ + +# Install dependencies using Poetry +cd /opt/admin +poetry config virtualenvs.in-project true +poetry install --only main --no-root + +echo "Admin Service installation complete" diff --git a/workspaces/src/startup/configure_nginx.py b/workspaces/src/startup/configure_nginx.py index 282ee55..822fbaa 100755 --- a/workspaces/src/startup/configure_nginx.py +++ b/workspaces/src/startup/configure_nginx.py @@ -66,3 +66,12 @@ + NGINX_FILE, shell=True ) + +admin_server_port = os.getenv("ADMIN_SERVER_PORT") +call( + "sed -i 's@{ADMIN_SERVER_PORT}@" + + admin_server_port + + "@g' " + + NGINX_FILE, + shell=True +) diff --git a/workspaces/src/startup/custom_startup.sh b/workspaces/src/startup/custom_startup.sh index 35d1f5e..c58b342 100755 --- a/workspaces/src/startup/custom_startup.sh +++ b/workspaces/src/startup/custom_startup.sh @@ -44,6 +44,13 @@ function start_vscode_server { DTAAS_PROCS['vscode']=$! } +function start_admin_server { + export PATH="/root/.local/bin:${PATH}" + cd /opt/admin + poetry run uvicorn admin.main:app --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" & + DTAAS_PROCS['admin']=$! +} + # Links the persistent dir to its subdirectory in home. Can only happen after # KASM has setup the main user home directories. if [[ ! -h "${HOME}"/Desktop/workspace ]]; then @@ -53,6 +60,7 @@ fi start_nginx start_jupyter start_vscode_server "${PERSISTENT_DIR}" +start_admin_server # Monitor and resurrect DTaaS services. sleep 3 @@ -82,6 +90,10 @@ do echo "[INFO] Restarting VS Code server" start_vscode_server "${PERSISTENT_DIR}" ;; + admin) + echo "[INFO] Restarting Admin server" + start_admin_server + ;; *) echo "[WARNING] An unknown service '${process}' unexpectededly monitored by the custom_startup script was reported to have exitted. This is most irregular - check if something is adding processes to the custom_startup scripts list of monitored subprocesses." ;; diff --git a/workspaces/src/startup/nginx.conf b/workspaces/src/startup/nginx.conf index d0fe579..30b9315 100755 --- a/workspaces/src/startup/nginx.conf +++ b/workspaces/src/startup/nginx.conf @@ -16,6 +16,10 @@ http { server 127.0.0.1:{VNC_PORT}; } + upstream admin { + server 127.0.0.1:{ADMIN_SERVER_PORT}; + } + server { access_log /home/{MAIN_USER}/.nginx/logs/access.log; client_body_temp_path /home/{MAIN_USER}/.nginx/temp/client_body; @@ -41,6 +45,16 @@ http { return 200; } + location ~* "^{WORKSPACE_BASE_URL_DECODED}/services" { + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + proxy_pass http://admin; + } + # if base path is not in request url -> alway redirect to base path location ~* "^(?!{WORKSPACE_BASE_URL_DECODED}).*$" { auth_basic off; From a5de3f30f875207a188f0e95e71567a3b7b1dfc3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:15:58 +0000 Subject: [PATCH 03/14] Fix type hints and add comprehensive documentation - Fix FastAPI response type hints for root endpoint - Add service discovery section to README - Create detailed DOCUMENTATION.md for admin service - Verify all tests pass (8/8 passing) - Verify service works locally with correct MAIN_USER substitution Co-authored-by: prasadtalasila <9206466+prasadtalasila@users.noreply.github.com> --- README.md | 42 ++ workspaces/src/admin/DOCUMENTATION.md | 187 +++++ workspaces/src/admin/poetry.lock | 905 +++++++++++++++++++++++++ workspaces/src/admin/src/admin/main.py | 2 +- 4 files changed, 1135 insertions(+), 1 deletion(-) create mode 100644 workspaces/src/admin/DOCUMENTATION.md create mode 100644 workspaces/src/admin/poetry.lock diff --git a/README.md b/README.md index 5e3740d..794da32 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,48 @@ the `.env` file. - ***Open Jupyter Notebook*** - - ***Open Jupyter Lab*** - +### Service Discovery + +The workspace provides a `/services` endpoint that returns a JSON list of +available services. This enables dynamic service discovery for frontend +applications. + +**Example**: Get service list for user1 + +```bash +curl http://localhost:8080/user1/services +``` + +**Response**: + +```json +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path=user1%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} +``` + +The endpoint values are dynamically populated with the user's username from the +`MAIN_USER` environment variable. + ## :broom: Clean Up *Either* diff --git a/workspaces/src/admin/DOCUMENTATION.md b/workspaces/src/admin/DOCUMENTATION.md new file mode 100644 index 0000000..04c2672 --- /dev/null +++ b/workspaces/src/admin/DOCUMENTATION.md @@ -0,0 +1,187 @@ +# Admin Service Documentation + +The admin service is a FastAPI-based REST API that provides service discovery +and management capabilities for the DTaaS workspace. + +## Overview + +The service runs on port 8091 (configurable via `ADMIN_SERVER_PORT` environment +variable) and is proxied through nginx to be accessible at the `/services` +endpoint. + +## Endpoints + +### GET /services + +Returns a JSON object containing information about all available workspace +services. + +**Request**: + +```bash +curl http://localhost:8080/{username}/services +``` + +**Response**: Status 200 OK + +```json +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path={username}%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} +``` + +**Notes**: +- The `{username}` placeholder in endpoints is automatically replaced with the + value of the `MAIN_USER` environment variable +- Service endpoints are relative paths that should be appended to the base + workspace URL +- Empty string endpoints indicate the service is available at the root path + +### GET /health + +Health check endpoint for monitoring service availability. + +**Request**: + +```bash +curl http://localhost:8091/health +``` + +**Response**: Status 200 OK + +```json +{ + "status": "healthy" +} +``` + +### GET / + +Root endpoint providing service metadata and available endpoints. + +**Request**: + +```bash +curl http://localhost:8091/ +``` + +**Response**: Status 200 OK + +```json +{ + "service": "Workspace Admin Service", + "version": "0.1.0", + "endpoints": { + "/services": "Get list of available workspace services", + "/health": "Health check endpoint" + } +} +``` + +## Architecture + +### Service Discovery Flow + +1. User accesses `http://{domain}/{username}/services` +2. nginx receives the request and routes it to the admin service on port 8091 +3. Admin service reads the `services_template.json` file +4. Template placeholders (e.g., `{MAIN_USER}`) are replaced with environment + variable values +5. JSON response is returned to the client + +### Components + +- **FastAPI Application** (`src/admin/main.py`): Core service implementation +- **Services Template** (`src/admin/services_template.json`): JSON template + defining available services +- **nginx Configuration** (`startup/nginx.conf`): Reverse proxy routing +- **Startup Script** (`startup/custom_startup.sh`): Service bootstrap and + monitoring + +## Environment Variables + +- `MAIN_USER`: Username for the workspace (default: `dtaas-user`) +- `ADMIN_SERVER_PORT`: Port for the admin service (default: `8091`) + +## Development + +### Running Tests + +```bash +cd workspaces/src/admin +poetry install +poetry run pytest -v +``` + +### Running Locally + +```bash +cd workspaces/src/admin +export MAIN_USER=testuser +export ADMIN_SERVER_PORT=8091 +poetry run uvicorn admin.main:app --host 0.0.0.0 --port 8091 +``` + +### Adding New Services + +To add a new service to the workspace: + +1. Update `services_template.json` with the new service definition: + +```json +{ + "new_service": { + "name": "New Service Name", + "description": "Description of the service", + "endpoint": "path/to/service" + } +} +``` + +2. If the service requires dynamic values, use placeholders like `{MAIN_USER}` + which will be substituted at runtime + +3. No code changes are required - the service automatically reads and processes + the template + +## Integration with DTaaS + +The `/services` endpoint enables the DTaaS frontend to: + +1. Dynamically discover available workspace services +2. Display service shortcuts to users +3. Support different workspace configurations without hardcoded service lists +4. Handle multi-user deployments where each user has different available + services + +This replaces the previous approach of hardcoding service endpoints in the +frontend configuration, enabling more flexible workspace deployments. + +## Future Enhancements + +Potential future enhancements include: + +- Service registry for dynamic service registration +- Authentication and authorization integration +- Service health monitoring and status reporting +- Custom service definitions per workspace type +- Web application firewall integration for zero-trust security diff --git a/workspaces/src/admin/poetry.lock b/workspaces/src/admin/poetry.lock new file mode 100644 index 0000000..919b5c8 --- /dev/null +++ b/workspaces/src/admin/poetry.lock @@ -0,0 +1,905 @@ +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.12.1" +description = "High-level concurrency and networking framework on top of asyncio or Trio" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c"}, + {file = "anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703"}, +] + +[package.dependencies] +idna = ">=2.8" +typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} + +[package.extras] +trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] + +[[package]] +name = "certifi" +version = "2026.1.4" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, + {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, +] + +[[package]] +name = "click" +version = "8.3.1" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"}, + {file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} + +[[package]] +name = "fastapi" +version = "0.115.14" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, + {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "h11" +version = "0.16.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, + {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, + {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.16" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httptools" +version = "0.7.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "httptools-0.7.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:11d01b0ff1fe02c4c32d60af61a4d613b74fad069e47e06e9067758c01e9ac78"}, + {file = "httptools-0.7.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84d86c1e5afdc479a6fdabf570be0d3eb791df0ae727e8dbc0259ed1249998d4"}, + {file = "httptools-0.7.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c8c751014e13d88d2be5f5f14fc8b89612fcfa92a9cc480f2bc1598357a23a05"}, + {file = "httptools-0.7.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:654968cb6b6c77e37b832a9be3d3ecabb243bbe7a0b8f65fbc5b6b04c8fcabed"}, + {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b580968316348b474b020edf3988eecd5d6eec4634ee6561e72ae3a2a0e00a8a"}, + {file = "httptools-0.7.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d496e2f5245319da9d764296e86c5bb6fcf0cf7a8806d3d000717a889c8c0b7b"}, + {file = "httptools-0.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:cbf8317bfccf0fed3b5680c559d3459cccf1abe9039bfa159e62e391c7270568"}, + {file = "httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657"}, + {file = "httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70"}, + {file = "httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df"}, + {file = "httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e"}, + {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274"}, + {file = "httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec"}, + {file = "httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb"}, + {file = "httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5"}, + {file = "httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5"}, + {file = "httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03"}, + {file = "httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2"}, + {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362"}, + {file = "httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c"}, + {file = "httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321"}, + {file = "httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3"}, + {file = "httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca"}, + {file = "httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c"}, + {file = "httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66"}, + {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346"}, + {file = "httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650"}, + {file = "httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6"}, + {file = "httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270"}, + {file = "httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3"}, + {file = "httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1"}, + {file = "httptools-0.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df091cf961a3be783d6aebae963cc9b71e00d57fa6f149025075217bc6a55a7b"}, + {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f084813239e1eb403ddacd06a30de3d3e09a9b76e7894dcda2b22f8a726e9c60"}, + {file = "httptools-0.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7347714368fb2b335e9063bc2b96f2f87a9ceffcd9758ac295f8bbcd3ffbc0ca"}, + {file = "httptools-0.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:cfabda2a5bb85aa2a904ce06d974a3f30fb36cc63d7feaddec05d2050acede96"}, + {file = "httptools-0.7.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ac50afa68945df63ec7a2707c506bd02239272288add34539a2ef527254626a4"}, + {file = "httptools-0.7.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de987bb4e7ac95b99b805b99e0aae0ad51ae61df4263459d36e07cf4052d8b3a"}, + {file = "httptools-0.7.1-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d169162803a24425eb5e4d51d79cbf429fd7a491b9e570a55f495ea55b26f0bf"}, + {file = "httptools-0.7.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49794f9250188a57fa73c706b46cb21a313edb00d337ca4ce1a011fe3c760b28"}, + {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aeefa0648362bb97a7d6b5ff770bfb774930a327d7f65f8208394856862de517"}, + {file = "httptools-0.7.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0d92b10dbf0b3da4823cde6a96d18e6ae358a9daa741c71448975f6a2c339cad"}, + {file = "httptools-0.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:5ddbd045cfcb073db2449563dd479057f2c2b681ebc232380e63ef15edc9c023"}, + {file = "httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9"}, +] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "idna" +version = "3.11" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, + {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "iniconfig" +version = "2.3.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, + {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, +] + +[[package]] +name = "packaging" +version = "26.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, + {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["coverage", "pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.12.5" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d"}, + {file = "pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.41.5" +typing-extensions = ">=4.14.1" +typing-inspection = ">=0.4.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146"}, + {file = "pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a"}, + {file = "pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556"}, + {file = "pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba"}, + {file = "pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6"}, + {file = "pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594"}, + {file = "pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe"}, + {file = "pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7"}, + {file = "pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294"}, + {file = "pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815"}, + {file = "pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9"}, + {file = "pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586"}, + {file = "pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e"}, + {file = "pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11"}, + {file = "pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a"}, + {file = "pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375"}, + {file = "pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07"}, + {file = "pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf"}, + {file = "pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c"}, + {file = "pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8bfeaf8735be79f225f3fefab7f941c712aaca36f1128c9d7e2352ee1aa87bdf"}, + {file = "pydantic_core-2.41.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:346285d28e4c8017da95144c7f3acd42740d637ff41946af5ce6e5e420502dd5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a75dafbf87d6276ddc5b2bf6fae5254e3d0876b626eb24969a574fff9149ee5d"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7b93a4d08587e2b7e7882de461e82b6ed76d9026ce91ca7915e740ecc7855f60"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8465ab91a4bd96d36dde3263f06caa6a8a6019e4113f24dc753d79a8b3a3f82"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:299e0a22e7ae2b85c1a57f104538b2656e8ab1873511fd718a1c1c6f149b77b5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:707625ef0983fcfb461acfaf14de2067c5942c6bb0f3b4c99158bed6fedd3cf3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f41eb9797986d6ebac5e8edff36d5cef9de40def462311b3eb3eeded1431e425"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0384e2e1021894b1ff5a786dbf94771e2986ebe2869533874d7e43bc79c6f504"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:f0cd744688278965817fd0839c4a4116add48d23890d468bc436f78beb28abf5"}, + {file = "pydantic_core-2.41.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:753e230374206729bf0a807954bcc6c150d3743928a73faffee51ac6557a03c3"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win32.whl", hash = "sha256:873e0d5b4fb9b89ef7c2d2a963ea7d02879d9da0da8d9d4933dee8ee86a8b460"}, + {file = "pydantic_core-2.41.5-cp39-cp39-win_amd64.whl", hash = "sha256:e4f4a984405e91527a0d62649ee21138f8e3d0ef103be488c1dc11a80d7f184b"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2"}, + {file = "pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56"}, + {file = "pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963"}, + {file = "pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f"}, + {file = "pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51"}, + {file = "pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e"}, +] + +[package.dependencies] +typing-extensions = ">=4.14.1" + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.4.2" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"}, + {file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"}, +] + +[package.dependencies] +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +iniconfig = ">=1" +packaging = ">=20" +pluggy = ">=1.5,<2" +pygments = ">=2.7.2" + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.24.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "python-dotenv" +version = "1.2.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61"}, + {file = "python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.3" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, + {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, + {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, + {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, + {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, + {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, + {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, + {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, + {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, + {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, + {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, + {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, + {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, + {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, + {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, + {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, + {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, + {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, + {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, + {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, + {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, + {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, + {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, + {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, + {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, + {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, + {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, + {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, + {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, + {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, + {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, + {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, + {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, + {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, + {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, + {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, + {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, + {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, + {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, +] + +[[package]] +name = "starlette" +version = "0.46.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, + {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, +] + +[package.dependencies] +anyio = ">=3.6.2,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] +markers = {dev = "python_version == \"3.12\""} + +[[package]] +name = "typing-inspection" +version = "0.4.2" +description = "Runtime typing introspection tools" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"}, + {file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"}, +] + +[package.dependencies] +typing-extensions = ">=4.12.0" + +[[package]] +name = "uvicorn" +version = "0.32.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"}, + {file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.6.3", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.22.1" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.1" +groups = ["main"] +markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"" +files = [ + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ef6f0d4cc8a9fa1f6a910230cd53545d9a14479311e87e3cb225495952eb672c"}, + {file = "uvloop-0.22.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7cd375a12b71d33d46af85a3343b35d98e8116134ba404bd657b3b1d15988792"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac33ed96229b7790eb729702751c0e93ac5bc3bcf52ae9eccbff30da09194b86"}, + {file = "uvloop-0.22.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:481c990a7abe2c6f4fc3d98781cc9426ebd7f03a9aaa7eb03d3bfc68ac2a46bd"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a592b043a47ad17911add5fbd087c76716d7c9ccc1d64ec9249ceafd735f03c2"}, + {file = "uvloop-0.22.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1489cf791aa7b6e8c8be1c5a080bae3a672791fcb4e9e12249b05862a2ca9cec"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9"}, + {file = "uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21"}, + {file = "uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733"}, + {file = "uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42"}, + {file = "uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370"}, + {file = "uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2"}, + {file = "uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705"}, + {file = "uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d"}, + {file = "uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e"}, + {file = "uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142"}, + {file = "uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35"}, + {file = "uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6"}, + {file = "uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289"}, + {file = "uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c"}, + {file = "uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88"}, + {file = "uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:80eee091fe128e425177fbd82f8635769e2f32ec9daf6468286ec57ec0313efa"}, + {file = "uvloop-0.22.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:017bd46f9e7b78e81606329d07141d3da446f8798c6baeec124260e22c262772"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c3e5c6727a57cb6558592a95019e504f605d1c54eb86463ee9f7a2dbd411c820"}, + {file = "uvloop-0.22.1-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:57df59d8b48feb0e613d9b1f5e57b7532e97cbaf0d61f7aa9aa32221e84bc4b6"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:55502bc2c653ed2e9692e8c55cb95b397d33f9f2911e929dc97c4d6b26d04242"}, + {file = "uvloop-0.22.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4a968a72422a097b09042d5fa2c5c590251ad484acf910a651b4b620acd7f193"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b45649628d816c030dba3c80f8e2689bab1c89518ed10d426036cdc47874dfc4"}, + {file = "uvloop-0.22.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ea721dd3203b809039fcc2983f14608dae82b212288b346e0bfe46ec2fab0b7c"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ae676de143db2b2f60a9696d7eca5bb9d0dd6cc3ac3dad59a8ae7e95f9e1b54"}, + {file = "uvloop-0.22.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17d4e97258b0172dfa107b89aa1eeba3016f4b1974ce85ca3ef6a66b35cbf659"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:05e4b5f86e621cf3927631789999e697e58f0d2d32675b67d9ca9eb0bca55743"}, + {file = "uvloop-0.22.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:286322a90bea1f9422a470d5d2ad82d38080be0a29c4dd9b3e6384320a4d11e7"}, + {file = "uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f"}, +] + +[package.extras] +dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx_rtd_theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["aiohttp (>=3.10.5)", "flake8 (>=6.1,<7.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=25.3.0,<25.4.0)", "pycodestyle (>=2.11.0,<2.12.0)"] + +[[package]] +name = "watchfiles" +version = "1.1.1" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "watchfiles-1.1.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:eef58232d32daf2ac67f42dea51a2c80f0d03379075d44a587051e63cc2e368c"}, + {file = "watchfiles-1.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:03fa0f5237118a0c5e496185cafa92878568b652a2e9a9382a5151b1a0380a43"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca65483439f9c791897f7db49202301deb6e15fe9f8fe2fed555bf986d10c31"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f0ab1c1af0cb38e3f598244c17919fb1a84d1629cc08355b0074b6d7f53138ac"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3bc570d6c01c206c46deb6e935a260be44f186a2f05179f52f7fcd2be086a94d"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e84087b432b6ac94778de547e08611266f1f8ffad28c0ee4c82e028b0fc5966d"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:620bae625f4cb18427b1bb1a2d9426dc0dd5a5ba74c7c2cdb9de405f7b129863"}, + {file = "watchfiles-1.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:544364b2b51a9b0c7000a4b4b02f90e9423d97fbbf7e06689236443ebcad81ab"}, + {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bbe1ef33d45bc71cf21364df962af171f96ecaeca06bd9e3d0b583efb12aec82"}, + {file = "watchfiles-1.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1a0bb430adb19ef49389e1ad368450193a90038b5b752f4ac089ec6942c4dff4"}, + {file = "watchfiles-1.1.1-cp310-cp310-win32.whl", hash = "sha256:3f6d37644155fb5beca5378feb8c1708d5783145f2a0f1c4d5a061a210254844"}, + {file = "watchfiles-1.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:a36d8efe0f290835fd0f33da35042a1bb5dc0e83cbc092dcf69bce442579e88e"}, + {file = "watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5"}, + {file = "watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff"}, + {file = "watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606"}, + {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701"}, + {file = "watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10"}, + {file = "watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849"}, + {file = "watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4"}, + {file = "watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e"}, + {file = "watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d"}, + {file = "watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb"}, + {file = "watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803"}, + {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94"}, + {file = "watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43"}, + {file = "watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9"}, + {file = "watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9"}, + {file = "watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404"}, + {file = "watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18"}, + {file = "watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae"}, + {file = "watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d"}, + {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b"}, + {file = "watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374"}, + {file = "watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0"}, + {file = "watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42"}, + {file = "watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18"}, + {file = "watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da"}, + {file = "watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04"}, + {file = "watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77"}, + {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef"}, + {file = "watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf"}, + {file = "watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5"}, + {file = "watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bfff9740c69c0e4ed32416f013f3c45e2ae42ccedd1167ef2d805c000b6c71a5"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b27cf2eb1dda37b2089e3907d8ea92922b673c0c427886d4edc6b94d8dfe5db3"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526e86aced14a65a5b0ec50827c745597c782ff46b571dbfe46192ab9e0b3c33"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04e78dd0b6352db95507fd8cb46f39d185cf8c74e4cf1e4fbad1d3df96faf510"}, + {file = "watchfiles-1.1.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c85794a4cfa094714fb9c08d4a218375b2b95b8ed1666e8677c349906246c05"}, + {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:74d5012b7630714b66be7b7b7a78855ef7ad58e8650c73afc4c076a1f480a8d6"}, + {file = "watchfiles-1.1.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:8fbe85cb3201c7d380d3d0b90e63d520f15d6afe217165d7f98c9c649654db81"}, + {file = "watchfiles-1.1.1-cp314-cp314-win32.whl", hash = "sha256:3fa0b59c92278b5a7800d3ee7733da9d096d4aabcfabb9a928918bd276ef9b9b"}, + {file = "watchfiles-1.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:c2047d0b6cea13b3316bdbafbfa0c4228ae593d995030fda39089d36e64fc03a"}, + {file = "watchfiles-1.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:842178b126593addc05acf6fce960d28bc5fae7afbaa2c6c1b3a7b9460e5be02"}, + {file = "watchfiles-1.1.1-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:88863fbbc1a7312972f1c511f202eb30866370ebb8493aef2812b9ff28156a21"}, + {file = "watchfiles-1.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:55c7475190662e202c08c6c0f4d9e345a29367438cf8e8037f3155e10a88d5a5"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f53fa183d53a1d7a8852277c92b967ae99c2d4dcee2bfacff8868e6e30b15f7"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6aae418a8b323732fa89721d86f39ec8f092fc2af67f4217a2b07fd3e93c6101"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f096076119da54a6080e8920cbdaac3dbee667eb91dcc5e5b78840b87415bd44"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:00485f441d183717038ed2e887a7c868154f216877653121068107b227a2f64c"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a55f3e9e493158d7bfdb60a1165035f1cf7d320914e7b7ea83fe22c6023b58fc"}, + {file = "watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c"}, + {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099"}, + {file = "watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01"}, + {file = "watchfiles-1.1.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c882d69f6903ef6092bedfb7be973d9319940d56b8427ab9187d1ecd73438a70"}, + {file = "watchfiles-1.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d6ff426a7cb54f310d51bfe83fe9f2bbe40d540c741dc974ebc30e6aa238f52e"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79ff6c6eadf2e3fc0d7786331362e6ef1e51125892c75f1004bd6b52155fb956"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c1f5210f1b8fc91ead1283c6fd89f70e76fb07283ec738056cf34d51e9c1d62c"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9c4702f29ca48e023ffd9b7ff6b822acdf47cb1ff44cb490a3f1d5ec8987e9c"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acb08650863767cbc58bca4813b92df4d6c648459dcaa3d4155681962b2aa2d3"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08af70fd77eee58549cd69c25055dc344f918d992ff626068242259f98d598a2"}, + {file = "watchfiles-1.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c3631058c37e4a0ec440bf583bc53cdbd13e5661bb6f465bc1d88ee9a0a4d02"}, + {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:cf57a27fb986c6243d2ee78392c503826056ffe0287e8794503b10fb51b881be"}, + {file = "watchfiles-1.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d7e7067c98040d646982daa1f37a33d3544138ea155536c2e0e63e07ff8a7e0f"}, + {file = "watchfiles-1.1.1-cp39-cp39-win32.whl", hash = "sha256:6c9c9262f454d1c4d8aaa7050121eb4f3aea197360553699520767daebf2180b"}, + {file = "watchfiles-1.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:74472234c8370669850e1c312490f6026d132ca2d396abfad8830b4f1c096957"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:17ef139237dfced9da49fb7f2232c86ca9421f666d78c264c7ffca6601d154c3"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:672b8adf25b1a0d35c96b5888b7b18699d27d4194bac8beeae75be4b7a3fc9b2"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77a13aea58bc2b90173bc69f2a90de8e282648939a00a602e1dc4ee23e26b66d"}, + {file = "watchfiles-1.1.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b495de0bb386df6a12b18335a0285dda90260f51bdb505503c02bcd1ce27a8b"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24"}, + {file = "watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:cdab464fee731e0884c35ae3588514a9bcf718d0e2c82169c1c4a85cc19c3c7f"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:3dbd8cbadd46984f802f6d479b7e3afa86c42d13e8f0f322d669d79722c8ec34"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5524298e3827105b61951a29c3512deb9578586abf3a7c5da4a8069df247cccc"}, + {file = "watchfiles-1.1.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b943d3668d61cfa528eb949577479d3b077fd25fb83c641235437bc0b5bc60e"}, + {file = "watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "16.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.10" +groups = ["main"] +files = [ + {file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"}, + {file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"}, + {file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"}, + {file = "websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72"}, + {file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"}, + {file = "websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"}, + {file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"}, + {file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"}, + {file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"}, + {file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"}, + {file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"}, + {file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"}, + {file = "websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe"}, + {file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"}, + {file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"}, + {file = "websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"}, + {file = "websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"}, + {file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"}, + {file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"}, + {file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"}, + {file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"}, + {file = "websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c"}, + {file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"}, + {file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"}, + {file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"}, + {file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"}, + {file = "websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"}, + {file = "websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"}, + {file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"}, + {file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"}, + {file = "websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5"}, + {file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"}, + {file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"}, + {file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"}, + {file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"}, + {file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"}, + {file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"}, + {file = "websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"}, + {file = "websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"}, + {file = "websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e"}, + {file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"}, + {file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"}, + {file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"}, + {file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"}, + {file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"}, + {file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"}, + {file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"}, + {file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"}, + {file = "websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"}, + {file = "websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"}, + {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"}, + {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"}, + {file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"}, + {file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"}, + {file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"}, + {file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"}, + {file = "websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da"}, + {file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"}, + {file = "websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"}, + {file = "websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"}, + {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, +] + +[metadata] +lock-version = "2.1" +python-versions = "^3.12" +content-hash = "4a51ed0c479750d884b8b6b0bb2399126e6030980166661fcb88b1437b8e2e49" diff --git a/workspaces/src/admin/src/admin/main.py b/workspaces/src/admin/src/admin/main.py index 7ed3b66..d9e2dd0 100644 --- a/workspaces/src/admin/src/admin/main.py +++ b/workspaces/src/admin/src/admin/main.py @@ -48,7 +48,7 @@ def load_services() -> Dict[str, Any]: @app.get("/") -async def root() -> Dict[str, str]: +async def root() -> Dict[str, Any]: """Root endpoint providing service information.""" return { "service": "Workspace Admin Service", From 2ce79a4c5d804883cfb9da415c795b6b5958bbb0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:17:49 +0000 Subject: [PATCH 04/14] Update .gitignore to exclude pytest cache and poetry.lock - Add .pytest_cache/ to .gitignore - Add poetry.lock to .gitignore (project-specific lock files shouldn't be committed for libraries) Co-authored-by: prasadtalasila <9206466+prasadtalasila@users.noreply.github.com> --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ffe5bb9..316d66f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,8 @@ env/ .env/ ENV/ __pycache__/ +.pytest_cache/ +poetry.lock # Runtime data pids From 940d8134d97d3cd5102bf395de194c91b9756393 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 20:00:28 +0000 Subject: [PATCH 05/14] Fix Docker build path, linting issues, and add CLI interface Problem-1: Fixed installation script path from ${INST_DIR}/../../admin to ${INST_DIR}/../admin Problem-2: Removed whitespace and unused imports (os, Path) from Python files Problem-3: Added pylint ^4.0.4 and pytest-cov ^7.0.0 to dev dependencies Problem-4: Removed unused pytest-asyncio dependency Improvement-5: Added CLI interface with argparse, making service runnable as workspace-admin command - Updated DOCUMENTATION.md and README.md with pylint, pytest-cov, and CLI usage examples - CLI supports --list-services, --host, --port, --reload, and --help options - Service now works like a system utility similar to glances Co-authored-by: prasadtalasila <9206466+prasadtalasila@users.noreply.github.com> --- workspaces/src/admin/DOCUMENTATION.md | 46 ++++ workspaces/src/admin/README.md | 37 ++- workspaces/src/admin/poetry.lock | 250 +++++++++++++++++- workspaces/src/admin/pyproject.toml | 7 +- workspaces/src/admin/src/admin/main.py | 78 +++++- workspaces/src/admin/tests/test_main.py | 22 +- workspaces/src/install/admin/install_admin.sh | 3 +- 7 files changed, 410 insertions(+), 33 deletions(-) diff --git a/workspaces/src/admin/DOCUMENTATION.md b/workspaces/src/admin/DOCUMENTATION.md index 04c2672..32c5f79 100644 --- a/workspaces/src/admin/DOCUMENTATION.md +++ b/workspaces/src/admin/DOCUMENTATION.md @@ -132,8 +132,29 @@ poetry install poetry run pytest -v ``` +### Code Quality and Coverage + +**Run pylint for code quality analysis:** + +```bash +cd workspaces/src/admin +poetry run pylint src/admin tests +``` + +**Run tests with coverage analysis:** + +```bash +cd workspaces/src/admin +poetry run pytest --cov=admin --cov-report=html --cov-report=term +``` + +This will generate a coverage report in `htmlcov/index.html` and display a +summary in the terminal. + ### Running Locally +**As a service:** + ```bash cd workspaces/src/admin export MAIN_USER=testuser @@ -141,6 +162,31 @@ export ADMIN_SERVER_PORT=8091 poetry run uvicorn admin.main:app --host 0.0.0.0 --port 8091 ``` +**As a CLI utility:** + +```bash +cd workspaces/src/admin +poetry install + +# Run the service +poetry run workspace-admin + +# Run with custom host and port +poetry run workspace-admin --host 127.0.0.1 --port 9000 + +# List services without starting the server +poetry run workspace-admin --list-services + +# Run with auto-reload for development +poetry run workspace-admin --reload + +# Show help +poetry run workspace-admin --help +``` + +The CLI interface makes the admin service work like a system utility similar to +glances, allowing easy command-line operation and service listing. + ### Adding New Services To add a new service to the workspace: diff --git a/workspaces/src/admin/README.md b/workspaces/src/admin/README.md index ff3548b..2e76ebe 100644 --- a/workspaces/src/admin/README.md +++ b/workspaces/src/admin/README.md @@ -6,12 +6,37 @@ FastAPI service for workspace service discovery and management. - `/services` endpoint - Returns JSON list of available workspace services - Environment-aware configuration (uses MAIN_USER environment variable) +- Command-line interface for standalone operation ## Running +### As a Service (in workspace container) + The service is automatically started when the workspace container starts. It runs on port 8091 and is accessible via the nginx reverse proxy at `/services`. +### As a CLI Utility + +```bash +# Install dependencies +poetry install + +# Run the service +poetry run workspace-admin + +# Run with custom port +poetry run workspace-admin --port 9000 + +# List services without starting the server +poetry run workspace-admin --list-services + +# Run with auto-reload for development +poetry run workspace-admin --reload + +# Show help +poetry run workspace-admin --help +``` + ## Development Install dependencies: @@ -23,11 +48,17 @@ poetry install Run tests: ```bash -poetry run pytest +poetry run pytest -v +``` + +Run tests with coverage: + +```bash +poetry run pytest --cov=admin --cov-report=html --cov-report=term ``` -Run the service locally: +Run code quality checks: ```bash -poetry run uvicorn admin.main:app --host 0.0.0.0 --port 8091 +poetry run pylint src/admin tests ``` diff --git a/workspaces/src/admin/poetry.lock b/workspaces/src/admin/poetry.lock index 919b5c8..7875889 100644 --- a/workspaces/src/admin/poetry.lock +++ b/workspaces/src/admin/poetry.lock @@ -31,6 +31,18 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} [package.extras] trio = ["trio (>=0.31.0) ; python_version < \"3.10\"", "trio (>=0.32.0) ; python_version >= \"3.10\""] +[[package]] +name = "astroid" +version = "4.0.4" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.10.0" +groups = ["dev"] +files = [ + {file = "astroid-4.0.4-py3-none-any.whl", hash = "sha256:52f39653876c7dec3e3afd4c2696920e05c83832b9737afc21928f2d2eb7a753"}, + {file = "astroid-4.0.4.tar.gz", hash = "sha256:986fed8bcf79fb82c78b18a53352a0b287a73817d6dbcfba3162da36667c49a0"}, +] + [[package]] name = "certifi" version = "2026.1.4" @@ -71,6 +83,141 @@ files = [ ] markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} +[[package]] +name = "coverage" +version = "7.13.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.13.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fc31c787a84f8cd6027eba44010517020e0d18487064cd3d8968941856d1415"}, + {file = "coverage-7.13.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a32ebc02a1805adf637fc8dec324b5cdacd2e493515424f70ee33799573d661b"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e24f9156097ff9dc286f2f913df3a7f63c0e333dcafa3c196f2c18b4175ca09a"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8041b6c5bfdc03257666e9881d33b1abc88daccaf73f7b6340fb7946655cd10f"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a09cfa6a5862bc2fc6ca7c3def5b2926194a56b8ab78ffcf617d28911123012"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:296f8b0af861d3970c2a4d8c91d48eb4dd4771bcef9baedec6a9b515d7de3def"}, + {file = "coverage-7.13.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e101609bcbbfb04605ea1027b10dc3735c094d12d40826a60f897b98b1c30256"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aa3feb8db2e87ff5e6d00d7e1480ae241876286691265657b500886c98f38bda"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4fc7fa81bbaf5a02801b65346c8b3e657f1d93763e58c0abdf7c992addd81a92"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:33901f604424145c6e9c2398684b92e176c0b12df77d52db81c20abd48c3794c"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:bb28c0f2cf2782508a40cec377935829d5fcc3ad9a3681375af4e84eb34b6b58"}, + {file = "coverage-7.13.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d107aff57a83222ddbd8d9ee705ede2af2cc926608b57abed8ef96b50b7e8f9"}, + {file = "coverage-7.13.4-cp310-cp310-win32.whl", hash = "sha256:a6f94a7d00eb18f1b6d403c91a88fd58cfc92d4b16080dfdb774afc8294469bf"}, + {file = "coverage-7.13.4-cp310-cp310-win_amd64.whl", hash = "sha256:2cb0f1e000ebc419632bbe04366a8990b6e32c4e0b51543a6484ffe15eaeda95"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d490ba50c3f35dd7c17953c68f3270e7ccd1c6642e2d2afe2d8e720b98f5a053"}, + {file = "coverage-7.13.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:19bc3c88078789f8ef36acb014d7241961dbf883fd2533d18cb1e7a5b4e28b11"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3998e5a32e62fdf410c0dbd3115df86297995d6e3429af80b8798aad894ca7aa"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8e264226ec98e01a8e1054314af91ee6cde0eacac4f465cc93b03dbe0bce2fd7"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3aa4e7b9e416774b21797365b358a6e827ffadaaca81b69ee02946852449f00"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71ca20079dd8f27fcf808817e281e90220475cd75115162218d0e27549f95fef"}, + {file = "coverage-7.13.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e2f25215f1a359ab17320b47bcdaca3e6e6356652e8256f2441e4ef972052903"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d65b2d373032411e86960604dc4edac91fdfb5dca539461cf2cbe78327d1e64f"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94eb63f9b363180aff17de3e7c8760c3ba94664ea2695c52f10111244d16a299"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e856bf6616714c3a9fbc270ab54103f4e685ba236fa98c054e8f87f266c93505"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:65dfcbe305c3dfe658492df2d85259e0d79ead4177f9ae724b6fb245198f55d6"}, + {file = "coverage-7.13.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b507778ae8a4c915436ed5c2e05b4a6cecfa70f734e19c22a005152a11c7b6a9"}, + {file = "coverage-7.13.4-cp311-cp311-win32.whl", hash = "sha256:784fc3cf8be001197b652d51d3fd259b1e2262888693a4636e18879f613a62a9"}, + {file = "coverage-7.13.4-cp311-cp311-win_amd64.whl", hash = "sha256:2421d591f8ca05b308cf0092807308b2facbefe54af7c02ac22548b88b95c98f"}, + {file = "coverage-7.13.4-cp311-cp311-win_arm64.whl", hash = "sha256:79e73a76b854d9c6088fe5d8b2ebe745f8681c55f7397c3c0a016192d681045f"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459"}, + {file = "coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3"}, + {file = "coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985"}, + {file = "coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0"}, + {file = "coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246"}, + {file = "coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126"}, + {file = "coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b66a2da594b6068b48b2692f043f35d4d3693fb639d5ea8b39533c2ad9ac3ab9"}, + {file = "coverage-7.13.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3599eb3992d814d23b35c536c28df1a882caa950f8f507cef23d1cbf334995ac"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:93550784d9281e374fb5a12bf1324cc8a963fd63b2d2f223503ef0fd4aa339ea"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b720ce6a88a2755f7c697c23268ddc47a571b88052e6b155224347389fdf6a3b"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7b322db1284a2ed3aa28ffd8ebe3db91c929b7a333c0820abec3d838ef5b3525"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f4594c67d8a7c89cf922d9df0438c7c7bb022ad506eddb0fdb2863359ff78242"}, + {file = "coverage-7.13.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:53d133df809c743eb8bce33b24bcababb371f4441340578cd406e084d94a6148"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:76451d1978b95ba6507a039090ba076105c87cc76fc3efd5d35d72093964d49a"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7f57b33491e281e962021de110b451ab8a24182589be17e12a22c79047935e23"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1731dc33dc276dafc410a885cbf5992f1ff171393e48a21453b78727d090de80"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:bd60d4fe2f6fa7dff9223ca1bbc9f05d2b6697bc5961072e5d3b952d46e1b1ea"}, + {file = "coverage-7.13.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9181a3ccead280b828fae232df12b16652702b49d41e99d657f46cc7b1f6ec7a"}, + {file = "coverage-7.13.4-cp313-cp313-win32.whl", hash = "sha256:f53d492307962561ac7de4cd1de3e363589b000ab69617c6156a16ba7237998d"}, + {file = "coverage-7.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:e6f70dec1cc557e52df5306d051ef56003f74d56e9c4dd7ddb07e07ef32a84dd"}, + {file = "coverage-7.13.4-cp313-cp313-win_arm64.whl", hash = "sha256:fb07dc5da7e849e2ad31a5d74e9bece81f30ecf5a42909d0a695f8bd1874d6af"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40d74da8e6c4b9ac18b15331c4b5ebc35a17069410cad462ad4f40dcd2d50c0d"}, + {file = "coverage-7.13.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4223b4230a376138939a9173f1bdd6521994f2aff8047fae100d6d94d50c5a12"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1d4be36a5114c499f9f1f9195e95ebf979460dbe2d88e6816ea202010ba1c34b"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:200dea7d1e8095cc6e98cdabe3fd1d21ab17d3cee6dab00cadbb2fe35d9c15b9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8eb931ee8e6d8243e253e5ed7336deea6904369d2fd8ae6e43f68abbf167092"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:75eab1ebe4f2f64d9509b984f9314d4aa788540368218b858dad56dc8f3e5eb9"}, + {file = "coverage-7.13.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c35eb28c1d085eb7d8c9b3296567a1bebe03ce72962e932431b9a61f28facf26"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb88b316ec33760714a4720feb2816a3a59180fd58c1985012054fa7aebee4c2"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7d41eead3cc673cbd38a4417deb7fd0b4ca26954ff7dc6078e33f6ff97bed940"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:fb26a934946a6afe0e326aebe0730cdff393a8bc0bbb65a2f41e30feddca399c"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:dae88bc0fc77edaa65c14be099bd57ee140cf507e6bfdeea7938457ab387efb0"}, + {file = "coverage-7.13.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:845f352911777a8e722bfce168958214951e07e47e5d5d9744109fa5fe77f79b"}, + {file = "coverage-7.13.4-cp313-cp313t-win32.whl", hash = "sha256:2fa8d5f8de70688a28240de9e139fa16b153cc3cbb01c5f16d88d6505ebdadf9"}, + {file = "coverage-7.13.4-cp313-cp313t-win_amd64.whl", hash = "sha256:9351229c8c8407645840edcc277f4a2d44814d1bc34a2128c11c2a031d45a5dd"}, + {file = "coverage-7.13.4-cp313-cp313t-win_arm64.whl", hash = "sha256:30b8d0512f2dc8c8747557e8fb459d6176a2c9e5731e2b74d311c03b78451997"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:300deaee342f90696ed186e3a00c71b5b3d27bffe9e827677954f4ee56969601"}, + {file = "coverage-7.13.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:29e3220258d682b6226a9b0925bc563ed9a1ebcff3cad30f043eceea7eaf2689"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:391ee8f19bef69210978363ca930f7328081c6a0152f1166c91f0b5fdd2a773c"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0dd7ab8278f0d58a0128ba2fca25824321f05d059c1441800e934ff2efa52129"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78cdf0d578b15148b009ccf18c686aa4f719d887e76e6b40c38ffb61d264a552"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:48685fee12c2eb3b27c62f2658e7ea21e9c3239cba5a8a242801a0a3f6a8c62a"}, + {file = "coverage-7.13.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4e83efc079eb39480e6346a15a1bcb3e9b04759c5202d157e1dd4303cd619356"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ecae9737b72408d6a950f7e525f30aca12d4bd8dd95e37342e5beb3a2a8c4f71"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ae4578f8528569d3cf303fef2ea569c7f4c4059a38c8667ccef15c6e1f118aa5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:6fdef321fdfbb30a197efa02d48fcd9981f0d8ad2ae8903ac318adc653f5df98"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b0f6ccf3dbe577170bebfce1318707d0e8c3650003cb4b3a9dd744575daa8b5"}, + {file = "coverage-7.13.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75fcd519f2a5765db3f0e391eb3b7d150cce1a771bf4c9f861aeab86c767a3c0"}, + {file = "coverage-7.13.4-cp314-cp314-win32.whl", hash = "sha256:8e798c266c378da2bd819b0677df41ab46d78065fb2a399558f3f6cae78b2fbb"}, + {file = "coverage-7.13.4-cp314-cp314-win_amd64.whl", hash = "sha256:245e37f664d89861cf2329c9afa2c1fe9e6d4e1a09d872c947e70718aeeac505"}, + {file = "coverage-7.13.4-cp314-cp314-win_arm64.whl", hash = "sha256:ad27098a189e5838900ce4c2a99f2fe42a0bf0c2093c17c69b45a71579e8d4a2"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:85480adfb35ffc32d40918aad81b89c69c9cc5661a9b8a81476d3e645321a056"}, + {file = "coverage-7.13.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:79be69cf7f3bf9b0deeeb062eab7ac7f36cd4cc4c4dd694bd28921ba4d8596cc"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:caa421e2684e382c5d8973ac55e4f36bed6821a9bad5c953494de960c74595c9"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14375934243ee05f56c45393fe2ce81fe5cc503c07cee2bdf1725fb8bef3ffaf"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25a41c3104d08edb094d9db0d905ca54d0cd41c928bb6be3c4c799a54753af55"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f01afcff62bf9a08fb32b2c1d6e924236c0383c02c790732b6537269e466a72"}, + {file = "coverage-7.13.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eb9078108fbf0bcdde37c3f4779303673c2fa1fe8f7956e68d447d0dd426d38a"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e086334e8537ddd17e5f16a344777c1ab8194986ec533711cbe6c41cde841b6"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:725d985c5ab621268b2edb8e50dfe57633dc69bda071abc470fed55a14935fd3"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:3c06f0f1337c667b971ca2f975523347e63ec5e500b9aa5882d91931cd3ef750"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:590c0ed4bf8e85f745e6b805b2e1c457b2e33d5255dd9729743165253bc9ad39"}, + {file = "coverage-7.13.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eb30bf180de3f632cd043322dad5751390e5385108b2807368997d1a92a509d0"}, + {file = "coverage-7.13.4-cp314-cp314t-win32.whl", hash = "sha256:c4240e7eded42d131a2d2c4dec70374b781b043ddc79a9de4d55ca71f8e98aea"}, + {file = "coverage-7.13.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4c7d3cc01e7350f2f0f6f7036caaf5673fb56b6998889ccfe9e1c1fe75a9c932"}, + {file = "coverage-7.13.4-cp314-cp314t-win_arm64.whl", hash = "sha256:23e3f687cf945070d1c90f85db66d11e3025665d8dafa831301a0e0038f3db9b"}, + {file = "coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0"}, + {file = "coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + +[[package]] +name = "dill" +version = "0.4.1" +description = "serialize all of Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d"}, + {file = "dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa"}, +] + +[package.extras] +graph = ["objgraph (>=1.7.2)"] +profile = ["gprof2dot (>=2022.7.29)"] + [[package]] name = "fastapi" version = "0.115.14" @@ -231,6 +378,34 @@ files = [ {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, ] +[[package]] +name = "isort" +version = "7.0.0" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.10.0" +groups = ["dev"] +files = [ + {file = "isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1"}, + {file = "isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "mccabe" +version = "0.7.0" +description = "McCabe checker, plugin for flake8" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, +] + [[package]] name = "packaging" version = "26.0" @@ -243,6 +418,23 @@ files = [ {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, ] +[[package]] +name = "platformdirs" +version = "4.5.1" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31"}, + {file = "platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda"}, +] + +[package.extras] +docs = ["furo (>=2025.9.25)", "proselint (>=0.14)", "sphinx (>=8.2.3)", "sphinx-autodoc-typehints (>=3.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.4.2)", "pytest-cov (>=7)", "pytest-mock (>=3.15.1)"] +type = ["mypy (>=1.18.2)"] + [[package]] name = "pluggy" version = "1.6.0" @@ -430,6 +622,31 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] +[[package]] +name = "pylint" +version = "4.0.4" +description = "python code static checker" +optional = false +python-versions = ">=3.10.0" +groups = ["dev"] +files = [ + {file = "pylint-4.0.4-py3-none-any.whl", hash = "sha256:63e06a37d5922555ee2c20963eb42559918c20bd2b21244e4ef426e7c43b92e0"}, + {file = "pylint-4.0.4.tar.gz", hash = "sha256:d9b71674e19b1c36d79265b5887bf8e55278cbe236c9e95d22dc82cf044fdbd2"}, +] + +[package.dependencies] +astroid = ">=4.0.2,<=4.1.dev0" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} +dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} +isort = ">=5,<5.13 || >5.13,<8" +mccabe = ">=0.6,<0.8" +platformdirs = ">=2.2" +tomlkit = ">=0.10.1" + +[package.extras] +spelling = ["pyenchant (>=3.2,<4.0)"] +testutils = ["gitpython (>3)"] + [[package]] name = "pytest" version = "8.4.2" @@ -453,23 +670,24 @@ pygments = ">=2.7.2" dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] -name = "pytest-asyncio" -version = "0.24.0" -description = "Pytest support for asyncio" +name = "pytest-cov" +version = "7.0.0" +description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, - {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, + {file = "pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861"}, + {file = "pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1"}, ] [package.dependencies] -pytest = ">=8.2,<9" +coverage = {version = ">=7.10.6", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=7" [package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] +testing = ["process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "python-dotenv" @@ -587,6 +805,18 @@ anyio = ">=3.6.2,<5" [package.extras] full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] +[[package]] +name = "tomlkit" +version = "0.14.0" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tomlkit-0.14.0-py3-none-any.whl", hash = "sha256:592064ed85b40fa213469f81ac584f67a4f2992509a7c3ea2d632208623a3680"}, + {file = "tomlkit-0.14.0.tar.gz", hash = "sha256:cf00efca415dbd57575befb1f6634c4f42d2d87dbba376128adb42c121b87064"}, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -902,4 +1132,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "4a51ed0c479750d884b8b6b0bb2399126e6030980166661fcb88b1437b8e2e49" +content-hash = "846a5143df4479a08a43ecde2c37e42ec0d08c3e0f7c10c8b27bbc93b2e5966a" diff --git a/workspaces/src/admin/pyproject.toml b/workspaces/src/admin/pyproject.toml index 2f4ce59..70ea31c 100644 --- a/workspaces/src/admin/pyproject.toml +++ b/workspaces/src/admin/pyproject.toml @@ -6,6 +6,9 @@ authors = ["INTO-CPS Association"] readme = "README.md" packages = [{include = "admin", from = "src"}] +[tool.poetry.scripts] +workspace-admin = "admin.main:cli" + [tool.poetry.dependencies] python = "^3.12" fastapi = "^0.115.0" @@ -14,12 +17,12 @@ uvicorn = {extras = ["standard"], version = "^0.32.0"} [tool.poetry.group.dev.dependencies] pytest = "^8.3.0" httpx = "^0.28.0" -pytest-asyncio = "^0.24.0" +pylint = "^4.0.4" +pytest-cov = "^7.0.0" [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] -asyncio_mode = "auto" testpaths = ["tests"] diff --git a/workspaces/src/admin/src/admin/main.py b/workspaces/src/admin/src/admin/main.py index d9e2dd0..7dd7efa 100644 --- a/workspaces/src/admin/src/admin/main.py +++ b/workspaces/src/admin/src/admin/main.py @@ -7,6 +7,7 @@ import json import os +import sys from pathlib import Path from typing import Dict, Any @@ -26,24 +27,24 @@ def load_services() -> Dict[str, Any]: """ Load services from template and substitute environment variables. - + Returns: Dictionary containing service information with environment variables substituted. """ # Read the services template with open(SERVICES_TEMPLATE_PATH, 'r', encoding='utf-8') as f: services = json.load(f) - + # Get MAIN_USER from environment, default to 'dtaas-user' main_user = os.getenv('MAIN_USER', 'dtaas-user') - + # Substitute {MAIN_USER} in endpoint values for service_id, service_info in services.items(): if 'endpoint' in service_info: service_info['endpoint'] = service_info['endpoint'].replace( '{MAIN_USER}', main_user ) - + return services @@ -64,7 +65,7 @@ async def root() -> Dict[str, Any]: async def get_services() -> JSONResponse: """ Get list of available workspace services. - + Returns: JSONResponse containing service information. """ @@ -76,3 +77,70 @@ async def get_services() -> JSONResponse: async def health_check() -> Dict[str, str]: """Health check endpoint.""" return {"status": "healthy"} + + +def cli(): + """ + Command-line interface for the workspace admin service. + + This allows the service to be run as a standalone utility similar to glances. + """ + import argparse + import uvicorn + + parser = argparse.ArgumentParser( + description="Workspace Admin Service - Service discovery for DTaaS workspaces" + ) + parser.add_argument( + "--host", + default="0.0.0.0", + help="Host to bind the service to (default: 0.0.0.0)" + ) + parser.add_argument( + "--port", + type=int, + default=int(os.getenv("ADMIN_SERVER_PORT", "8091")), + help="Port to bind the service to (default: $ADMIN_SERVER_PORT or 8091)" + ) + parser.add_argument( + "--reload", + action="store_true", + help="Enable auto-reload for development" + ) + parser.add_argument( + "--list-services", + action="store_true", + help="List available services and exit" + ) + parser.add_argument( + "--version", + action="version", + version="%(prog)s 0.1.0" + ) + + args = parser.parse_args() + + if args.list_services: + # Just list services and exit + services = load_services() + print(json.dumps(services, indent=2)) + sys.exit(0) + + # Start the server + print(f"Starting Workspace Admin Service on {args.host}:{args.port}") + print(f"MAIN_USER: {os.getenv('MAIN_USER', 'dtaas-user')}") + print(f"Service endpoints:") + print(f" - http://{args.host}:{args.port}/services") + print(f" - http://{args.host}:{args.port}/health") + print(f" - http://{args.host}:{args.port}/") + + uvicorn.run( + "admin.main:app", + host=args.host, + port=args.port, + reload=args.reload + ) + + +if __name__ == "__main__": + cli() diff --git a/workspaces/src/admin/tests/test_main.py b/workspaces/src/admin/tests/test_main.py index ea2ce64..b253bd3 100644 --- a/workspaces/src/admin/tests/test_main.py +++ b/workspaces/src/admin/tests/test_main.py @@ -4,9 +4,7 @@ Tests the /services endpoint and service discovery functionality. """ -import os import json -from pathlib import Path import pytest from fastapi.testclient import TestClient @@ -48,22 +46,22 @@ def test_services_endpoint(client, mock_main_user): """Test the /services endpoint returns service list.""" response = client.get("/services") assert response.status_code == 200 - + services = response.json() - + # Check that we have the expected services assert "desktop" in services assert "vscode" in services assert "notebook" in services assert "lab" in services - + # Check desktop service structure desktop = services["desktop"] assert "name" in desktop assert "description" in desktop assert "endpoint" in desktop assert desktop["name"] == "Desktop" - + # Check that MAIN_USER is substituted correctly assert "testuser" in desktop["endpoint"] assert "{MAIN_USER}" not in desktop["endpoint"] @@ -73,13 +71,13 @@ def test_services_endpoint_default_user(client, monkeypatch): """Test /services endpoint with default MAIN_USER.""" # Remove MAIN_USER from environment monkeypatch.delenv('MAIN_USER', raising=False) - + response = client.get("/services") assert response.status_code == 200 - + services = response.json() desktop = services["desktop"] - + # Should use default 'dtaas-user' assert "dtaas-user" in desktop["endpoint"] @@ -87,7 +85,7 @@ def test_services_endpoint_default_user(client, monkeypatch): def test_load_services_substitutes_main_user(mock_main_user): """Test that load_services correctly substitutes MAIN_USER.""" services = load_services() - + # Check that {MAIN_USER} is replaced with 'testuser' desktop_endpoint = services["desktop"]["endpoint"] assert "testuser" in desktop_endpoint @@ -97,7 +95,7 @@ def test_load_services_substitutes_main_user(mock_main_user): def test_load_services_preserves_structure(): """Test that load_services preserves the service structure.""" services = load_services() - + # Check all required services exist required_services = ["desktop", "vscode", "notebook", "lab"] for service_id in required_services: @@ -117,7 +115,7 @@ def test_services_template_valid_json(): """Test that the services template is valid JSON.""" with open(SERVICES_TEMPLATE_PATH, 'r', encoding='utf-8') as f: services = json.load(f) - + # Should not raise an exception assert isinstance(services, dict) assert len(services) > 0 diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index b6a6263..50130ef 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -9,8 +9,9 @@ curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5 export PATH="/root/.local/bin:${PATH}" # Copy admin service to /opt/admin +# INST_DIR is /dockerstartup/install, so we need ../admin from there mkdir -p /opt/admin -cp -r "${INST_DIR}/../../admin" /opt/ +cp -r "${INST_DIR}/../admin" /opt/ # Install dependencies using Poetry cd /opt/admin From c1ce772cca4a75bf2ab50693af381f4fd39a1761 Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Wed, 11 Feb 2026 22:09:31 +0100 Subject: [PATCH 06/14] reduces installation complexity --- .gitignore | 8 +++++++- workspaces/src/admin/pyproject.toml | 2 +- workspaces/src/install/admin/install_admin.sh | 9 +++++---- workspaces/src/install/firefox/install_firefox.sh | 2 +- workspaces/src/install/jupyter/install_jupyter.sh | 2 +- workspaces/src/install/nginx/install_nginx.sh | 4 ++-- workspaces/src/startup/custom_startup.sh | 1 - 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/.gitignore b/.gitignore index 316d66f..6806371 100644 --- a/.gitignore +++ b/.gitignore @@ -86,4 +86,10 @@ workspaces/test/dtaas/certs/** !workspaces/test/dtaas/certs/README.md # temp files for workspace -.workspace \ No newline at end of file +.workspace + +# poetry files +dist/ +build/ +*.egg-info/ +.coverage \ No newline at end of file diff --git a/workspaces/src/admin/pyproject.toml b/workspaces/src/admin/pyproject.toml index 70ea31c..1ca9adf 100644 --- a/workspaces/src/admin/pyproject.toml +++ b/workspaces/src/admin/pyproject.toml @@ -1,5 +1,5 @@ [tool.poetry] -name = "admin" +name = "workspace-admin" version = "0.1.0" description = "Admin service for DTaaS workspace service discovery" authors = ["INTO-CPS Association"] diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index 50130ef..3142b19 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -4,14 +4,15 @@ set -e echo "Installing Admin Service" # Install Poetry -# shellcheck disable=SC2312 -curl -sSL https://install.python-poetry.org | python3 - --version 1.8.5 -export PATH="/root/.local/bin:${PATH}" +python3 -m pip install pipx +mkdir -p /opt/pipx +PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install poetry +poetry --version # Copy admin service to /opt/admin # INST_DIR is /dockerstartup/install, so we need ../admin from there mkdir -p /opt/admin -cp -r "${INST_DIR}/../admin" /opt/ +cp -r "${INST_DIR}/admin" /opt/ # Install dependencies using Poetry cd /opt/admin diff --git a/workspaces/src/install/firefox/install_firefox.sh b/workspaces/src/install/firefox/install_firefox.sh index faf9290..f22bf45 100644 --- a/workspaces/src/install/firefox/install_firefox.sh +++ b/workspaces/src/install/firefox/install_firefox.sh @@ -20,7 +20,7 @@ Pin: release o=LP-PPA-mozillateam Pin-Priority: 1001 ' > /etc/apt/preferences.d/mozilla-firefox fi -apt-get install -y firefox p11-kit-modules +DEBIAN_FRONTEND=noninteractive apt-get install -y firefox p11-kit-modules # Update firefox to utilize the system certificate store instead of the one that ships with firefox rm -f /usr/lib/firefox/libnssckbi.so diff --git a/workspaces/src/install/jupyter/install_jupyter.sh b/workspaces/src/install/jupyter/install_jupyter.sh index 5f9eed2..78f772c 100644 --- a/workspaces/src/install/jupyter/install_jupyter.sh +++ b/workspaces/src/install/jupyter/install_jupyter.sh @@ -3,7 +3,7 @@ set -xe # Installs Jupyter and Python3 -apt-get update && apt-get install -y \ +DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y \ python3 \ python3-pip diff --git a/workspaces/src/install/nginx/install_nginx.sh b/workspaces/src/install/nginx/install_nginx.sh index 12e8c57..8d4882c 100644 --- a/workspaces/src/install/nginx/install_nginx.sh +++ b/workspaces/src/install/nginx/install_nginx.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash set -xe -apt-get install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring +DEBIAN_FRONTEND=noninteractive apt-get install -y curl gnupg2 ca-certificates lsb-release ubuntu-keyring curl https://nginx.org/keys/nginx_signing.key | gpg --dearmor \ | tee /usr/share/keyrings/nginx-archive-keyring.gpg >/dev/null @@ -13,7 +13,7 @@ http://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" \ echo -e "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" \ | tee /etc/apt/preferences.d/99nginx -apt-get update && apt-get install -y nginx +DEBIAN_FRONTEND=noninteractive apt-get update && apt-get install -y nginx mkdir -p "${HOME}/.nginx/logs" mkdir -p "${HOME}/.nginx/temp" diff --git a/workspaces/src/startup/custom_startup.sh b/workspaces/src/startup/custom_startup.sh index c58b342..5bd9ae8 100755 --- a/workspaces/src/startup/custom_startup.sh +++ b/workspaces/src/startup/custom_startup.sh @@ -45,7 +45,6 @@ function start_vscode_server { } function start_admin_server { - export PATH="/root/.local/bin:${PATH}" cd /opt/admin poetry run uvicorn admin.main:app --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" & DTAAS_PROCS['admin']=$! From c5a90b20b88eb7801b6c34077b63f6468f207996 Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Wed, 11 Feb 2026 22:26:57 +0100 Subject: [PATCH 07/14] changes poetry installation --- workspaces/src/install/admin/install_admin.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index 3142b19..3818374 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -4,9 +4,7 @@ set -e echo "Installing Admin Service" # Install Poetry -python3 -m pip install pipx -mkdir -p /opt/pipx -PIPX_HOME=/opt/pipx PIPX_BIN_DIR=/usr/local/bin pipx install poetry +apt-get install -y python3-poetry poetry --version # Copy admin service to /opt/admin From 2bca46a6faa40e386a4cdb6192cd0243c81fb8a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:41:28 +0000 Subject: [PATCH 08/14] Fix linting issues, increase test coverage to 84%, and change to wheel+pipx installation Problem-1: Fixed all linting issues - Moved argparse and uvicorn imports to top level - Renamed unused service_id to _service_id - Fixed f-string without interpolation - Fixed test fixture warnings by renaming and proper usage - Fixed all flake8 whitespace and line length issues Problem-2: Increased test coverage from 50% to 84% - Added 6 new tests for CLI functionality and edge cases - All 14 tests pass Problem-3: Added locale prevention in apt-get - Added DEBIAN_FRONTEND=noninteractive and --no-install-recommends Problem-4 & 5: Changed from poetry run to wheel package + pipx - Updated install_admin.sh to build wheel with poetry build - Install package using pipx - Updated custom_startup.sh to use workspace-admin command - Fixed copy path to ensure pyproject.toml is found Co-authored-by: prasadtalasila <9206466+prasadtalasila@users.noreply.github.com> --- workspaces/src/admin/src/admin/main.py | 25 +++-- workspaces/src/admin/tests/test_main.py | 101 +++++++++++++++--- workspaces/src/install/admin/install_admin.sh | 43 ++++++-- workspaces/src/startup/custom_startup.sh | 3 +- 4 files changed, 141 insertions(+), 31 deletions(-) diff --git a/workspaces/src/admin/src/admin/main.py b/workspaces/src/admin/src/admin/main.py index 7dd7efa..fd04893 100644 --- a/workspaces/src/admin/src/admin/main.py +++ b/workspaces/src/admin/src/admin/main.py @@ -5,12 +5,14 @@ containing information about all available services in the workspace. """ +import argparse import json import os import sys from pathlib import Path from typing import Dict, Any +import uvicorn from fastapi import FastAPI from fastapi.responses import JSONResponse @@ -29,7 +31,8 @@ def load_services() -> Dict[str, Any]: Load services from template and substitute environment variables. Returns: - Dictionary containing service information with environment variables substituted. + Dictionary containing service information with environment + variables substituted. """ # Read the services template with open(SERVICES_TEMPLATE_PATH, 'r', encoding='utf-8') as f: @@ -39,7 +42,7 @@ def load_services() -> Dict[str, Any]: main_user = os.getenv('MAIN_USER', 'dtaas-user') # Substitute {MAIN_USER} in endpoint values - for service_id, service_info in services.items(): + for _service_id, service_info in services.items(): if 'endpoint' in service_info: service_info['endpoint'] = service_info['endpoint'].replace( '{MAIN_USER}', main_user @@ -83,13 +86,14 @@ def cli(): """ Command-line interface for the workspace admin service. - This allows the service to be run as a standalone utility similar to glances. + This allows the service to be run as a standalone utility + similar to glances. """ - import argparse - import uvicorn - parser = argparse.ArgumentParser( - description="Workspace Admin Service - Service discovery for DTaaS workspaces" + description=( + "Workspace Admin Service - " + "Service discovery for DTaaS workspaces" + ) ) parser.add_argument( "--host", @@ -100,7 +104,10 @@ def cli(): "--port", type=int, default=int(os.getenv("ADMIN_SERVER_PORT", "8091")), - help="Port to bind the service to (default: $ADMIN_SERVER_PORT or 8091)" + help=( + "Port to bind the service to " + "(default: $ADMIN_SERVER_PORT or 8091)" + ) ) parser.add_argument( "--reload", @@ -129,7 +136,7 @@ def cli(): # Start the server print(f"Starting Workspace Admin Service on {args.host}:{args.port}") print(f"MAIN_USER: {os.getenv('MAIN_USER', 'dtaas-user')}") - print(f"Service endpoints:") + print("Service endpoints:") print(f" - http://{args.host}:{args.port}/services") print(f" - http://{args.host}:{args.port}/health") print(f" - http://{args.host}:{args.port}/") diff --git a/workspaces/src/admin/tests/test_main.py b/workspaces/src/admin/tests/test_main.py index b253bd3..8d8754a 100644 --- a/workspaces/src/admin/tests/test_main.py +++ b/workspaces/src/admin/tests/test_main.py @@ -12,21 +12,21 @@ from admin.main import app, load_services, SERVICES_TEMPLATE_PATH -@pytest.fixture -def client(): +@pytest.fixture(name='test_client') +def fixture_test_client(): """Create a test client for the FastAPI app.""" return TestClient(app) -@pytest.fixture -def mock_main_user(monkeypatch): +@pytest.fixture(name='setup_mock_main_user') +def fixture_mock_main_user(monkeypatch): """Set up mock MAIN_USER environment variable.""" monkeypatch.setenv('MAIN_USER', 'testuser') -def test_root_endpoint(client): +def test_root_endpoint(test_client): """Test the root endpoint returns service information.""" - response = client.get("/") + response = test_client.get("/") assert response.status_code == 200 data = response.json() assert data["service"] == "Workspace Admin Service" @@ -34,17 +34,19 @@ def test_root_endpoint(client): assert "/services" in data["endpoints"] -def test_health_check(client): +def test_health_check(test_client): """Test the health check endpoint.""" - response = client.get("/health") + response = test_client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" -def test_services_endpoint(client, mock_main_user): +def test_services_endpoint(test_client, setup_mock_main_user): """Test the /services endpoint returns service list.""" - response = client.get("/services") + # setup_mock_main_user fixture sets MAIN_USER='testuser' + _ = setup_mock_main_user # Mark as intentionally used + response = test_client.get("/services") assert response.status_code == 200 services = response.json() @@ -67,12 +69,12 @@ def test_services_endpoint(client, mock_main_user): assert "{MAIN_USER}" not in desktop["endpoint"] -def test_services_endpoint_default_user(client, monkeypatch): +def test_services_endpoint_default_user(test_client, monkeypatch): """Test /services endpoint with default MAIN_USER.""" # Remove MAIN_USER from environment monkeypatch.delenv('MAIN_USER', raising=False) - response = client.get("/services") + response = test_client.get("/services") assert response.status_code == 200 services = response.json() @@ -82,8 +84,10 @@ def test_services_endpoint_default_user(client, monkeypatch): assert "dtaas-user" in desktop["endpoint"] -def test_load_services_substitutes_main_user(mock_main_user): +def test_load_services_substitutes_main_user(setup_mock_main_user): """Test that load_services correctly substitutes MAIN_USER.""" + # setup_mock_main_user fixture sets MAIN_USER='testuser' + _ = setup_mock_main_user # Mark as intentionally used services = load_services() # Check that {MAIN_USER} is replaced with 'testuser' @@ -119,3 +123,74 @@ def test_services_template_valid_json(): # Should not raise an exception assert isinstance(services, dict) assert len(services) > 0 + + +def test_cli_list_services(monkeypatch, capsys): + """Test CLI --list-services flag.""" + import sys + monkeypatch.setenv('MAIN_USER', 'clitest') + monkeypatch.setattr(sys, 'argv', ['workspace-admin', '--list-services']) + + from admin.main import cli + + with pytest.raises(SystemExit) as exc_info: + cli() + + assert exc_info.value.code == 0 + captured = capsys.readouterr() + output = json.loads(captured.out) + assert "desktop" in output + assert "clitest" in output["desktop"]["endpoint"] + + +def test_cli_version(monkeypatch): + """Test CLI --version flag.""" + import sys + monkeypatch.setattr(sys, 'argv', ['workspace-admin', '--version']) + + from admin.main import cli + + with pytest.raises(SystemExit) as exc_info: + cli() + + # argparse exits with 0 for --version + assert exc_info.value.code == 0 + + +def test_load_services_with_missing_endpoint(): + """Test load_services handles services without endpoint field.""" + services = load_services() + # All services should have endpoint field, even if empty + for service_info in services.values(): + assert 'endpoint' in service_info + + +def test_services_endpoint_json_structure(test_client): + """Test that services endpoint returns proper JSON structure.""" + response = test_client.get("/services") + assert response.status_code == 200 + assert response.headers["content-type"] == "application/json" + + services = response.json() + # Verify it's a dictionary with string keys + assert isinstance(services, dict) + for key, value in services.items(): + assert isinstance(key, str) + assert isinstance(value, dict) + assert "name" in value + assert "description" in value + assert "endpoint" in value + + +def test_root_endpoint_version(test_client): + """Test that root endpoint includes version.""" + response = test_client.get("/") + data = response.json() + assert "version" in data + assert data["version"] == "0.1.0" + + +def test_health_endpoint_returns_json(test_client): + """Test that health endpoint returns JSON.""" + response = test_client.get("/health") + assert response.headers["content-type"] == "application/json" diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index 3818374..5c8f79b 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -3,18 +3,47 @@ set -e echo "Installing Admin Service" -# Install Poetry -apt-get install -y python3-poetry +# Install Poetry and pipx +DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ + python3-poetry \ + pipx + poetry --version +pipx --version # Copy admin service to /opt/admin -# INST_DIR is /dockerstartup/install, so we need ../admin from there +# INST_DIR is /dockerstartup/install, admin source is at src/admin mkdir -p /opt/admin -cp -r "${INST_DIR}/admin" /opt/ +cp -r "${INST_DIR}/../admin/"* /opt/admin/ + +# Verify pyproject.toml exists +if [ ! -f /opt/admin/pyproject.toml ]; then + echo "Error: pyproject.toml not found in /opt/admin" + ls -la /opt/admin/ + exit 1 +fi -# Install dependencies using Poetry +# Build the wheel package cd /opt/admin -poetry config virtualenvs.in-project true -poetry install --only main --no-root +poetry build + +# Install the wheel package using pipx +# Find the built wheel file +WHEEL_FILE=$(find /opt/admin/dist -name "*.whl" -type f | head -n 1) +if [ -z "$WHEEL_FILE" ]; then + echo "Error: No wheel file found in /opt/admin/dist" + exit 1 +fi + +echo "Installing wheel: $WHEEL_FILE" +pipx install "$WHEEL_FILE" + +# Verify installation +if ! command -v workspace-admin &> /dev/null; then + echo "Error: workspace-admin command not found after installation" + exit 1 +fi + +workspace-admin --version echo "Admin Service installation complete" diff --git a/workspaces/src/startup/custom_startup.sh b/workspaces/src/startup/custom_startup.sh index 5bd9ae8..c8086a0 100755 --- a/workspaces/src/startup/custom_startup.sh +++ b/workspaces/src/startup/custom_startup.sh @@ -45,8 +45,7 @@ function start_vscode_server { } function start_admin_server { - cd /opt/admin - poetry run uvicorn admin.main:app --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" & + workspace-admin --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" & DTAAS_PROCS['admin']=$! } From 2cba061b763d31de9ba520a199a22a4ddb76685b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 21:42:22 +0000 Subject: [PATCH 09/14] Fix shellcheck issues in install_admin.sh - Use [[ ]] instead of [ ] for tests - Add braces around variable references - Add || true to find command to handle potential errors Co-authored-by: prasadtalasila <9206466+prasadtalasila@users.noreply.github.com> --- workspaces/src/install/admin/install_admin.sh | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index 5c8f79b..a3cb158 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -17,7 +17,7 @@ mkdir -p /opt/admin cp -r "${INST_DIR}/../admin/"* /opt/admin/ # Verify pyproject.toml exists -if [ ! -f /opt/admin/pyproject.toml ]; then +if [[ ! -f /opt/admin/pyproject.toml ]]; then echo "Error: pyproject.toml not found in /opt/admin" ls -la /opt/admin/ exit 1 @@ -29,14 +29,14 @@ poetry build # Install the wheel package using pipx # Find the built wheel file -WHEEL_FILE=$(find /opt/admin/dist -name "*.whl" -type f | head -n 1) -if [ -z "$WHEEL_FILE" ]; then +WHEEL_FILE=$(find /opt/admin/dist -name "*.whl" -type f | head -n 1) || true +if [[ -z "${WHEEL_FILE}" ]]; then echo "Error: No wheel file found in /opt/admin/dist" exit 1 fi -echo "Installing wheel: $WHEEL_FILE" -pipx install "$WHEEL_FILE" +echo "Installing wheel: ${WHEEL_FILE}" +pipx install "${WHEEL_FILE}" # Verify installation if ! command -v workspace-admin &> /dev/null; then From 3ebb8bc4752c3fb02e254d482925484bd4b6d5e4 Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Thu, 12 Feb 2026 12:20:12 +0100 Subject: [PATCH 10/14] fix mistakes in workspace admin installation --- workspaces/Dockerfile.ubuntu.noble.gnome | 2 ++ workspaces/src/install/admin/install_admin.sh | 8 +++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/workspaces/Dockerfile.ubuntu.noble.gnome b/workspaces/Dockerfile.ubuntu.noble.gnome index 78b86b6..761fc55 100644 --- a/workspaces/Dockerfile.ubuntu.noble.gnome +++ b/workspaces/Dockerfile.ubuntu.noble.gnome @@ -5,6 +5,7 @@ ENV ADMIN_SERVER_PORT=8091 \ CODE_SERVER_PORT=8054 \ HOME=/home/kasm-default-profile \ INST_DIR=${STARTUPDIR}/install \ + ADMIN_DIR=${STARTUPDIR}/admin \ JUPYTER_SERVER_PORT=8090 \ PERSISTENT_DIR=/workspace \ VNCOPTIONS="${VNCOPTIONS} -disableBasicAuth" \ @@ -19,6 +20,7 @@ ENV ADMIN_SERVER_PORT=8091 \ WORKDIR $HOME COPY ./src/install/ ${INST_DIR} +COPY ./src/admin/ ${ADMIN_DIR} RUN bash ${INST_DIR}/firefox/install_firefox.sh && \ bash ${INST_DIR}/nginx/install_nginx.sh && \ diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index a3cb158..2326e5a 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -12,9 +12,7 @@ poetry --version pipx --version # Copy admin service to /opt/admin -# INST_DIR is /dockerstartup/install, admin source is at src/admin -mkdir -p /opt/admin -cp -r "${INST_DIR}/../admin/"* /opt/admin/ +cp -r "${ADMIN_DIR}" /opt/ # Verify pyproject.toml exists if [[ ! -f /opt/admin/pyproject.toml ]]; then @@ -25,6 +23,8 @@ fi # Build the wheel package cd /opt/admin +poetry config virtualenvs.in-project true +poetry install --only main --no-root poetry build # Install the wheel package using pipx @@ -37,6 +37,8 @@ fi echo "Installing wheel: ${WHEEL_FILE}" pipx install "${WHEEL_FILE}" +pipx ensurepath +source ~/.bashrc # Verify installation if ! command -v workspace-admin &> /dev/null; then From e6d93e5ee87969b46ffad9b7adb8090d7ab3e83d Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Thu, 12 Feb 2026 16:18:40 +0100 Subject: [PATCH 11/14] improves the workspace admin code --- .github/copilot-instructions.md | 25 ++- CLAUDE.md | 209 ++++++++++++++---- workspaces/src/admin/DOCUMENTATION.md | 46 ++-- workspaces/src/admin/README.md | 5 +- workspaces/src/admin/src/admin/main.py | 141 +++++++----- .../admin/src/admin/services_template.json | 2 +- workspaces/src/admin/tests/test_main.py | 100 ++++++--- workspaces/src/startup/custom_startup.sh | 7 +- 8 files changed, 390 insertions(+), 145 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 19c7660..0958a8e 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -9,16 +9,17 @@ This repository builds a containerized virtual desktop environment (Workspace) ### `.` - Project root Contains files more related to the repository than the project artifact. This includes various linting configurations, the README, the LICENSE, the CODE_OF_CONDUCT and so on. Apart from this, it also contains: - `scripts/` - Contains various scripts useful in the development and maintanence of the repository. -- [`workspaces/`](#workspaces---main-artifact-directory) - Contains files related more to the project artifact than the repository infrastructure. +- [`workspaces/`](#workspaces-main-artifact-directory) - Contains files related more to the project artifact than the repository infrastructure. ### `workspaces/` - Main artifact directory Contains all files related to the Workspace image artifact. -- [`src/`](#workspacessrc---workspace-image-source-files) - Source files needed to build the image. +- [`src/`](#workspacessrc-workspace-image-source-files) - Source files needed to build the image. - `test/` - Files related to testing the image. - `Dockerfile.*` - The dockerfile(s) used in building the image. (must be linted with hadolint) #### `workspaces/src/` - Workspace image source files +- `admin/` - Admin service for workspace service discovery (FastAPI application) - `install/` - Files used as part of the image build process. - `resources/` - Static files injected into the image during building. - `startup/` - Files used during image startup, bootstrapping the workspace, configuring it dependent on container runtime environment variables. @@ -62,6 +63,13 @@ Contains files needed to test the integration of the workspace into existing DTa - Include docstrings for functions and modules - Use type hints where appropriate +#### Python Projects (admin service) +- Use Poetry for dependency management +- Run tests with pytest before committing +- Maintain test coverage above 75% +- Ensure all tests pass: `poetry run pytest --cov` +- Lint with pylint: `poetry run pylint src tests` + #### Dockerfile - Use hadolint for linting - Pin specific versions for base images and packages @@ -102,6 +110,14 @@ Before committing changes: 4. Update Dockerfile to call the installation script 5. Update README.md with component information +#### Modifying the Admin Service +1. Make changes to `workspaces/src/admin/src/admin/` +2. Update tests in `workspaces/src/admin/tests/` +3. Run tests: `cd workspaces/src/admin && poetry run pytest --cov` +4. Run linting: `poetry run pylint src/admin tests` +5. Update documentation in README.md and DOCUMENTATION.md +6. Rebuild and reinstall: `poetry build && poetry install` + #### Modifying Startup Behavior 1. Edit or add scripts in `workspaces/src/startup/`` 2. Ensure scripts are executable @@ -128,6 +144,11 @@ docker compose -f workspaces/test/dtaas/compose.traefik.secure.tls.yaml config # Python scripts (if any) pylint **/*.py flake8 **/*.py + +# Admin service (Python FastAPI project) +cd workspaces/src/admin +poetry run pytest --cov=admin --cov-report=term-missing +poetry run pylint src/admin tests ``` ## Best Practices diff --git a/CLAUDE.md b/CLAUDE.md index 9048def..6fcc131 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,43 +1,84 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code (claude.ai/code) when +working with code in this repository. ## Repository Overview -This repository builds a containerized virtual desktop environment (Workspace) for the DTaaS (Digital Twin as a Service) platform. The container provides multiple services through a web-based interface including KasmVNC for desktop access, Jupyter notebooks, VS Code Server, and Firefox browser. +This repository builds a containerized virtual desktop environment +(Workspace) for the DTaaS (Digital Twin as a Service) platform. The +container provides multiple services through a web-based interface +including KasmVNC for desktop access, Jupyter notebooks, VS Code +Server, and Firefox browser. ## Core Architecture ### Multi-stage Docker Build + The Dockerfile uses a multi-stage build pattern: -1. **Configure Stage**: Installs all software components and configurations -2. **Deploy Stage**: Creates a clean scratch image with all files from the configure stage + +1. **Configure Stage**: Installs all software components and + configurations +2. **Deploy Stage**: Creates a clean scratch image with all files + from the configure stage Key installed components in the workspace image: + - KasmVNC (web-based desktop/VNC) - Jupyter Server (port 8090) - VS Code Server (port 8054) +- Admin Service (port 8091) - FastAPI service for workspace service + discovery - Firefox browser - nginx (reverse proxy routing) ### Service Routing -All services are accessed through nginx, which routes traffic based on URL paths: + +All services are accessed through nginx, which routes traffic based +on URL paths: + - Jupyter: `/` - Jupyter Lab: `/lab` - VS Code: `/tools/vscode` - VNC Desktop: `/tools/vnc?path=%2Ftools%2Fvnc%2Fwebsockify` +- Admin/Services API: `/{path-prefix}/services` (service discovery + endpoint) + +The routing configuration is templated in `startup/nginx.conf` and +processed by `startup/configure_nginx.py:12-59` which substitutes +environment variables at container startup. + +### Admin Service -The routing configuration is templated in `startup/nginx.conf` and processed by `startup/configure_nginx.py:12-59` which substitutes environment variables at container startup. +The admin service (`workspaces/src/admin/`) is a FastAPI application +that provides: + +- `/services` endpoint - Returns JSON of available workspace services +- `/{path-prefix}/services` - Path-prefixed route for multi-user + deployments +- `/health` - Health check endpoint +- Command-line interface via `workspace-admin` command + +The service supports path prefixes for multi-user deployments, +configured via the `--path-prefix` CLI argument when starting the +service. ### Multi-User Deployment -The workspace supports multi-user deployments via Traefik reverse proxy. Each user gets their own workspace container with path-based routing: + +The workspace supports multi-user deployments via Traefik reverse +proxy. Each user gets their own workspace container with path-based +routing: + - `domain.com/user1/...` → User 1's workspace services - `domain.com/user2/...` → User 2's workspace services See `TRAEFIK.md:69-85` for detailed configuration and access patterns. ### Data Persistence Model -User data is persisted through Docker volume mounts to the `PERSISTENT_DIR` location (default: `/workspace`): + +User data is persisted through Docker volume mounts to the +`PERSISTENT_DIR` location (default: `/workspace`): + - `./persistent_dir/` → `/workspace` in container - This directory persists across container rebuilds and restarts - Multiple users can share data through `./persistent_dir/common` @@ -48,7 +89,9 @@ The persistence configuration is defined in `compose.yml:14-15`. ### Build and Run Workspace -**Local single-user workspace** (see `compose.yml:1-20` for configuration): +**Local single-user workspace** (see `compose.yml:1-20` for +configuration): + ```bash # Build the image docker build -t workspace:latest -f Dockerfile . @@ -64,7 +107,9 @@ docker run -d --shm-size=512m -p 8080:8080 \ ``` ### Manual Testing After Changes -After your changes build successfully, test them in a clean environment: + +After your changes build successfully, test them in a clean +environment: ```bash # Test single-user deployment @@ -73,12 +118,15 @@ docker compose down && docker compose up -d # Check logs: docker compose logs workspace # Test multi-user with Traefik -docker compose -f compose.traefik.yml down && docker compose -f compose.traefik.yml up -d -# Verify users are accessible at http://localhost/user1 and http://localhost/user2 +docker compose -f compose.traefik.yml down && \ + docker compose -f compose.traefik.yml up -d +# Verify users are accessible at http://localhost/user1 and \ +# http://localhost/user2 # Check Traefik dashboard at http://localhost:8088 (when enabled) ``` Multi-user with Traefik: + ```bash # Build first user container (if using local image) docker compose -f compose.traefik.yml build user1 @@ -91,45 +139,65 @@ docker compose -f compose.traefik.yml up -d Before submitting a PR, ensure all quality checks pass: -**Docker & Compose** (see `config/kasm_vnc/kasmvnc.yaml:1-50` and `config/jupyter/jupyter_notebook_config.py:1-20`) +#### Docker & Compose + +(see `config/kasm_vnc/kasmvnc.yaml:1-50` and +`config/jupyter/jupyter_notebook_config.py:1-20`) + ```bash hadolint Dockerfile # Dockerfile linting -for file in $(find . -name "compose*.yaml" -o -name "compose*.yml"); do docker compose -f "$file" config --quiet; done # Compose validation +for file in $(find . -name "compose*.yaml" -o -name "compose*.yml"); do \ + docker compose -f "$file" config --quiet; done # Compose validation ``` -**Shell Scripts** (see `.shellcheckrc:1-5` for configuration) +#### Shell Scripts + +(see `.shellcheckrc:1-5` for configuration) + ```bash shellcheck install/**/*.sh startup/*.sh # All shell scripts ``` -**Python Scripts** (see `.pylintrc:1-30` for configuration) +#### Python Scripts + +(see `.pylintrc:1-30` for configuration) + ```bash pylint startup/configure_nginx.py # Python linting flake8 startup/configure_nginx.py # Python style ``` -**YAML & Markdown** (see `.yamllint.yml:1-10` and `.markdownlint.yaml:1-15`) +#### YAML & Markdown + +(see `.yamllint.yml:1-10` and `.markdownlint.yaml:1-15`) + ```bash yamllint . # YAML validation markdownlint . # Markdown style ``` -See `.github/workflows/` for all automated checks (see `workspace-publish.yml:1-20` for publish workflow). +See `.github/workflows/` for all automated checks (see +`workspace-publish.yml:1-20` for publish workflow). ### Troubleshooting **Container fails to start** + ```bash docker compose logs workspace # View container logs docker ps -a # Check container status ``` **Service unreachable** -- Verify `MAIN_USER` environment variable matches the username in your URL -- Check nginx configuration with `docker exec workspace cat /etc/nginx/nginx.conf` + +- Verify `MAIN_USER` environment variable matches the username in + your URL +- Check nginx configuration with `docker exec workspace cat \ + /etc/nginx/nginx.conf` - Confirm all ports are exposed and not blocked by firewall **Linting failures** + ```bash # Run individual linters to identify specific issues shellcheck -x # Verbose shellcheck @@ -137,6 +205,7 @@ hadolint --no-color Dockerfile # Verbose hadolint ``` **Permission errors** + ```bash chmod +x install/**/*.sh startup/*.sh # Ensure scripts are executable git update-index --chmod=+x # Fix git executable bit @@ -144,34 +213,44 @@ git update-index --chmod=+x # Fix git executable bit ### Publishing Docker Images -Images are automatically published via GitHub Actions when PRs merge to main. The workflow: +Images are automatically published via GitHub Actions when PRs merge +to main. The workflow: + 1. Runs all quality checks 2. Builds the Docker image 3. Publishes to both GHCR and Docker Hub 4. Tests the published images To manually trigger publishing: + 1. Go to Actions → "Publish Workspace Docker Image" 2. Click "Run workflow" 3. Select the branch **Required Secrets** (for Docker Hub): + - `DOCKERHUB_USERNAME` - `DOCKERHUB_TOKEN` ## Adding or Modifying Components ### Installation Scripts + Location: `install//install_.sh` -When adding new software (see example patterns in `install/firefox/install_firefox.sh:1-30`): -1. Create installation script following shell format (see existing examples) +When adding new software (see example patterns in +`install/firefox/install_firefox.sh:1-30`): + +1. Create installation script following shell format (see existing + examples) 2. Pin versions explicitly 3. Clean up temporary files 4. Update `Dockerfile:32-36` to call the installation script -5. Run installation scripts as part of the Docker build process, not at container runtime +5. Run installation scripts as part of the Docker build process, not + at container runtime ### Configuration Files + Location: `config//` - Place service-specific config files here @@ -179,55 +258,108 @@ Location: `config//` - Ensure configs work in containerized environment ### Startup Scripts + Location: `startup/` -- `dtaas_shim.sh`: Runs before base image startup scripts (see `Dockerfile:61`) -- `configure_nginx.py:12-59`: Configures nginx routing with environment variables +- `dtaas_shim.sh`: Runs before base image startup scripts (see + `Dockerfile:61`) +- `configure_nginx.py:12-59`: Configures nginx routing with + environment variables - `custom_startup.sh`: Pluggable service configuration -**Python scripts**: Use type hints and error handling. See `startup/configure_nginx.py:1-10` for proper module docstring and imports. +**Python scripts**: Use type hints and error handling. See +`startup/configure_nginx.py:1-10` for proper module docstring and +imports. + +### Admin Service Development + +Location: `workspaces/src/admin/` + +The admin service is a Poetry-managed Python project with FastAPI: + +```bash +cd workspaces/src/admin + +# Install dependencies +poetry install + +# Run tests with coverage +poetry run pytest --cov=admin --cov-report=html --cov-report=term-missing + +# Run linting +poetry run pylint src/admin tests + +# Run the service locally +poetry run workspace-admin --path-prefix dtaas-user + +# List services without starting server +poetry run workspace-admin --list-services +``` + +To add new services to the workspace: + +1. Edit `src/admin/src/admin/services_template.json` +2. Add service definition with name, description, and endpoint +3. No code changes needed - template is read at runtime ## Important Environment Variables - `MAIN_USER`: Username for the workspace (default: `dtaas-user`) - `JUPYTER_SERVER_PORT`: Port for Jupyter (default: 8090) - `CODE_SERVER_PORT`: Port for VS Code Server (default: 8054) -- `NO_VNC_PORT`: Port for KasmVNC (default varies, read by `configure_nginx.py`) -- `PERSISTENT_DIR`: Directory for user data persistence (default: `/workspace`) +- `ADMIN_SERVER_PORT`: Port for Admin service (default: 8091) +- `NO_VNC_PORT`: Port for KasmVNC (default varies, read by + `configure_nginx.py`) +- `PERSISTENT_DIR`: Directory for user data persistence (default: + `/workspace`) +- `PATH_PREFIX`: Optional path prefix for admin service routes (can + be set via CLI) ## Code Quality Standards -This project enforces strict code quality - all linting must pass before merging. See `.github/copilot-instructions.md` for detailed guidelines. +This project enforces strict code quality - all linting must pass +before merging. See `.github/copilot-instructions.md` for detailed +guidelines. ### Shell Scripts + - Follow Google Shell Style Guide (see `.shellcheckrc:1-5`) - Use `set -e` for error handling - Quote variables appropriately - Include proper shebang lines -- **Example**: `install/firefox/install_firefox.sh` demonstrates proper structure +- **Example**: `install/firefox/install_firefox.sh` demonstrates + proper structure ### Python Scripts + - Follow PEP 8 style guide (see `.pylintrc`) - Include docstrings for functions and modules - Use type hints where appropriate - Handle errors gracefully -- **Example**: `startup/configure_nginx.py:1-10` shows proper docstring and imports +- **Example**: `startup/configure_nginx.py:1-10` shows proper + docstring and imports ### Dockerfile -- Pin specific versions for base images and packages (see `Dockerfile:1` uses `1.18.0`) + +- Pin specific versions for base images and packages (see + `Dockerfile:1` uses `1.18.0`) - Minimize layers by combining RUN commands - Clean up package manager caches in the same RUN command - Use OCI-compliant labels (see `Dockerfile:4-12`) ### Docker Compose + - Use version 3.x syntax - Define explicit service dependencies - Use environment variables for configuration - Include volume mounts for persistent data ### Before Committing Changes -1. Lint all modified scripts using the commands in "Before PR Checklist" -2. Build the Docker image locally: `docker build -t workspace-test:latest .` + +1. Lint all modified scripts using the commands in "Before PR + Checklist" +2. Build the Docker image locally: `docker build -t workspace-test:\ + latest .` 3. Test services start correctly and are accessible 4. Verify persistent data survives container rebuilds 5. Run both single-user and multi-user test scenarios if applicable @@ -235,13 +367,16 @@ This project enforces strict code quality - all linting must pass before merging ## Additional Documentation - **README.md:25-88** - Basic build and run instructions -- **TRAEFIK.md:1-20** - Multi-user deployment with Traefik reverse proxy -- **PUBLISHING.md:1-30** - Docker image publishing workflow and registry configuration +- **TRAEFIK.md:1-20** - Multi-user deployment with Traefik reverse + proxy +- **PUBLISHING.md:1-30** - Docker image publishing workflow and + registry configuration - **CHANGELOG.md** - Version history and release notes ## Getting Help If you encounter issues: + 1. Check the troubleshooting section above 2. Review logs: `docker compose logs workspace` 3. Inspect container: `docker exec -it workspace bash` diff --git a/workspaces/src/admin/DOCUMENTATION.md b/workspaces/src/admin/DOCUMENTATION.md index 32c5f79..fa6b3f0 100644 --- a/workspaces/src/admin/DOCUMENTATION.md +++ b/workspaces/src/admin/DOCUMENTATION.md @@ -6,8 +6,8 @@ and management capabilities for the DTaaS workspace. ## Overview The service runs on port 8091 (configurable via `ADMIN_SERVER_PORT` environment -variable) and is proxied through nginx to be accessible at the `/services` -endpoint. +variable) and is proxied through nginx. It supports path prefixes for multi-user +deployments, allowing routes to be accessible at `/{path-prefix}/services`. ## Endpoints @@ -19,7 +19,11 @@ services. **Request**: ```bash -curl http://localhost:8080/{username}/services +# Without path prefix +curl http://localhost:8080/services + +# With path prefix +curl http://localhost:8080/{path-prefix}/services ``` **Response**: Status 200 OK @@ -29,7 +33,7 @@ curl http://localhost:8080/{username}/services "desktop": { "name": "Desktop", "description": "Virtual Desktop Environment", - "endpoint": "tools/vnc?path={username}%2Ftools%2Fvnc%2Fwebsockify" + "endpoint": "tools/vnc?path={path-prefix}tools%2Fvnc%2Fwebsockify" }, "vscode": { "name": "VS Code", @@ -50,10 +54,9 @@ curl http://localhost:8080/{username}/services ``` **Notes**: -- The `{username}` placeholder in endpoints is automatically replaced with the - value of the `MAIN_USER` environment variable - Service endpoints are relative paths that should be appended to the base workspace URL +- When using path prefixes, the prefix is automatically prepended to all routes - Empty string endpoints indicate the service is available at the root path ### GET /health @@ -63,7 +66,11 @@ Health check endpoint for monitoring service availability. **Request**: ```bash +# Without path prefix curl http://localhost:8091/health + +# With path prefix +curl http://localhost:8091/{path-prefix}/health ``` **Response**: Status 200 OK @@ -81,7 +88,11 @@ Root endpoint providing service metadata and available endpoints. **Request**: ```bash +# Without path prefix curl http://localhost:8091/ + +# With path prefix +curl http://localhost:8091/{path-prefix} ``` **Response**: Status 200 OK @@ -101,12 +112,11 @@ curl http://localhost:8091/ ### Service Discovery Flow -1. User accesses `http://{domain}/{username}/services` +1. User accesses `http://{domain}/{path-prefix}/services` (or `/services` without prefix) 2. nginx receives the request and routes it to the admin service on port 8091 3. Admin service reads the `services_template.json` file -4. Template placeholders (e.g., `{MAIN_USER}`) are replaced with environment - variable values -5. JSON response is returned to the client +4. JSON response is returned to the client +5. Path prefix is configured via CLI argument when starting the service ### Components @@ -119,8 +129,8 @@ curl http://localhost:8091/ ## Environment Variables -- `MAIN_USER`: Username for the workspace (default: `dtaas-user`) - `ADMIN_SERVER_PORT`: Port for the admin service (default: `8091`) +- `PATH_PREFIX`: Optional path prefix for API routes (can also be set via CLI `--path-prefix` argument) ## Development @@ -129,7 +139,7 @@ curl http://localhost:8091/ ```bash cd workspaces/src/admin poetry install -poetry run pytest -v +poetry run pytest --cov=admin --cov-report=html --cov-report=term-missing ``` ### Code Quality and Coverage @@ -157,9 +167,8 @@ summary in the terminal. ```bash cd workspaces/src/admin -export MAIN_USER=testuser export ADMIN_SERVER_PORT=8091 -poetry run uvicorn admin.main:app --host 0.0.0.0 --port 8091 +poetry run workspace-admin --path-prefix dtaas-user ``` **As a CLI utility:** @@ -174,6 +183,9 @@ poetry run workspace-admin # Run with custom host and port poetry run workspace-admin --host 127.0.0.1 --port 9000 +# Run with path prefix for multi-user deployments +poetry run workspace-admin --path-prefix dtaas-user + # List services without starting the server poetry run workspace-admin --list-services @@ -203,11 +215,7 @@ To add a new service to the workspace: } ``` -2. If the service requires dynamic values, use placeholders like `{MAIN_USER}` - which will be substituted at runtime - -3. No code changes are required - the service automatically reads and processes - the template +2. The service automatically reads and processes the template - no code changes required ## Integration with DTaaS diff --git a/workspaces/src/admin/README.md b/workspaces/src/admin/README.md index 2e76ebe..de3fe7b 100644 --- a/workspaces/src/admin/README.md +++ b/workspaces/src/admin/README.md @@ -5,7 +5,7 @@ FastAPI service for workspace service discovery and management. ## Features - `/services` endpoint - Returns JSON list of available workspace services -- Environment-aware configuration (uses MAIN_USER environment variable) +- Path prefix support for multi-user deployments - Command-line interface for standalone operation ## Running @@ -27,6 +27,9 @@ poetry run workspace-admin # Run with custom port poetry run workspace-admin --port 9000 +# Run with path prefix for multi-user setup +poetry run workspace-admin --path-prefix dtaas-user + # List services without starting the server poetry run workspace-admin --list-services diff --git a/workspaces/src/admin/src/admin/main.py b/workspaces/src/admin/src/admin/main.py index fd04893..d85b8ea 100644 --- a/workspaces/src/admin/src/admin/main.py +++ b/workspaces/src/admin/src/admin/main.py @@ -13,20 +13,79 @@ from typing import Dict, Any import uvicorn -from fastapi import FastAPI +from fastapi import FastAPI, APIRouter from fastapi.responses import JSONResponse -app = FastAPI( - title="Workspace Admin Service", - description="Service discovery and management for DTaaS workspace", - version="0.1.0" -) +def create_app(path_prefix: str = "") -> FastAPI: + """ + Create and configure the FastAPI application. + + Args: + path_prefix: Optional path prefix for all routes (e.g., "user1") + + Returns: + Configured FastAPI application instance. + """ + # Clean up path prefix + if path_prefix: + path_prefix = path_prefix.strip("/") + if path_prefix: + path_prefix = f"/{path_prefix}" + else: + path_prefix = "" + + # Create the FastAPI app + fastapi_app = FastAPI( + title="Workspace Admin Service", + description="Service discovery and management for DTaaS workspace", + version="0.1.0" + ) + + # Create router for our endpoints + router = APIRouter() + + @router.get("/") + async def root() -> Dict[str, Any]: + """Root endpoint providing service information.""" + return { + "service": "Workspace Admin Service", + "version": "0.1.0", + "endpoints": { + "/services": "Get list of available workspace services", + "/health": "Health check endpoint" + } + } + + @router.get("/services") + async def get_services() -> JSONResponse: + """ + Get list of available workspace services. + + Returns: + JSONResponse containing service information. + """ + services = load_services(os.environ["PATH_PREFIX"] if "PATH_PREFIX" in os.environ else "") + return JSONResponse(content=services) + + @router.get("/health") + async def health_check() -> Dict[str, str]: + """Health check endpoint.""" + return {"status": "healthy"} + + # Include router with optional prefix + fastapi_app.include_router(router, prefix=path_prefix) + + return fastapi_app + + +# Create default app instance +app = create_app() # Path to services template SERVICES_TEMPLATE_PATH = Path(__file__).parent / "services_template.json" -def load_services() -> Dict[str, Any]: +def load_services(path_prefix: str = "") -> Dict[str, Any]: """ Load services from template and substitute environment variables. @@ -38,50 +97,16 @@ def load_services() -> Dict[str, Any]: with open(SERVICES_TEMPLATE_PATH, 'r', encoding='utf-8') as f: services = json.load(f) - # Get MAIN_USER from environment, default to 'dtaas-user' - main_user = os.getenv('MAIN_USER', 'dtaas-user') - - # Substitute {MAIN_USER} in endpoint values - for _service_id, service_info in services.items(): + # Substitute {PATH_PREFIX} in endpoint values + for _, service_info in services.items(): if 'endpoint' in service_info: service_info['endpoint'] = service_info['endpoint'].replace( - '{MAIN_USER}', main_user + '{PATH_PREFIX}', path_prefix ) return services -@app.get("/") -async def root() -> Dict[str, Any]: - """Root endpoint providing service information.""" - return { - "service": "Workspace Admin Service", - "version": "0.1.0", - "endpoints": { - "/services": "Get list of available workspace services", - "/health": "Health check endpoint" - } - } - - -@app.get("/services") -async def get_services() -> JSONResponse: - """ - Get list of available workspace services. - - Returns: - JSONResponse containing service information. - """ - services = load_services() - return JSONResponse(content=services) - - -@app.get("/health") -async def health_check() -> Dict[str, str]: - """Health check endpoint.""" - return {"status": "healthy"} - - def cli(): """ Command-line interface for the workspace admin service. @@ -109,6 +134,11 @@ def cli(): "(default: $ADMIN_SERVER_PORT or 8091)" ) ) + parser.add_argument( + "--path-prefix", + default=os.getenv("PATH_PREFIX", "dtaas-user"), + help="Path prefix for API routes (e.g., 'dtaas-user' for routes at /dtaas-user/services)" + ) parser.add_argument( "--reload", action="store_true", @@ -127,22 +157,33 @@ def cli(): args = parser.parse_args() + # Set up path prefix + path_prefix = args.path_prefix.strip("/") + if path_prefix: + os.environ["PATH_PREFIX"] = path_prefix + prefix_display = f"/{path_prefix}" + else: + prefix_display = "" + if args.list_services: # Just list services and exit - services = load_services() + services = load_services(path_prefix) print(json.dumps(services, indent=2)) sys.exit(0) # Start the server print(f"Starting Workspace Admin Service on {args.host}:{args.port}") - print(f"MAIN_USER: {os.getenv('MAIN_USER', 'dtaas-user')}") print("Service endpoints:") - print(f" - http://{args.host}:{args.port}/services") - print(f" - http://{args.host}:{args.port}/health") - print(f" - http://{args.host}:{args.port}/") + print(f" - http://{args.host}:{args.port}{prefix_display}/services") + print(f" - http://{args.host}:{args.port}{prefix_display}/health") + print(f" - http://{args.host}:{args.port}{prefix_display}/") + + # Recreate app with path prefix + global app # pylint: disable=global-statement + app = create_app(path_prefix) uvicorn.run( - "admin.main:app", + app, host=args.host, port=args.port, reload=args.reload diff --git a/workspaces/src/admin/src/admin/services_template.json b/workspaces/src/admin/src/admin/services_template.json index c076735..a3f68b7 100644 --- a/workspaces/src/admin/src/admin/services_template.json +++ b/workspaces/src/admin/src/admin/services_template.json @@ -2,7 +2,7 @@ "desktop": { "name": "Desktop", "description": "Virtual Desktop Environment", - "endpoint": "tools/vnc?path={MAIN_USER}%2Ftools%2Fvnc%2Fwebsockify" + "endpoint": "tools/vnc?path={PATH_PREFIX}%2Ftools%2Fvnc%2Fwebsockify" }, "vscode": { "name": "VS Code", diff --git a/workspaces/src/admin/tests/test_main.py b/workspaces/src/admin/tests/test_main.py index 8d8754a..40a712a 100644 --- a/workspaces/src/admin/tests/test_main.py +++ b/workspaces/src/admin/tests/test_main.py @@ -9,19 +9,21 @@ import pytest from fastapi.testclient import TestClient -from admin.main import app, load_services, SERVICES_TEMPLATE_PATH +from admin.main import create_app, load_services, SERVICES_TEMPLATE_PATH @pytest.fixture(name='test_client') def fixture_test_client(): """Create a test client for the FastAPI app.""" + app = create_app() return TestClient(app) -@pytest.fixture(name='setup_mock_main_user') -def fixture_mock_main_user(monkeypatch): - """Set up mock MAIN_USER environment variable.""" - monkeypatch.setenv('MAIN_USER', 'testuser') +@pytest.fixture(name='test_client_with_prefix') +def fixture_test_client_with_prefix(): + """Create a test client for the FastAPI app with path prefix.""" + app = create_app(path_prefix="user1") + return TestClient(app) def test_root_endpoint(test_client): @@ -42,10 +44,8 @@ def test_health_check(test_client): assert data["status"] == "healthy" -def test_services_endpoint(test_client, setup_mock_main_user): +def test_services_endpoint(test_client): """Test the /services endpoint returns service list.""" - # setup_mock_main_user fixture sets MAIN_USER='testuser' - _ = setup_mock_main_user # Mark as intentionally used response = test_client.get("/services") assert response.status_code == 200 @@ -64,36 +64,18 @@ def test_services_endpoint(test_client, setup_mock_main_user): assert "endpoint" in desktop assert desktop["name"] == "Desktop" - # Check that MAIN_USER is substituted correctly - assert "testuser" in desktop["endpoint"] - assert "{MAIN_USER}" not in desktop["endpoint"] - - -def test_services_endpoint_default_user(test_client, monkeypatch): - """Test /services endpoint with default MAIN_USER.""" - # Remove MAIN_USER from environment - monkeypatch.delenv('MAIN_USER', raising=False) +def test_services_endpoint_returns_all_services(test_client): + """Test /services endpoint returns all defined services.""" response = test_client.get("/services") assert response.status_code == 200 services = response.json() - desktop = services["desktop"] - - # Should use default 'dtaas-user' - assert "dtaas-user" in desktop["endpoint"] - -def test_load_services_substitutes_main_user(setup_mock_main_user): - """Test that load_services correctly substitutes MAIN_USER.""" - # setup_mock_main_user fixture sets MAIN_USER='testuser' - _ = setup_mock_main_user # Mark as intentionally used - services = load_services() - - # Check that {MAIN_USER} is replaced with 'testuser' - desktop_endpoint = services["desktop"]["endpoint"] - assert "testuser" in desktop_endpoint - assert "{MAIN_USER}" not in desktop_endpoint + # Verify all expected services are present + required_services = ["desktop", "vscode", "notebook", "lab"] + for service_id in required_services: + assert service_id in services def test_load_services_preserves_structure(): @@ -128,7 +110,6 @@ def test_services_template_valid_json(): def test_cli_list_services(monkeypatch, capsys): """Test CLI --list-services flag.""" import sys - monkeypatch.setenv('MAIN_USER', 'clitest') monkeypatch.setattr(sys, 'argv', ['workspace-admin', '--list-services']) from admin.main import cli @@ -140,7 +121,7 @@ def test_cli_list_services(monkeypatch, capsys): captured = capsys.readouterr() output = json.loads(captured.out) assert "desktop" in output - assert "clitest" in output["desktop"]["endpoint"] + assert "vscode" in output def test_cli_version(monkeypatch): @@ -194,3 +175,54 @@ def test_health_endpoint_returns_json(test_client): """Test that health endpoint returns JSON.""" response = test_client.get("/health") assert response.headers["content-type"] == "application/json" + + +def test_root_endpoint_with_prefix(test_client_with_prefix): + """Test root endpoint with path prefix.""" + response = test_client_with_prefix.get("/user1/") + assert response.status_code == 200 + data = response.json() + assert data["service"] == "Workspace Admin Service" + + +def test_services_endpoint_with_prefix(test_client_with_prefix): + """Test services endpoint with path prefix.""" + response = test_client_with_prefix.get("/user1/services") + assert response.status_code == 200 + services = response.json() + assert "desktop" in services + + +def test_health_endpoint_with_prefix(test_client_with_prefix): + """Test health endpoint with path prefix.""" + response = test_client_with_prefix.get("/user1/health") + assert response.status_code == 200 + data = response.json() + assert data["status"] == "healthy" + + +def test_path_prefix_not_accessible_without_prefix(test_client_with_prefix): + """Test that endpoints are not accessible without prefix when prefix is set.""" + # When app has prefix, root-level routes should not work + response = test_client_with_prefix.get("/services") + assert response.status_code == 404 + + +def test_create_app_with_various_prefixes(): + """Test creating app with different prefix formats.""" + # Test with leading/trailing slashes - routes should work + app1 = create_app("/user1/") + client1 = TestClient(app1) + assert client1.get("/user1/health").status_code == 200 + + app2 = create_app("user1") + client2 = TestClient(app2) + assert client2.get("/user1/health").status_code == 200 + + app3 = create_app("") + client3 = TestClient(app3) + assert client3.get("/health").status_code == 200 + + app4 = create_app("/") + client4 = TestClient(app4) + assert client4.get("/health").status_code == 200 diff --git a/workspaces/src/startup/custom_startup.sh b/workspaces/src/startup/custom_startup.sh index c8086a0..dc37372 100755 --- a/workspaces/src/startup/custom_startup.sh +++ b/workspaces/src/startup/custom_startup.sh @@ -45,7 +45,12 @@ function start_vscode_server { } function start_admin_server { - workspace-admin --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" & + local path_prefix="${MAIN_USER:-}" + if [[ -n "${path_prefix}" ]]; then + workspace-admin --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" --path-prefix "${path_prefix}" & + else + workspace-admin --host 0.0.0.0 --port "${ADMIN_SERVER_PORT}" & + fi DTAAS_PROCS['admin']=$! } From e68deeea31f30f758828665dc30760e641c0974b Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Thu, 12 Feb 2026 17:42:46 +0100 Subject: [PATCH 12/14] Updates changelog and adds workflow file for admin package --- .github/workflows/workspace-admin.yml | 80 +++++++++++++++++++ CHANGELOG.md | 73 +++++++++++++++++ workspaces/src/admin/src/admin/main.py | 3 +- workspaces/src/install/admin/install_admin.sh | 1 + 4 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/workspace-admin.yml diff --git a/.github/workflows/workspace-admin.yml b/.github/workflows/workspace-admin.yml new file mode 100644 index 0000000..f4e251c --- /dev/null +++ b/.github/workflows/workspace-admin.yml @@ -0,0 +1,80 @@ +name: Workspace Admin Service + +on: + push: + paths: + - 'workspaces/src/admin/**' + - '.github/workflows/workspace-admin.yml' + pull_request: + paths: + - 'workspaces/src/admin/**' + - '.github/workflows/workspace-admin.yml' + workflow_dispatch: + +jobs: + test-and-build: + name: Test and Build Workspace Admin + runs-on: ubuntu-latest + permissions: + contents: read # Required for checking out the code + actions: write # Required for uploading artifacts + + defaults: + run: + working-directory: workspaces/src/admin + + steps: + - name: Checkout code + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 + + - name: Set up Python + uses: actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 + with: + python-version: '3.12' + + - name: Install Poetry + run: | + curl -sSL https://install.python-poetry.org | python3 - + echo "$HOME/.local/bin" >> $GITHUB_PATH + + - name: Verify Poetry installation + run: poetry --version + + - name: Configure Poetry + run: | + poetry config virtualenvs.in-project true + poetry config virtualenvs.create true + + - name: Install dependencies + run: poetry install --no-interaction --no-root + + - name: Install project + run: poetry install --no-interaction + + - name: Run pylint (min score 9.0) + run: | + poetry run pylint src/admin tests --rcfile=${GITHUB_WORKSPACE}/.pylintrc --fail-under=9.0 + + - name: Run pytest with coverage + run: | + poetry run pytest --cov=admin --cov-report=xml --cov-report=term-missing + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@c23a129e932ebdcb56ca1565c68c6abdbf173769 + with: + files: workspaces/src/admin/coverage.xml + flags: workspace-admin-tests + fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} + + - name: Build package + run: poetry build + + - name: Upload build artifacts + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f + with: + name: workspace-admin + path: | + workspaces/src/admin/dist/*.whl + workspaces/src/admin/dist/*.tar.gz + retention-days: 7 diff --git a/CHANGELOG.md b/CHANGELOG.md index 97f5937..b87b0fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,79 @@ The main changes made so far are listed here. +## Week of 10-Feb-2026 + +### Added +* Admin service FastAPI application with `/services` endpoint for service discovery +* CLI interface for admin service (`workspace-admin` command) with options for listing services, host, port, and reload +* Comprehensive test suite for admin service with 84% coverage (14 tests) +* Detailed documentation (DOCUMENTATION.md and README.md) for admin service + +### Changed +* Admin service installation changed from poetry run to wheel package + pipx installation +* Poetry installation now uses install.python-poetry.org installer with virtualenvs configured in-project + +### Fixed +* ShellCheck issues in install_admin.sh (test syntax, variable bracing, error handling) +* Docker build path from `${INST_DIR}/../../admin` to `${INST_DIR}/../admin` +* Linting issues: removed whitespace, unused imports, fixed f-strings +* Type hints for FastAPI response endpoints +* Mistakes in workspace admin installation + +## Week of 03-Feb-2026 + +### Changed +* Workspace Docker image now published to `intocps/workspace` registry + +## Week of 20-Jan-2026 + +### Changed +* Docker labels moved from build stage to deploy stage in Dockerfile + +## Week of 13-Jan-2026 + +### Added +* TLS/HTTPS support with OAuth2 authentication for production deployments +* New Docker compose files for TLS configuration +* Self-signed certificate generation support +* `.gitattributes` file specifying LF line endings for all non-binary files + +### Changed +* Workspace name changed from `workspace-nouveau` to `workspace` +* GitHub Actions updated to reflect new image location +* Traefik-forward-auth version updated to fix endless redirect loop bug +* Consolidated environment file setup between OAuth2 and TLS features + +### Fixed +* Docker image publish problems +* Regular user set for login user + +## Week of 06-Jan-2026 + +### Added +* Automated Docker image publishing to GHCR and Docker Hub +* OCI labels to Dockerfile for better metadata +* PUBLISHING.md documentation for Docker image publishing workflow +* CLAUDE.md file for Claude code use + +### Changed +* Main image name from "workspace-nouveau" to "workspace" + +## Week of 16-Dec-2025 + +### Added +* Traefik reverse proxy integration for multi-user deployments +* OAuth2-secured multi-user deployment with traefik-forward-auth and DTaaS web client integration +* Strict linting enforcement in GitHub Actions workflows +* New project structure with dedicated DTaaS testing directory +* Configuration and certificates organization in dedicated DTaaS directory + +### Fixed +* Resolved Copilot review comments from PR #10 + +### Changed +* Improved documentation for multi-user deployments + ## 15-Dec-2025 * Adds both ml-workspace and workspace in one docker compose diff --git a/workspaces/src/admin/src/admin/main.py b/workspaces/src/admin/src/admin/main.py index d85b8ea..fcfb930 100644 --- a/workspaces/src/admin/src/admin/main.py +++ b/workspaces/src/admin/src/admin/main.py @@ -16,12 +16,13 @@ from fastapi import FastAPI, APIRouter from fastapi.responses import JSONResponse + def create_app(path_prefix: str = "") -> FastAPI: """ Create and configure the FastAPI application. Args: - path_prefix: Optional path prefix for all routes (e.g., "user1") + path_prefix: Optional path prefix for all routes (e.g., "dtaas-user") Returns: Configured FastAPI application instance. diff --git a/workspaces/src/install/admin/install_admin.sh b/workspaces/src/install/admin/install_admin.sh index 2326e5a..bfaad7b 100755 --- a/workspaces/src/install/admin/install_admin.sh +++ b/workspaces/src/install/admin/install_admin.sh @@ -38,6 +38,7 @@ fi echo "Installing wheel: ${WHEEL_FILE}" pipx install "${WHEEL_FILE}" pipx ensurepath +# shellcheck disable=SC1090 source ~/.bashrc # Verify installation From 7ecab5868a20d8131d4775fed4d0bb8f5870b8f5 Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Thu, 12 Feb 2026 17:48:39 +0100 Subject: [PATCH 13/14] fix docs --- workspaces/src/admin/DOCUMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/workspaces/src/admin/DOCUMENTATION.md b/workspaces/src/admin/DOCUMENTATION.md index fa6b3f0..2adbcdd 100644 --- a/workspaces/src/admin/DOCUMENTATION.md +++ b/workspaces/src/admin/DOCUMENTATION.md @@ -33,7 +33,7 @@ curl http://localhost:8080/{path-prefix}/services "desktop": { "name": "Desktop", "description": "Virtual Desktop Environment", - "endpoint": "tools/vnc?path={path-prefix}tools%2Fvnc%2Fwebsockify" + "endpoint": "tools/vnc?path={PATH_PREFIX}%2Ftools%2Fvnc%2Fwebsockify" }, "vscode": { "name": "VS Code", From 1bbaa546f521c2128b726c93486e8adbe91a607c Mon Sep 17 00:00:00 2001 From: prasadtalasila Date: Thu, 12 Feb 2026 17:54:49 +0100 Subject: [PATCH 14/14] update docs for compose files --- workspaces/test/dtaas/SINGLE_USER.md | 42 ++++++++++++++++++++++++ workspaces/test/dtaas/TRAEFIK.md | 43 +++++++++++++++++++++++++ workspaces/test/dtaas/TRAEFIK_SECURE.md | 43 +++++++++++++++++++++++++ workspaces/test/dtaas/TRAEFIK_TLS.md | 43 +++++++++++++++++++++++++ 4 files changed, 171 insertions(+) diff --git a/workspaces/test/dtaas/SINGLE_USER.md b/workspaces/test/dtaas/SINGLE_USER.md index 8411f34..11c5cad 100644 --- a/workspaces/test/dtaas/SINGLE_USER.md +++ b/workspaces/test/dtaas/SINGLE_USER.md @@ -87,6 +87,48 @@ Once all services are running, access the workspaces through Traefik: - **Jupyter Notebook**: `http://localhost/user1` - **Jupyter Lab**: `http://localhost/user1/lab` +### Service Discovery + +The workspace provides a `/services` endpoint that returns a JSON list of +available services. This enables dynamic service discovery for frontend +applications. + +**Example**: Get service list for user1 + +```bash +curl http://localhost/user1/services +``` + +**Response**: + +```json +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path=user1%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} +``` + +The endpoint values are dynamically populated with the user's username from the +`MAIN_USER` environment variable. + ### Use of ENV file If `.env` file is used for docker compose command, remember to: diff --git a/workspaces/test/dtaas/TRAEFIK.md b/workspaces/test/dtaas/TRAEFIK.md index 34bdf4b..c8ae7fd 100644 --- a/workspaces/test/dtaas/TRAEFIK.md +++ b/workspaces/test/dtaas/TRAEFIK.md @@ -64,6 +64,49 @@ Once all services are running, access the workspaces through Traefik: - **Jupyter Notebook**: `http://localhost/user1` - **Jupyter Lab**: `http://localhost/user1/lab` +#### Service Discovery + +The workspace provides a `/services` endpoint that returns a JSON list of +available services. This enables dynamic service discovery for frontend +applications. + +**Example**: Get service list for user1 + +```bash +curl http://localhost/user1/services +``` + +**Response**: + +```json +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path=user1%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} +``` + +The endpoint values are dynamically populated with the user's username from the +`MAIN_USER` environment variable. This variable corresponds to `USERNAME1` of +`.env` file. + ### User2 Workspace (ml-workspace-minimal) - **VNC Desktop**: `http://localhost/user2/tools/vnc/?password=vncpassword` diff --git a/workspaces/test/dtaas/TRAEFIK_SECURE.md b/workspaces/test/dtaas/TRAEFIK_SECURE.md index d342f57..0f76032 100644 --- a/workspaces/test/dtaas/TRAEFIK_SECURE.md +++ b/workspaces/test/dtaas/TRAEFIK_SECURE.md @@ -84,6 +84,49 @@ All endpoints require authentication: - **Jupyter Notebook**: `http://localhost/user1` - **Jupyter Lab**: `http://localhost/user1/lab` +#### Service Discovery + +The workspace provides a `/services` endpoint that returns a JSON list of +available services. This enables dynamic service discovery for frontend +applications. + +**Example**: Get service list for user1 + +```bash +curl http://localhost/user1/services +``` + +**Response**: + +```json +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path=user1%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} +``` + +The endpoint values are dynamically populated with the user's username from the +`MAIN_USER` environment variable. This variable corresponds to `USERNAME1` of +`.env` file. + ### User2 Workspace (ml-workspace-minimal) All endpoints require authentication: diff --git a/workspaces/test/dtaas/TRAEFIK_TLS.md b/workspaces/test/dtaas/TRAEFIK_TLS.md index 92ab0d0..fe2d1cf 100644 --- a/workspaces/test/dtaas/TRAEFIK_TLS.md +++ b/workspaces/test/dtaas/TRAEFIK_TLS.md @@ -72,6 +72,49 @@ Once all services are running, access the workspaces through Traefik with HTTPS: - **Jupyter Notebook**: `https://yourdomain.com/user1` - **Jupyter Lab**: `https://yourdomain.com/user1/lab` +#### Service Discovery + +The workspace provides a `/services` endpoint that returns a JSON list of +available services. This enables dynamic service discovery for frontend +applications. + +**Example**: Get service list for user1 + +```bash +curl https://yourdomain.com/user1/services +``` + +**Response**: + +```json +{ + "desktop": { + "name": "Desktop", + "description": "Virtual Desktop Environment", + "endpoint": "tools/vnc?path=user1%2Ftools%2Fvnc%2Fwebsockify" + }, + "vscode": { + "name": "VS Code", + "description": "VS Code IDE", + "endpoint": "tools/vscode" + }, + "notebook": { + "name": "Jupyter Notebook", + "description": "Jupyter Notebook", + "endpoint": "" + }, + "lab": { + "name": "Jupyter Lab", + "description": "Jupyter Lab IDE", + "endpoint": "lab" + } +} +``` + +The endpoint values are dynamically populated with the user's username from the +`MAIN_USER` environment variable. This variable corresponds to `USERNAME1` of +`.env` file. + ### User2 Workspace (ml-workspace-minimal) - **VNC Desktop**: `https://yourdomain.com/user2/tools/vnc/?password=vncpassword`