Skip to content

Commit ff179af

Browse files
authored
0.61.3 (#18)
* Update uv.lock * Update pyproject.toml * Update uv.lock * update sles and ws client * websocket clietn hardening * ws client hardening * ws client hardening * ws client queue_maxsize hardening * ws client hardening * Update __ws_client.py * Update test_websocket_client.py * Update __ws_client.py * Update __ws_client.py * Update orgs.py * fix ws dosctrings * Update CHANGELOG.md
1 parent ef665a2 commit ff179af

16 files changed

Lines changed: 1161 additions & 450 deletions

File tree

.gitmodules

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
[submodule "mist_openapi"]
22
path = mist_openapi
33
url = https://github.com/mistsys/mist_openapi.git
4-
branch = 2602.1.7
4+
branch = 2602.1.8

CHANGELOG.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,53 @@
11
# CHANGELOG
2+
## Version 0.61.3 (March 2026)
3+
4+
**Released**: March 18, 2026
5+
6+
This release hardens the WebSocket client with improved thread-safety, bounded message queues, and better error handling.
7+
8+
---
9+
10+
### 1. NEW FEATURES
11+
12+
#### **Bounded Message Queue (`queue_maxsize`)**
13+
The `_MistWebsocket` client now supports a `queue_maxsize` parameter to limit the internal message buffer size. When set, incoming messages are dropped with a warning when the queue is full, preventing unbounded memory growth on high-frequency streams.
14+
15+
```python
16+
ws = mistapi.websockets.sites.DeviceStatsEvents(
17+
apisession,
18+
site_ids=["<site_id>"],
19+
queue_maxsize=1000 # Limit buffer to 1000 messages
20+
)
21+
```
22+
23+
---
24+
25+
### 2. IMPROVEMENTS
26+
27+
#### **Thread-Safety Enhancements**
28+
- Added `threading.Lock()` to protect shared state during concurrent access
29+
- Added `_finished` event for cleaner lifecycle management and proper shutdown signaling
30+
31+
#### **Error Handling Hardening**
32+
- User-provided callbacks (`on_open`, `on_message`, `on_error`, `on_close`) are now wrapped in try/except blocks to prevent exceptions from crashing the WebSocket thread
33+
- Added warning when `cloud_uri` does not start with `"api."` (WebSocket URL may be incorrect)
34+
35+
#### **Security: Header Redaction**
36+
- Added `_HeaderRedactFilter` to automatically redact `Authorization` and `Cookie` headers from `websocket-client` debug logs
37+
38+
---
39+
40+
### 3. API CHANGES
41+
42+
#### **Insights API**
43+
Added optional `port_id` query parameter to the following functions for port-level filtering:
44+
- **`getSiteInsightMetricsForDevice()`**
45+
- **`getSiteInsightMetricsForGateway()`**
46+
- **`getSiteInsightMetricsForMxEdge()`**
47+
- **`getSiteInsightMetricsForSwitch()`**
48+
49+
---
50+
251
## Version 0.61.2 (March 2026)
352

453
**Released**: March 17, 2026

README.md

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ A comprehensive Python package to interact with the Mist Cloud APIs, built from
3232
- [Examples](#examples)
3333
- [WebSocket Streaming](#websocket-streaming)
3434
- [Connection Parameters](#connection-parameters)
35-
- [Callbacks](#callbacks)
35+
- [Methods](#methods)
3636
- [Available Channels](#available-channels)
3737
- [Usage Patterns](#usage-patterns)
3838
- [Async Usage](#async-usage)
@@ -588,6 +588,7 @@ All channel classes accept the following optional keyword arguments:
588588
| `auto_reconnect` | `bool` | `False` | Automatically reconnect on transient failures using exponential backoff. |
589589
| `max_reconnect_attempts` | `int` | `5` | Maximum number of reconnect attempts before giving up. |
590590
| `reconnect_backoff` | `float` | `2.0` | Base backoff delay in seconds. Doubles after each failed attempt (2s, 4s, 8s, ...). Resets on successful reconnection. |
591+
| `queue_maxsize` | `int` | `0` | Maximum messages buffered in the internal queue for `receive()`. `0` means unbounded. When set, incoming messages are dropped with a warning when the queue is full, preventing memory growth on high-frequency streams. |
591592

592593
```python
593594
ws = mistapi.websockets.sites.DeviceStatsEvents(
@@ -600,15 +601,18 @@ ws = mistapi.websockets.sites.DeviceStatsEvents(
600601
ws.connect()
601602
```
602603

603-
### Callbacks
604+
### Methods
604605

605606
| Method | Signature | Description |
606607
|--------|-----------|-------------|
607-
| `ws.on_open(cb)` | `cb()` | Called when the connection is established |
608-
| `ws.on_message(cb)` | `cb(data: dict)` | Called for every incoming message |
609-
| `ws.on_error(cb)` | `cb(error: Exception)` | Called on WebSocket errors |
610-
| `ws.on_close(cb)` | `cb(status_code: int, msg: str)` | Called when the connection closes |
611-
| `ws.ready()` | `-> bool \| None` | Returns `True` if the connection is open and ready |
608+
| `ws.on_open(cb)` | `cb()` | Register callback for connection established |
609+
| `ws.on_message(cb)` | `cb(data: dict)` | Register callback for incoming messages. Mutually exclusive with `receive()`. |
610+
| `ws.on_error(cb)` | `cb(error: Exception)` | Register callback for WebSocket errors |
611+
| `ws.on_close(cb)` | `cb(code: int \| None, msg: str \| None)` | Register callback for connection close. Safe to call `connect()` from within. |
612+
| `ws.connect(run_in_background)` | | Open the connection. `True` (default) runs in a daemon thread; `False` blocks. |
613+
| `ws.disconnect(wait, timeout)` | | Close the connection. `wait=True` blocks until the background thread finishes. |
614+
| `ws.receive()` | `-> Generator[dict]` | Blocking generator yielding messages. Mutually exclusive with `on_message`. |
615+
| `ws.ready()` | `-> bool` | Returns `True` if the connection is open and ready |
612616

613617
### Available Channels
614618

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "mistapi"
7-
version = "0.61.2"
7+
version = "0.61.3"
88
authors = [{ name = "Thomas Munzer", email = "tmunzer@juniper.net" }]
99
description = "Python package to simplify the Mist System APIs usage"
1010
keywords = ["Mist", "Juniper", "API"]
@@ -64,7 +64,7 @@ dev = [
6464
"twine>=6.1.0",
6565
"build>=1.2.2.post1",
6666
"pyyaml>=6.0.2",
67-
"urllib3>=2.4.0",
67+
"urllib3>=2.6.3",
6868
]
6969

7070
# Hatchling configuration (replaces setup.cfg)

scripts/generate_from_openapi.py

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -669,23 +669,26 @@ def _create_get(
669669
if code:
670670
has_deprecated = True
671671

672-
# Generate main function parameters and documentation
673-
code_path_params, desc_path_params = _gen_code_params(path_params, operation_id)
674-
code_query_params, desc_query_params = _gen_code_params(query_params, operation_id)
675-
code_query = _gen_query_code(query_params)
676-
code_desc = _gen_description(
677-
operation_id, tags, desc_path_params, desc_query_params
678-
)
672+
else:
673+
# Generate main function parameters and documentation
674+
code_path_params, desc_path_params = _gen_code_params(path_params, operation_id)
675+
code_query_params, desc_query_params = _gen_code_params(
676+
query_params, operation_id
677+
)
678+
code_query = _gen_query_code(query_params)
679+
code_desc = _gen_description(
680+
operation_id, tags, desc_path_params, desc_query_params
681+
)
679682

680-
# Generate main GET function code
681-
code += FUNCTION_GET_TEMPLATE.format(
682-
operation_id=operation_id,
683-
code_path_params=code_path_params,
684-
code_query_params=code_query_params,
685-
code_desc=code_desc,
686-
uri=_gen_uri(endpoint_path),
687-
query_code=code_query,
688-
)
683+
# Generate main GET function code
684+
code += FUNCTION_GET_TEMPLATE.format(
685+
operation_id=operation_id,
686+
code_path_params=code_path_params,
687+
code_query_params=code_query_params,
688+
code_desc=code_desc,
689+
uri=_gen_uri(endpoint_path),
690+
query_code=code_query,
691+
)
689692
return code, has_deprecated
690693

691694

src/mistapi/__version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
__version__ = "0.61.2"
1+
__version__ = "0.61.3"
22
__author__ = "Thomas Munzer <tmunzer@juniper.net>"

src/mistapi/api/v1/sites/insights.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ def getSiteInsightMetricsForDevice(
200200
site_id: str,
201201
metric: str,
202202
device_mac: str,
203+
port_id: str | None = None,
203204
start: str | None = None,
204205
end: str | None = None,
205206
duration: str | None = None,
@@ -223,6 +224,7 @@ def getSiteInsightMetricsForDevice(
223224
224225
QUERY PARAMS
225226
------------
227+
port_id : str
226228
start : str
227229
end : str
228230
duration : str, default: 1d
@@ -238,6 +240,8 @@ def getSiteInsightMetricsForDevice(
238240

239241
uri = f"/api/v1/sites/{site_id}/insights/device/{device_mac}/{metric}"
240242
query_params: dict[str, str] = {}
243+
if port_id:
244+
query_params["port_id"] = str(port_id)
241245
if start:
242246
query_params["start"] = str(start)
243247
if end:
@@ -398,6 +402,7 @@ def getSiteInsightMetricsForGateway(
398402
site_id: str,
399403
device_id: str,
400404
metrics: str,
405+
port_id: str | None = None,
401406
start: str | None = None,
402407
end: str | None = None,
403408
duration: str | None = None,
@@ -421,6 +426,7 @@ def getSiteInsightMetricsForGateway(
421426
QUERY PARAMS
422427
------------
423428
metrics : str
429+
port_id : str
424430
start : str
425431
end : str
426432
duration : str, default: 1d
@@ -438,6 +444,8 @@ def getSiteInsightMetricsForGateway(
438444
query_params: dict[str, str] = {}
439445
if metrics:
440446
query_params["metrics"] = str(metrics)
447+
if port_id:
448+
query_params["port_id"] = str(port_id)
441449
if start:
442450
query_params["start"] = str(start)
443451
if end:
@@ -459,6 +467,7 @@ def getSiteInsightMetricsForMxEdge(
459467
site_id: str,
460468
metric: str,
461469
device_mac: str,
470+
port_id: str | None = None,
462471
start: str | None = None,
463472
end: str | None = None,
464473
duration: str | None = None,
@@ -482,6 +491,7 @@ def getSiteInsightMetricsForMxEdge(
482491
483492
QUERY PARAMS
484493
------------
494+
port_id : str
485495
start : str
486496
end : str
487497
duration : str, default: 1d
@@ -497,6 +507,8 @@ def getSiteInsightMetricsForMxEdge(
497507

498508
uri = f"/api/v1/sites/{site_id}/insights/mxedge/{device_mac}/{metric}"
499509
query_params: dict[str, str] = {}
510+
if port_id:
511+
query_params["port_id"] = str(port_id)
500512
if start:
501513
query_params["start"] = str(start)
502514
if end:
@@ -624,6 +636,7 @@ def getSiteInsightMetricsForSwitch(
624636
site_id: str,
625637
metric: str,
626638
device_mac: str,
639+
port_id: str | None = None,
627640
start: str | None = None,
628641
end: str | None = None,
629642
duration: str | None = None,
@@ -647,6 +660,7 @@ def getSiteInsightMetricsForSwitch(
647660
648661
QUERY PARAMS
649662
------------
663+
port_id : str
650664
start : str
651665
end : str
652666
duration : str, default: 1d
@@ -662,6 +676,8 @@ def getSiteInsightMetricsForSwitch(
662676

663677
uri = f"/api/v1/sites/{site_id}/insights/switch/{device_mac}/{metric}"
664678
query_params: dict[str, str] = {}
679+
if port_id:
680+
query_params["port_id"] = str(port_id)
665681
if start:
666682
query_params["start"] = str(start)
667683
if end:

src/mistapi/api/v1/sites/sle.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@
1010
--------------------------------------------------------------------------------
1111
"""
1212

13-
import deprecation
14-
1513
from mistapi import APISession as _APISession
1614
from mistapi.__api_response import APIResponse as _APIResponse
15+
import deprecation
1716

1817

1918
@deprecation.deprecated(
2019
deprecated_in="0.59.2",
2120
removed_in="0.65.0",
22-
current_version="0.61.2",
21+
current_version="0.61.3",
2322
details="function replaced with getSiteSleClassifierSummaryTrend",
2423
)
2524
def getSiteSleClassifierDetails(
@@ -691,7 +690,7 @@ def listSiteSleImpactedWirelessClients(
691690
@deprecation.deprecated(
692691
deprecated_in="0.59.2",
693692
removed_in="0.65.0",
694-
current_version="0.61.2",
693+
current_version="0.61.3",
695694
details="function replaced with getSiteSleSummaryTrend",
696695
)
697696
def getSiteSleSummary(

0 commit comments

Comments
 (0)