Skip to content

Commit ef665a2

Browse files
0.61.2 (#17)
* 0.61.2 * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * propagate ws reconnect to other classes * bug fixes and other improvements * update tests * bug fixes --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent ac62836 commit ef665a2

File tree

25 files changed

+822
-242
lines changed

25 files changed

+822
-242
lines changed

.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 = master
4+
branch = 2602.1.7

CHANGELOG.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,58 @@
11
# CHANGELOG
2+
## Version 0.61.2 (March 2026)
3+
4+
**Released**: March 17, 2026
5+
6+
This release adds automatic reconnection support for WebSocket streams, updates the OpenAPI specification, and includes minor bug fixes.
7+
8+
---
9+
10+
### 1. NEW FEATURES
11+
12+
#### **WebSocket Auto-Reconnect**
13+
`_MistWebsocket` now supports automatic reconnection with configurable parameters:
14+
- `auto_reconnect` — Enable/disable auto-reconnect (default: `False`)
15+
- `max_reconnect_attempts` — Maximum reconnect attempts before giving up (default: `5`)
16+
- `reconnect_backoff` — Base backoff delay in seconds, with exponential increase (default: `2.0`)
17+
18+
When enabled, the WebSocket automatically reconnects on transient failures using exponential backoff. User-initiated `disconnect()` calls are respected during reconnection attempts.
19+
20+
```python
21+
ws = mistapi.websockets.sites.DeviceStatsEvents(
22+
apisession,
23+
site_ids=["<site_id>"],
24+
auto_reconnect=True,
25+
max_reconnect_attempts=5,
26+
reconnect_backoff=2.0
27+
)
28+
ws.connect(run_in_background=True)
29+
```
30+
31+
---
32+
33+
### 2. API CHANGES (OpenAPI 2602.1.7)
34+
35+
Updated to mist_openapi spec version 2602.1.7.
36+
37+
#### **Insights API**
38+
- **`getSiteInsightMetrics()`** — Now uses `metrics` as a query parameter instead of a path parameter
39+
- **`getSiteInsightMetricsForAP()`** — New function to retrieve insight metrics for a specific AP
40+
- **`getSiteInsightMetricsForClient()`** — Changed `metric` path parameter to `metrics` query parameter
41+
- **`getSiteInsightMetricsForGateway()`** — Changed `metric` path parameter to `metrics` query parameter
42+
43+
#### **Stats API**
44+
- **`getOrgStats()`** — Removed `start`, `end`, `duration`, `limit`, `page` query parameters
45+
- **`listOrgSiteStats()`** — Removed `start`, `end`, `duration` query parameters
46+
47+
---
48+
49+
### 3. BUG FIXES
50+
- Fixed `ShellSession.recv()` to gracefully handle socket timeout reset when the connection is already closed
51+
- Fixed thread-safety (TOCTOU) race conditions in `ShellSession` by capturing WebSocket reference in local variables across `disconnect()`, `connected`, `send()`, `recv()`, and `resize()` methods
52+
- Fixed thread-safety race condition in `_MistWebsocket.disconnect()` with local variable capture
53+
54+
---
55+
256
## Version 0.61.1 (March 2026)
357

458
**Released**: March 15, 2026

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -579,19 +579,23 @@ The package provides a WebSocket client for real-time event streaming from the M
579579

580580
### Connection Parameters
581581

582-
All channel classes accept the following optional keyword arguments to control the WebSocket keep-alive behaviour:
582+
All channel classes accept the following optional keyword arguments:
583583

584584
| Parameter | Type | Default | Description |
585585
|-----------|------|---------|-------------|
586586
| `ping_interval` | `int` | `30` | Seconds between automatic ping frames. Set to `0` to disable pings. |
587587
| `ping_timeout` | `int` | `10` | Seconds to wait for a pong response before treating the connection as dead. |
588+
| `auto_reconnect` | `bool` | `False` | Automatically reconnect on transient failures using exponential backoff. |
589+
| `max_reconnect_attempts` | `int` | `5` | Maximum number of reconnect attempts before giving up. |
590+
| `reconnect_backoff` | `float` | `2.0` | Base backoff delay in seconds. Doubles after each failed attempt (2s, 4s, 8s, ...). Resets on successful reconnection. |
588591

589592
```python
590593
ws = mistapi.websockets.sites.DeviceStatsEvents(
591594
apisession,
592595
site_ids=["<site_id>"],
593-
ping_interval=60, # ping every 60 s
594-
ping_timeout=20, # wait up to 20 s for pong
596+
ping_interval=60, # ping every 60 s
597+
ping_timeout=20, # wait up to 20 s for pong
598+
auto_reconnect=True, # reconnect on transient failures
595599
)
596600
ws.connect()
597601
```

pyproject.toml

Lines changed: 1 addition & 1 deletion
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.1"
7+
version = "0.61.2"
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"]

src/mistapi/__api_request.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,8 @@ def _request_with_retry(
253253
"apirequest:%s:Exception occurred", method_name, exc_info=True
254254
)
255255
break
256-
self._count += 1
256+
with self._token_lock:
257+
self._count += 1
257258
return APIResponse(url=url, response=resp, proxy_error=proxy_failed)
258259

259260
def mist_get(self, uri: str, query: dict[str, str] | None = None) -> APIResponse:

src/mistapi/__api_response.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
This module manages API responses
1212
"""
1313

14+
import re
15+
1416
from requests import Response
1517
from requests.structures import CaseInsensitiveDict
1618

@@ -85,7 +87,11 @@ def _check_next(self) -> None:
8587
separator = "&" if "?" in uri else "?"
8688
self.next = f"{uri}{separator}page={page + 1}"
8789
else:
88-
self.next = uri.replace(f"page={page}", f"page={page + 1}")
90+
self.next = re.sub(
91+
rf"(?<=[?&])page={page}(?=&|$)",
92+
f"page={page + 1}",
93+
uri,
94+
)
8995
logger.debug(f"apiresponse:_check_next:set next to {self.next}")
9096
except ValueError:
9197
logger.error(

src/mistapi/__api_session.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,7 @@ def _get_api_token_data(self, apitoken) -> tuple[str | None, list | None]:
580580
if data_json.get("email"):
581581
token_type = "user" # nosec bandit B105
582582

583-
for priv in data_json.get("privileges"):
583+
for priv in data_json.get("privileges", []):
584584
tmp = {
585585
"scope": priv.get("scope"),
586586
"role": priv.get("role"),
@@ -715,7 +715,7 @@ def _process_login(self, retry: bool = True) -> str | None:
715715
"email/password cleaned up. Restarting authentication function"
716716
)
717717
if retry:
718-
return self._process_login(retry)
718+
return self._process_login(retry=False)
719719
except requests.exceptions.ProxyError as proxy_error:
720720
LOGGER.critical("apisession:_process_login:proxy not valid...")
721721
CONSOLE.critical("Proxy not valid...\r\n")
@@ -935,9 +935,15 @@ def _set_authenticated(self, authentication_status: bool) -> None:
935935
LOGGER.error(
936936
"apirequest:mist_post_file: Exception occurred", exc_info=True
937937
)
938-
self._csrftoken = self._session.cookies["csrftoken" + cookies_ext]
939-
self._session.headers.update({"X-CSRFToken": self._csrftoken})
940-
LOGGER.info("apisession:_set_authenticated:CSRF Token stored")
938+
csrf_cookie = self._session.cookies.get("csrftoken" + cookies_ext)
939+
if csrf_cookie:
940+
self._csrftoken = csrf_cookie
941+
self._session.headers.update({"X-CSRFToken": self._csrftoken})
942+
LOGGER.info("apisession:_set_authenticated:CSRF Token stored")
943+
else:
944+
LOGGER.error(
945+
"apisession:_set_authenticated:CSRF Token cookie not found"
946+
)
941947
elif authentication_status is False:
942948
self._authenticated = False
943949
LOGGER.info(
@@ -1093,7 +1099,7 @@ def _getself(self) -> bool:
10931099
for key, val in resp.data.items():
10941100
if key == "privileges":
10951101
self.privileges = Privileges(resp.data["privileges"])
1096-
if key == "tags":
1102+
elif key == "tags":
10971103
for tag in resp.data["tags"]:
10981104
self.tags.append(tag)
10991105
else:

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.1"
1+
__version__ = "0.61.2"
22
__author__ = "Thomas Munzer <tmunzer@juniper.net>"

src/mistapi/api/v1/orgs/stats.py

Lines changed: 1 addition & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,7 @@
1414
from mistapi.__api_response import APIResponse as _APIResponse
1515

1616

17-
def getOrgStats(
18-
mist_session: _APISession,
19-
org_id: str,
20-
start: str | None = None,
21-
end: str | None = None,
22-
duration: str | None = None,
23-
limit: int | None = None,
24-
page: int | None = None,
25-
) -> _APIResponse:
17+
def getOrgStats(mist_session: _APISession, org_id: str) -> _APIResponse:
2618
"""
2719
API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/stats/get-org-stats
2820
@@ -35,14 +27,6 @@ def getOrgStats(
3527
-----------
3628
org_id : str
3729
38-
QUERY PARAMS
39-
------------
40-
start : str
41-
end : str
42-
duration : str, default: 1d
43-
limit : int, default: 100
44-
page : int, default: 1
45-
4630
RETURN
4731
-----------
4832
mistapi.APIResponse
@@ -51,16 +35,6 @@ def getOrgStats(
5135

5236
uri = f"/api/v1/orgs/{org_id}/stats"
5337
query_params: dict[str, str] = {}
54-
if start:
55-
query_params["start"] = str(start)
56-
if end:
57-
query_params["end"] = str(end)
58-
if duration:
59-
query_params["duration"] = str(duration)
60-
if limit:
61-
query_params["limit"] = str(limit)
62-
if page:
63-
query_params["page"] = str(page)
6438
resp = mist_session.mist_get(uri=uri, query=query_params)
6539
return resp
6640

@@ -1017,9 +991,6 @@ def searchOrgSwOrGwPorts(
1017991
def listOrgSiteStats(
1018992
mist_session: _APISession,
1019993
org_id: str,
1020-
start: str | None = None,
1021-
end: str | None = None,
1022-
duration: str | None = None,
1023994
limit: int | None = None,
1024995
page: int | None = None,
1025996
) -> _APIResponse:
@@ -1037,9 +1008,6 @@ def listOrgSiteStats(
10371008
10381009
QUERY PARAMS
10391010
------------
1040-
start : str
1041-
end : str
1042-
duration : str, default: 1d
10431011
limit : int, default: 100
10441012
page : int, default: 1
10451013
@@ -1051,12 +1019,6 @@ def listOrgSiteStats(
10511019

10521020
uri = f"/api/v1/orgs/{org_id}/stats/sites"
10531021
query_params: dict[str, str] = {}
1054-
if start:
1055-
query_params["start"] = str(start)
1056-
if end:
1057-
query_params["end"] = str(end)
1058-
if duration:
1059-
query_params["duration"] = str(duration)
10601022
if limit:
10611023
query_params["limit"] = str(limit)
10621024
if page:

0 commit comments

Comments
 (0)