{"id":45069957,"url":"https://github.com/tmunzer/mistapi_python","last_synced_at":"2026-03-18T00:22:06.584Z","repository":{"id":65384998,"uuid":"588474309","full_name":"tmunzer/mistapi_python","owner":"tmunzer","description":"Python package to simplify the Mist System APIs usage","archived":false,"fork":false,"pushed_at":"2026-01-28T12:19:37.000Z","size":1744,"stargazers_count":14,"open_issues_count":1,"forks_count":2,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-01-29T04:42:38.349Z","etag":null,"topics":["api","mist","python","python-package","script"],"latest_commit_sha":null,"homepage":"https://pypi.org/project/mistapi/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tmunzer.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-01-13T07:55:58.000Z","updated_at":"2026-01-28T12:19:41.000Z","dependencies_parsed_at":"2023-02-12T04:03:01.813Z","dependency_job_id":"233759df-eae3-4da5-bd8b-019a3404113c","html_url":"https://github.com/tmunzer/mistapi_python","commit_stats":{"total_commits":90,"total_committers":2,"mean_commits":45.0,"dds":0.3666666666666667,"last_synced_commit":"7d9dd88bbd1b2a67a2c0140f541c28753172a8be"},"previous_names":[],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/tmunzer/mistapi_python","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmunzer%2Fmistapi_python","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmunzer%2Fmistapi_python/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmunzer%2Fmistapi_python/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmunzer%2Fmistapi_python/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tmunzer","download_url":"https://codeload.github.com/tmunzer/mistapi_python/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tmunzer%2Fmistapi_python/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29612521,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T10:52:55.328Z","status":"ssl_error","status_checked_at":"2026-02-19T10:52:26.323Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["api","mist","python","python-package","script"],"created_at":"2026-02-19T12:06:31.541Z","updated_at":"2026-03-18T00:22:06.563Z","avatar_url":"https://github.com/tmunzer.png","language":"Python","funding_links":[],"categories":[],"sub_categories":[],"readme":"# MISTAPI - Python Package for Mist API\n\n[![PyPI version](https://img.shields.io/pypi/v/mistapi.svg)](https://pypi.org/project/mistapi/)\n[![Python versions](https://img.shields.io/pypi/pyversions/mistapi.svg)](https://pypi.org/project/mistapi/)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\n\nA comprehensive Python package to interact with the Mist Cloud APIs, built from the official [Mist OpenAPI specifications](https://www.juniper.net/documentation/us/en/software/mist/api/http/getting-started/how-to-get-started).\n\n---\n\n## Table of Contents\n\n- [Features](#features)\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Configuration](#configuration)\n    - [Using Environment File](#using-environment-file)\n    - [Environment Variables](#environment-variables)\n- [Authentication](#authentication)\n    - [Interactive Authentication](#interactive-authentication)\n    - [Environment File Authentication](#environment-file-authentication)\n    - [HashiCorp Vault Authentication](#hashicorp-vault-authentication)\n    - [System Keyring Authentication](#system-keyring-authentication)\n    - [Direct Parameter Authentication](#direct-parameter-authentication)\n- [API Requests Usage](#api-requests-usage)\n    - [Basic API Calls](#basic-api-calls)\n    - [Error Handling](#error-handling)\n    - [Log Sanitization](#log-sanitization)\n    - [Getting Help](#getting-help)\n    - [CLI Helper Functions](#cli-helper-functions)\n    - [Pagination](#pagination-support)\n    - [Examples](#examples)\n- [WebSocket Streaming](#websocket-streaming)\n    - [Connection Parameters](#connection-parameters)\n    - [Callbacks](#callbacks)\n    - [Available Channels](#available-channels)\n    - [Usage Patterns](#usage-patterns)\n- [Async Usage](#async-usage)\n    - [Running API Calls Asynchronously](#running-api-calls-asynchronously)\n    - [Concurrent API Calls](#concurrent-api-calls)\n    - [Combining with Device Utilities](#combining-with-device-utilities)\n- [Device Utilities](#device-utilities)\n    - [Supported Devices](#supported-devices)\n    - [Usage](#device-utilities-usage)\n    - [UtilResponse Object](#utilresponse-object)\n- [Development](#development-and-testing)\n- [Contributing](#contributing)\n- [License](#license)\n- [Links](#links)\n\n---\n\n## Features\n\n### Supported Mist Clouds\nSupport for all Mist cloud instances worldwide:\n- **APAC**: api.ac5.mist.com, api.gc5.mist.com, api.gc7.mist.com\n- **EMEA**: api.eu.mist.com, api.gc3.mist.com, api.ac6.mist.com, api.gc6.mist.com\n- **Global**: api.mist.com, api.gc1.mist.com, api.ac2.mist.com, api.gc2.mist.com, api.gc4.mist.com\n\n### Authentication\n- API token and username/password authentication (with 2FA support)\n- Environment variable configuration (`.env` file support)\n- HashiCorp Vault integration for secure credential storage\n- System keyring integration (macOS Keychain, Windows Credential Locker, etc.)\n- Interactive CLI prompts for credentials when needed\n\n### Core Features\n- **Complete API Coverage**: Auto-generated from OpenAPI specs\n- **Async Support**: Run any API call asynchronously with `mistapi.arun()` — no changes to existing code\n- **Automatic Pagination**: Built-in support for paginated responses\n- **WebSocket Streaming**: Real-time event streaming for devices, clients, and location data\n- **Device Diagnostics**: High-level, non-blocking utilities for ping, traceroute, ARP, BGP, OSPF, and more\n- **Error Handling**: Detailed error responses and logging\n- **Proxy Support**: HTTP/HTTPS proxy configuration\n- **Log Sanitization**: Automatic redaction of sensitive data in logs\n\n\n---\n\n## Installation\n\n### Basic Installation\n\n```bash\n# Linux/macOS\npython3 -m pip install mistapi\n\n# Windows\npy -m pip install mistapi\n```\n\n### Upgrade to Latest Version\n\n```bash\n# Linux/macOS\npython3 -m pip install --upgrade mistapi\n\n# Windows\npy -m pip install --upgrade mistapi\n```\n\n### Installation with uv\n\n[uv](https://docs.astral.sh/uv/) is a fast Python package manager:\n\n```bash\n# Install in current project\nuv add mistapi\n\n# Or run directly without installing\nuv run --with mistapi python my_script.py\n```\n\n### Development Installation\n\n```bash\n# With pip\npip install -e \".[dev]\"\n\n# With uv\nuv sync\n```\n\n### Requirements\n- Python 3.10 or higher\n- Dependencies: `requests`, `python-dotenv`, `tabulate`, `deprecation`, `hvac`, `keyring`, `websocket-client`\n\n---\n\n## Quick Start\n\n```python\nimport mistapi\n\n# Initialize session\napisession = mistapi.APISession()\n\n# Authenticate (interactive prompt if credentials not configured)\napisession.login()\n\n# Use the API - Get device models\ndevice_models = mistapi.api.v1.const.device_models.listDeviceModels(apisession)\nprint(f\"Found {len(device_models.data)} device models\")\n\n# Interactive organization selection\norg_id = mistapi.cli.select_org(apisession)[0]\n\n# Get organization information\norg_info = mistapi.api.v1.orgs.orgs.getOrg(apisession, org_id)\nprint(f\"Organization: {org_info.data['name']}\")\n```\n\n---\n\n## Configuration\n\nConfiguration is optional - you can pass all parameters directly to `APISession`. However, using an `.env` file simplifies credential management.\n\n### Using Environment File\n\n```python\nimport mistapi\napisession = mistapi.APISession(env_file=\"~/.mist_env\")\n```\n\n### Environment Variables\n\nCreate a `.env` file with your credentials:\n\n```bash\nMIST_HOST=api.mist.com\nMIST_APITOKEN=your_api_token_here\n\n# Alternative to API token\n# MIST_USER=your_email@example.com\n# MIST_PASSWORD=your_password\n\n# Proxy configuration\n# HTTPS_PROXY=http://user:password@myproxy.com:3128\n\n# Logging configuration\n# CONSOLE_LOG_LEVEL=20  # 0=Disabled, 10=Debug, 20=Info, 30=Warning, 40=Error, 50=Critical\n# LOGGING_LOG_LEVEL=10\n```\n\n### Configuration Options\n\n| Environment Variable | APISession Parameter | Type | Default | Description |\n|---|---|---|---|---|\n| `MIST_HOST` | `host` | string | None | Mist Cloud API endpoint (e.g., `api.mist.com`) |\n| `MIST_APITOKEN` | `apitoken` | string | None | API Token for authentication (recommended) |\n| `MIST_USER` | `email` | string | None | Username/email for authentication |\n| `MIST_PASSWORD` | `password` | string | None | Password for authentication |\n| `MIST_KEYRING_SERVICE` | `keyring_service` | string | None | System keyring service name |\n| `MIST_VAULT_URL` | `vault_url` | string | None | HashiCorp Vault URL |\n| `MIST_VAULT_PATH` | `vault_path` | string | None | Path to secret in Vault |\n| `MIST_VAULT_MOUNT_POINT` | `vault_mount_point` | string | None | Vault mount point |\n| `MIST_VAULT_TOKEN` | `vault_token` | string | None | Vault authentication token |\n| `CONSOLE_LOG_LEVEL` | `console_log_level` | int | 20 | Console log level (0-50) |\n| `LOGGING_LOG_LEVEL` | `logging_log_level` | int | 10 | File log level (0-50) |\n| `HTTPS_PROXY` | `https_proxy` | string | None | HTTP/HTTPS proxy URL |\n| | `env_file` | str | None | Path to `.env` file |\n\n\n---\n\n## Authentication\n\nThe `login()` function must be called to authenticate. The package supports multiple authentication methods.\n\n### 1. Interactive Authentication\n\nIf credentials are not configured, you'll be prompted interactively:\n\n**Cloud Selection:**\n```\n----------------------------- Mist Cloud Selection -----------------------------\n\n0) APAC 01 (host: api.ac5.mist.com)\n1) EMEA 01 (host: api.eu.mist.com)\n2) Global 01 (host: api.mist.com)\n...\n\nSelect a Cloud (0 to 10, or q to exit):\n```\n\n**Credential Prompt:**\n```\n--------------------------- Login/Pwd authentication ---------------------------\n\nLogin: user@example.com\nPassword: \n[  INFO   ] Authentication successful!\n\nTwo Factor Authentication code required: 123456\n[  INFO   ] 2FA authentication succeeded\n\n-------------------------------- Authenticated ---------------------------------\nWelcome Thomas Munzer!\n```\n\n### 2. Environment File Authentication\n\n```python\nimport mistapi\n\napisession = mistapi.APISession(env_file=\"~/.mist_env\")\napisession.login()\n\n# Output:\n# -------------------------------- Authenticated ---------------------------------\n# Welcome Thomas Munzer!\n```\n\n### 3. HashiCorp Vault Authentication\n\n```python\nimport mistapi\n\napisession = mistapi.APISession(\n    vault_url=\"https://vault.mycompany.com:8200\",\n    vault_path=\"secret/data/mist/credentials\",\n    vault_token=\"s.xxxxxxx\"\n)\napisession.login()\n```\n\n### 4. System Keyring Authentication\n\n```python\nimport mistapi\n\napisession = mistapi.APISession(keyring_service=\"my_mist_service\")\napisession.login()\n```\n\n**Note:** The keyring must contain: `MIST_HOST`, `MIST_APITOKEN` (or `MIST_USER` and `MIST_PASSWORD`)\n\n### 5. Direct Parameter Authentication\n\n```python\nimport mistapi\n\napisession = mistapi.APISession(\n    host=\"api.mist.com\",\n    apitoken=\"your_token_here\"\n)\napisession.login()\n```\n\n\n---\n\n## API Requests Usage\n\n### Basic API Calls\n\n```python\n# Get device models (constants)\nresponse = mistapi.api.v1.const.device_models.listDeviceModels(apisession)\nprint(f\"Status: {response.status_code}\")\nprint(f\"Data: {len(response.data)} models\")\n\n# Get organization information\norg_info = mistapi.api.v1.orgs.orgs.getOrg(apisession, org_id)\nprint(f\"Organization: {org_info.data['name']}\")\n\n# Get organization statistics\norg_stats = mistapi.api.v1.orgs.stats.getOrgStats(apisession, org_id)\nprint(f\"Organization has {org_stats.data['num_sites']} sites\")\n\n# Search for devices\ndevices = mistapi.api.v1.orgs.devices.searchOrgDevices(apisession, org_id, type=\"ap\")\nprint(f\"Found {len(devices.data['results'])} access points\")\n```\n\n### Error Handling\n\n```python\n# Check response status\nresponse = mistapi.api.v1.orgs.orgs.listOrgs(apisession)\nif response.status_code == 200:\n    print(f\"Success: {len(response.data)} organizations\")\nelse:\n    print(f\"Error {response.status_code}: {response.data}\")\n\n# Exception handling\ntry:\n    org_info = mistapi.api.v1.orgs.orgs.getOrg(apisession, \"invalid-org-id\")\nexcept Exception as e:\n    print(f\"API Error: {e}\")\n```\n\n\n### Log Sanitization\n\nThe package automatically sanitizes sensitive data in logs:\n\n```python\nimport logging\nfrom mistapi.__logger import LogSanitizer\n\n# Configure logging\nLOG_FILE = \"./app.log\"\nlogging.basicConfig(filename=LOG_FILE, filemode=\"w\")\nLOGGER = logging.getLogger(__name__)\nLOGGER.setLevel(logging.DEBUG)\n\n# Add sanitization filter\nLOGGER.addFilter(LogSanitizer())\n\n# Sensitive data is automatically redacted\nLOGGER.debug({\"user\": \"john\", \"apitoken\": \"secret123\", \"password\": \"pass456\"})\n# Output: {\"user\": \"john\", \"apitoken\": \"****\", \"password\": \"****\"}\n```\n\n### Getting Help\n\n```python\n# Get detailed help on any API function\nhelp(mistapi.api.v1.orgs.stats.getOrgStats)\n```\n\n---\n\n### CLI Helper Functions\n\nInteractive functions for selecting organizations and sites.\n\n#### Organization Selection\n\n```python\n# Select single organization\norg_id = mistapi.cli.select_org(apisession)[0]\n\n# Select multiple organizations\norg_ids = mistapi.cli.select_org(apisession, allow_many=True)\n```\n\n**Output:**\n```\nAvailable organizations:\n0) Acme Corp (id: 203d3d02-xxxx-xxxx-xxxx-76896a3330f4)\n1) Demo Lab (id: 6374a757-xxxx-xxxx-xxxx-361e45b2d4ac)\n\nSelect an Org (0 to 1, or q to exit): 0\n```\n\n#### Site Selection\n\n```python\n# Select site within an organization\nsite_id = mistapi.cli.select_site(apisession, org_id=org_id)[0]\n```\n\n**Output:**\n```\nAvailable sites:\n0) Headquarters (id: f5fcbee5-xxxx-xxxx-xxxx-1619ede87879)\n1) Branch Office (id: a8b2c3d4-xxxx-xxxx-xxxx-987654321abc)\n\nSelect a Site (0 to 1, or q to exit): 0\n```\n\n---\n\n### Pagination Support\n\n#### Get Next Page\n\n```python\n# Get first page\nresponse = mistapi.api.v1.orgs.clients.searchOrgClientsEvents(\n    apisession, org_id, duration=\"1d\"\n)\nprint(f\"First page: {len(response.data['results'])} results\")\n\n# Get next page\nif response.next:\n    response_2 = mistapi.get_next(apisession, response)\n    print(f\"Second page: {len(response_2.data['results'])} results\")\n```\n\n#### Get All Pages Automatically\n\n```python\n# Get all pages with a single call\nresponse = mistapi.api.v1.orgs.clients.searchOrgClientsEvents(\n    apisession, org_id, duration=\"1d\"\n)\nprint(f\"First page: {len(response.data['results'])} results\")\n\n# Retrieve all remaining pages\nall_data = mistapi.get_all(apisession, response)\nprint(f\"Total results across all pages: {len(all_data)}\")\n```\n\n---\n\n### Examples\n\nComprehensive examples are available in the [Mist Library repository](https://github.com/tmunzer/mist_library).\n\n#### Device Management\n\n```python\n# List all devices in an organization\ndevices = mistapi.api.v1.orgs.devices.listOrgDevices(apisession, org_id)\n\n# Get specific device details\ndevice = mistapi.api.v1.orgs.devices.getOrgDevice(\n    apisession, org_id, device_id\n)\n\n# Update device configuration\nupdate_data = {\"name\": \"New Device Name\"}\nresult = mistapi.api.v1.orgs.devices.updateOrgDevice(\n    apisession, device.org_id, device.id, body=update_data\n)\n```\n\n#### Site Management\n\n```python\n# Create a new site\nsite_data = {\n    \"name\": \"New Branch Office\",\n    \"country_code\": \"US\",\n    \"timezone\": \"America/New_York\"\n}\nnew_site = mistapi.api.v1.orgs.sites.createOrgSite(\n    apisession, org_id, body=site_data\n)\n\n# Get site statistics\nsite_stats = mistapi.api.v1.sites.stats.getSiteStats(apisession, new_site.id)\n```\n\n#### Client Analytics\n\n```python\n# Search for wireless clients\nclients = mistapi.api.v1.orgs.clients.searchOrgWirelessClients(\n    apisession, org_id, \n    duration=\"1d\",\n    limit=100\n)\n\n# Get client events\nevents = mistapi.api.v1.orgs.clients.searchOrgClientsEvents(\n    apisession, org_id,\n    duration=\"1h\",\n    client_mac=\"aabbccddeeff\"\n)\n```\n\n---\n\n## Async Usage\n\nAll API functions in `mistapi.api.v1` are synchronous by default. To use them in an `asyncio` context (e.g., FastAPI, aiohttp, or any async application) without blocking the event loop, use `mistapi.arun()`.\n\n`arun()` wraps any sync mistapi function in `asyncio.to_thread()`, running the blocking HTTP request in a thread pool while the event loop continues. No changes are needed to the existing API functions.\n\n### Running API Calls Asynchronously\n\n```python\nimport asyncio\nimport mistapi\nfrom mistapi.api.v1.sites import devices\n\napisession = mistapi.APISession(env_file=\"~/.mist_env\")\napisession.login()\n\nasync def main():\n    # Wrap any sync API call with mistapi.arun()\n    response = await mistapi.arun(\n        devices.listSiteDevices, apisession, site_id\n    )\n    print(response.data)\n\nasyncio.run(main())\n```\n\n### Concurrent API Calls\n\nUse `asyncio.gather()` to run multiple API calls concurrently:\n\n```python\nimport asyncio\nimport mistapi\nfrom mistapi.api.v1.orgs import orgs\nfrom mistapi.api.v1.sites import devices\n\nasync def main():\n    org_info, site_devices = await asyncio.gather(\n        mistapi.arun(orgs.getOrg, apisession, org_id),\n        mistapi.arun(devices.listSiteDevices, apisession, site_id),\n    )\n    print(f\"Org: {org_info.data['name']}\")\n    print(f\"Devices: {len(site_devices.data)}\")\n\nasyncio.run(main())\n```\n\n### Combining with Device Utilities\n\nDevice utility functions are already non-blocking and return a `UtilResponse` that supports `await`. You can mix `arun()` for API calls and `await` for device utilities:\n\n```python\nimport asyncio\nimport mistapi\nfrom mistapi.api.v1.sites import devices\nfrom mistapi.device_utils import ex\n\nasync def main():\n    # Start device utility — returns immediately, collects data in a background thread\n    response = ex.retrieveArpTable(apisession, site_id, device_id)\n\n    # Meanwhile, run an API call via arun() — both execute concurrently\n    device_info = await mistapi.arun(\n        devices.getSiteDevice, apisession, site_id, device_id\n    )\n    print(f\"Device: {device_info.data['name']}\")\n\n    # Wait for the device utility background thread to finish\n    await response\n    print(f\"ARP entries: {len(response.ws_data)}\")\n\nasyncio.run(main())\n```\n\n---\n\n## WebSocket Streaming\n\nThe package provides a WebSocket client for real-time event streaming from the Mist API (`wss://{host}/api-ws/v1/stream`). Authentication is handled automatically using the same session credentials (API token or login/password).\n\n### Connection Parameters\n\nAll channel classes accept the following optional keyword arguments:\n\n| Parameter | Type | Default | Description |\n|-----------|------|---------|-------------|\n| `ping_interval` | `int` | `30` | Seconds between automatic ping frames. Set to `0` to disable pings. |\n| `ping_timeout` | `int` | `10` | Seconds to wait for a pong response before treating the connection as dead. |\n| `auto_reconnect` | `bool` | `False` | Automatically reconnect on transient failures using exponential backoff. |\n| `max_reconnect_attempts` | `int` | `5` | Maximum number of reconnect attempts before giving up. |\n| `reconnect_backoff` | `float` | `2.0` | Base backoff delay in seconds. Doubles after each failed attempt (2s, 4s, 8s, ...). Resets on successful reconnection. |\n\n```python\nws = mistapi.websockets.sites.DeviceStatsEvents(\n    apisession,\n    site_ids=[\"\u003csite_id\u003e\"],\n    ping_interval=60,       # ping every 60 s\n    ping_timeout=20,        # wait up to 20 s for pong\n    auto_reconnect=True,    # reconnect on transient failures\n)\nws.connect()\n```\n\n### Callbacks\n\n| Method | Signature | Description |\n|--------|-----------|-------------|\n| `ws.on_open(cb)` | `cb()` | Called when the connection is established |\n| `ws.on_message(cb)` | `cb(data: dict)` | Called for every incoming message |\n| `ws.on_error(cb)` | `cb(error: Exception)` | Called on WebSocket errors |\n| `ws.on_close(cb)` | `cb(status_code: int, msg: str)` | Called when the connection closes |\n| `ws.ready()` | `-\u003e bool \\| None` | Returns `True` if the connection is open and ready |\n\n### Available Channels\n\n#### Organization Channels\n\n| Class | Channel | Description |\n|-------|---------|-------------|\n| `mistapi.websockets.orgs.InsightsEvents` | `/orgs/{org_id}/insights/summary` | Real-time insights events for an organization |\n| `mistapi.websockets.orgs.MxEdgesStatsEvents` | `/orgs/{org_id}/stats/mxedges` | Real-time MX edges stats for an organization |\n| `mistapi.websockets.orgs.MxEdgesEvents` | `/orgs/{org_id}/mxedges` | Real-time MX edges events for an organization |\n\n#### Site Channels\n\n| Class | Channel | Description |\n|-------|---------|-------------|\n| `mistapi.websockets.sites.ClientsStatsEvents` | `/sites/{site_id}/stats/clients` | Real-time clients stats for a site |\n| `mistapi.websockets.sites.DeviceCmdEvents` | `/sites/{site_id}/devices/{device_id}/cmd` | Real-time device command events for a site |\n| `mistapi.websockets.sites.DeviceStatsEvents` | `/sites/{site_id}/stats/devices` | Real-time device stats for a site |\n| `mistapi.websockets.sites.DeviceEvents` | `/sites/{site_id}/devices` | Real-time device events for a site |\n| `mistapi.websockets.sites.MxEdgesStatsEvents` | `/sites/{site_id}/stats/mxedges` | Real-time MX edges stats for a site |\n| `mistapi.websockets.sites.PcapEvents` | `/sites/{site_id}/pcap` | Real-time PCAP events for a site |\n\n#### Location Channels\n\n| Class | Channel | Description |\n|-------|---------|-------------|\n| `mistapi.websockets.location.BleAssetsEvents` | `/sites/{site_id}/stats/maps/{map_id}/assets` | Real-time BLE assets location events |\n| `mistapi.websockets.location.ConnectedClientsEvents` | `/sites/{site_id}/stats/maps/{map_id}/clients` | Real-time connected clients location events |\n| `mistapi.websockets.location.SdkClientsEvents` | `/sites/{site_id}/stats/maps/{map_id}/sdkclients` | Real-time SDK clients location events |\n| `mistapi.websockets.location.UnconnectedClientsEvents` | `/sites/{site_id}/stats/maps/{map_id}/unconnected_clients` | Real-time unconnected clients location events |\n| `mistapi.websockets.location.DiscoveredBleAssetsEvents` | `/sites/{site_id}/stats/maps/{map_id}/discovered_assets` | Real-time discovered BLE assets location events |\n\n#### Session Channels\n\n| Class | Channel | Description |\n|-------|---------|-------------|\n| `mistapi.websockets.session.SessionWithUrl` | Custom URL | Connect to a custom WebSocket channel URL |\n\n### Usage Patterns\n\n#### Callback style (recommended)\n\n`connect()` defaults to `run_in_background=True` and returns immediately. The WebSocket runs in a daemon thread, so your program must stay alive (e.g., with `input()` or an event loop). Messages are delivered to the registered callback in the background thread.\n\n```python\nimport mistapi\n\napisession = mistapi.APISession(env_file=\"~/.mist_env\")\napisession.login()\n\nws = mistapi.websockets.sites.DeviceStatsEvents(apisession, site_ids=[\"\u003csite_id\u003e\"])\nws.on_message(lambda data: print(data))\nws.connect()                    # non-blocking\n\ninput(\"Press Enter to stop\")\nws.disconnect()\n```\n\n#### Generator style\n\nIterate over incoming messages as a blocking generator. Useful when you want to process messages sequentially in a loop.\n\n```python\nws = mistapi.websockets.sites.DeviceStatsEvents(apisession, site_ids=[\"\u003csite_id\u003e\"])\nws.connect(run_in_background=True)\n\nfor msg in ws.receive():        # blocks, yields each message as a dict\n    print(msg)\n    if some_condition:\n        ws.disconnect()         # stops the generator cleanly\n```\n\n#### Blocking style\n\n`connect(run_in_background=False)` blocks the calling thread until the connection closes. Useful for simple scripts.\n\n```python\nws = mistapi.websockets.sites.DeviceStatsEvents(apisession, site_ids=[\"\u003csite_id\u003e\"])\nws.on_message(lambda data: print(data))\nws.connect(run_in_background=False)  # blocks until disconnected\n```\n\n#### Context manager\n\n`disconnect()` is called automatically on exit, even if an exception is raised.\n\n```python\nimport time\n\nwith mistapi.websockets.sites.DeviceStatsEvents(apisession, site_ids=[\"\u003csite_id\u003e\"]) as ws:\n    ws.on_message(lambda data: print(data))\n    ws.connect()\n    time.sleep(60)\n# ws.disconnect() called automatically here\n```\n\n---\n\n## Device Utilities\n\n`mistapi.device_utils` provides high-level utilities for running diagnostic commands on Mist-managed devices. Each function triggers a REST API call and streams the results back via WebSocket. The library handles the connection plumbing — you just call the function and get back a `UtilResponse` object.\n\n### Supported Devices\n\n| Module | Device Type | Functions |\n|--------|-------------|-----------|\n| `device_utils.ap` | Mist Access Points | `ping`, `traceroute`, `retrieveArpTable` |\n| `device_utils.ex` | Juniper EX Switches | `ping`, `monitorTraffic`, `topCommand`, `interactiveShell`, `createShellSession`, `retrieveArpTable`, `retrieveBgpSummary`, `retrieveDhcpLeases`, `releaseDhcpLeases`, `retrieveMacTable`, `clearMacTable`, `clearLearnedMac`, `clearBpduError`, `clearDot1xSessions`, `clearHitCount`, `bouncePort`, `cableTest` |\n| `device_utils.srx` | Juniper SRX Firewalls | `ping`, `monitorTraffic`, `topCommand`, `interactiveShell`, `createShellSession`, `retrieveArpTable`, `retrieveBgpSummary`, `retrieveDhcpLeases`, `releaseDhcpLeases`, `retrieveOspfDatabase`, `retrieveOspfNeighbors`, `retrieveOspfInterfaces`, `retrieveOspfSummary`, `retrieveSessions`, `clearSessions`, `bouncePort`, `retrieveRoutes` |\n| `device_utils.ssr` | Juniper SSR Routers | `ping`, `retrieveArpTable`, `retrieveBgpSummary`, `retrieveDhcpLeases`, `releaseDhcpLeases`, `retrieveOspfDatabase`, `retrieveOspfNeighbors`, `retrieveOspfInterfaces`, `retrieveOspfSummary`, `retrieveSessions`, `clearSessions`, `bouncePort`, `retrieveRoutes`, `showServicePath` |\n\n### Device Utilities Usage\n\nAll device utility functions are **non-blocking**: they trigger the REST API call, start a WebSocket stream in the background, and return a `UtilResponse` immediately. Your script can continue processing while data streams in.\n\n#### Callback style\n\nPass an `on_message` callback to process each result as it arrives:\n\n```python\nfrom mistapi.device_utils import ex\n\ndef handle(msg):\n    print(\"Live:\", msg)\n\nresponse = ex.retrieveArpTable(apisession, site_id, device_id, on_message=handle)\n# returns immediately — on_message fires for each message in the background\n\ndo_other_work()\n\nresponse.wait()              # block until streaming is complete\nprint(response.ws_data)      # all collected data\n```\n\n#### Generator style\n\nIterate over processed messages as they arrive, similar to `_MistWebsocket.receive()`:\n\n```python\nresponse = ex.retrieveMacTable(apisession, site_id, device_id)\nfor msg in response.receive():    # blocking generator, yields each message\n    print(msg, end=\"\", flush=True)\n# loop ends when the WebSocket closes\nprint(response.ws_data)\n```\n\n#### Context manager\n\n`disconnect()` is called automatically when the context exits:\n\n```python\nwith ex.cableTest(apisession, site_id, device_id, port_id=\"ge-0/0/0\") as response:\n    for msg in response.receive():\n        print(msg, end=\"\", flush=True)\n# WebSocket disconnected, data ready\nprint(response.ws_data)\n```\n\n#### Polling\n\nCheck `response.done` to avoid blocking:\n\n```python\nresponse = ex.retrieveBgpSummary(apisession, site_id, device_id)\nwhile not response.done:\n    do_other_work()\nprint(response.ws_data)\n```\n\n#### Cancel early\n\nStop a long-running stream before it completes:\n\n```python\nresponse = ex.monitorTraffic(apisession, site_id, device_id, port_id=\"ge-0/0/0\")\ndo_some_work()\nresponse.disconnect()        # stop the WebSocket\nprint(response.ws_data)      # data collected so far\n```\n\n#### Async await\n\nWorks in `asyncio` contexts without blocking the event loop:\n\n```python\nimport asyncio\nfrom mistapi.device_utils import ex\n\nasync def main():\n    response = ex.retrieveArpTable(apisession, site_id, device_id)\n    await response               # non-blocking await\n    print(response.ws_data)\n\nasyncio.run(main())\n```\n\n### UtilResponse Object\n\nAll device utility functions return a `UtilResponse` object:\n\n#### Attributes\n\n| Attribute | Type | Description |\n|-----------|------|-------------|\n| `trigger_api_response` | `APIResponse` | The initial REST API response that triggered the device command. Contains `status_code`, `data`, and `headers` from the trigger request. |\n| `ws_required` | `bool` | `True` if the command required a WebSocket connection to stream results (most diagnostic commands do). `False` if the REST response alone was sufficient. |\n| `ws_data` | `list[str]` | Parsed result data extracted from the WebSocket stream. This list is **live** — it grows as messages arrive in the background, even before `wait()` is called. |\n| `ws_raw_events` | `list[str]` | Raw, unprocessed WebSocket event payloads as received from the Mist API. Useful for debugging or custom parsing. |\n\n#### Properties and Methods\n\n| Method / Property | Returns | Description |\n|-------------------|---------|-------------|\n| `done` | `bool` | `True` if data collection is complete (or no WS was needed). |\n| `wait(timeout=None)` | `UtilResponse` | Block until data collection is complete. Returns `self`. |\n| `receive()` | `Generator` | Blocking generator that yields each processed message as it arrives. Exits when the WebSocket closes. |\n| `disconnect()` | `None` | Stop the WebSocket connection early. |\n| `await response` | `UtilResponse` | Non-blocking await for `asyncio` contexts. |\n\n`UtilResponse` also supports the context manager protocol (`with` statement).\n\n### Enums\n\n- `ap.TracerouteProtocol` — `ICMP`, `UDP` (for `ap.traceroute()`)\n- `srx.Node` / `ssr.Node` — `NODE0`, `NODE1` (for dual-node devices)\n\n### Interactive Shell\n\n`interactiveShell()` and `createShellSession()` provide SSH-over-WebSocket access to EX and SRX devices. Unlike the diagnostic utilities above, the shell is **bidirectional** — you send keystrokes and receive terminal output in real time.\n\n#### Interactive mode (human at the keyboard)\n\nTakes over the terminal. Blocks until the connection closes or you press Ctrl+C:\n\n```python\nfrom mistapi.device_utils import ex\n\nex.interactiveShell(apisession, site_id, device_id)\n```\n\nRequires the `sshkeyboard` package (installed automatically as a dependency).\n\n#### Programmatic mode\n\nUse `createShellSession()` to get a `ShellSession` object for scripting:\n\n```python\nfrom mistapi.device_utils import ex\nimport time\n\nwith ex.createShellSession(apisession, site_id, device_id) as session:\n    session.send_text(\"show version\\r\\n\")\n    time.sleep(3)\n    while True:\n        data = session.recv(timeout=0.5)\n        if data is None:\n            break\n        print(data.decode(\"utf-8\", errors=\"replace\"), end=\"\")\n```\n\n#### ShellSession API\n\n| Method / Property | Returns | Description |\n|-------------------|---------|-------------|\n| `connect()` | `None` | Open the WebSocket connection. Called automatically by `createShellSession()`. |\n| `disconnect()` | `None` | Close the WebSocket connection. |\n| `connected` | `bool` | `True` if the WebSocket is currently connected. |\n| `send(data)` | `None` | Send raw bytes (keystrokes) to the device. |\n| `send_text(text)` | `None` | Send a text string to the device (auto-prefixed with `\\x00`). |\n| `recv(timeout=0.1)` | `bytes \\| None` | Receive output from the device. Returns `None` on timeout or if disconnected. |\n| `resize(rows, cols)` | `None` | Send a terminal resize message. |\n\n`ShellSession` also supports the context manager protocol (`with` statement).\n\n---\n\n## Development and Testing\n\n### Development Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/tmunzer/mistapi_python.git\ncd mistapi_python\n\n# With pip\npip install -e \".[dev]\"\n\n# With uv\nuv sync\n```\n\n### Running Tests\n\n```bash\n# Run all tests\npytest\n# or with uv\nuv run pytest\n\n# Run with coverage report\npytest --cov=src/mistapi --cov-report=html\n\n# Run specific test file\npytest tests/unit/test_api_session.py\n\n# Run linting\nruff check src/\n# or with uv\nuv run ruff check src/\n```\n\n### Package Structure\n\n```\nsrc/mistapi/\n├── __init__.py           # Main package exports (lazy-loads api, cli, utils, websockets)\n├── __api_session.py      # Session management and authentication\n├── __api_request.py      # HTTP request handling\n├── __api_response.py     # Response parsing and pagination\n├── __logger.py           # Logging and sanitization\n├── __pagination.py       # Pagination utilities\n├── cli.py                # Interactive CLI functions\n├── __models/             # Data models\n│   ├── __init__.py\n│   └── privilege.py\n├── api/v1/               # Auto-generated API endpoints\n│   ├── const/            # Constants and enums\n│   ├── orgs/             # Organization-level APIs\n│   ├── sites/            # Site-level APIs\n│   ├── login/            # Authentication APIs\n│   └── utils/            # Utility functions\n├── device_utils/         # Device utility implementations\n│   ├── ap.py             # Access Point utilities\n│   ├── ex.py             # EX Switch utilities\n│   ├── srx.py            # SRX Firewall utilities\n│   ├── ssr.py            # Session Smart Router utilities\n│   └── ...               # Function-based modules (arp, bgp, dhcp, etc.)\n└── websockets/           # Real-time WebSocket streaming\n    ├── __ws_client.py    # Base WebSocket client\n    ├── orgs.py           # Organization-level channels\n    ├── sites.py          # Site-level channels\n    ├── location.py       # Location/map channels\n    └── session.py        # Custom URL session channel\n```\n\n---\n\n## Contributing\n\nContributions are welcome! Please follow these guidelines:\n\n### How to Contribute\n\n1. **Fork** the repository\n2. **Create** a feature branch\n   ```bash\n   git checkout -b feature/amazing-feature\n   ```\n3. **Commit** your changes\n   ```bash\n   git commit -m 'Add amazing feature'\n   ```\n4. **Push** to the branch\n   ```bash\n   git push origin feature/amazing-feature\n   ```\n5. **Open** a Pull Request\n\n### Development Guidelines\n\n- Write tests for new features\n- Ensure all tests pass before submitting PR\n- Follow existing code style and conventions\n- Update documentation as needed\n- Add entries to CHANGELOG.md for significant changes\n\n---\n\n## License\n\n**MIT License**\n\nCopyright (c) 2023 Thomas Munzer\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n\n---\n\n## Links\n\n- **Mist API Specs**: [OpenAPI Documentation](https://www.juniper.net/documentation/us/en/software/mist/api/http/getting-started/how-to-get-started)\n- **Source Code**: [GitHub Repository](https://github.com/tmunzer/mistapi_python)\n- **PyPI Package**: [mistapi on PyPI](https://pypi.org/project/mistapi/)\n- **Examples**: [Mist Library Examples](https://github.com/tmunzer/mist_library)\n- **Bug Reports**: [GitHub Issues](https://github.com/tmunzer/mistapi_python/issues)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftmunzer%2Fmistapi_python","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftmunzer%2Fmistapi_python","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftmunzer%2Fmistapi_python/lists"}