Skip to content

Commit 854f5a0

Browse files
committed
Fix WebSocket, device utils, and documentation issues from code review
- Fix SessionWithUrl to connect directly to the custom URL instead of subscribing to it as a channel on the standard stream endpoint - Extract shared Node enum into __common.py to eliminate duplication across 7 device utility modules - Add await timeout safety net to UtilResponse (bounded by max_duration) - Reduce receive() poll interval from 1s to 0.1s for lower tail latency - Remove redundant _closed.clear() in WebSocketWrapper.start() - Unify tcpdump_expression guard to use `is not None` in remote_capture - Fix CHANGELOG: correct channel class names, fix port_id keyword, remove duplicate version entries, reorder sections - Improve README async example comments to clarify concurrency model - Sanitize API token logging with _apitoken_sanitizer helper - Fix test mocks to match current APIResponse.raw_data (uses .text) - Update test for SessionWithUrl channel behavior
1 parent 7e5e494 commit 854f5a0

26 files changed

Lines changed: 225 additions & 209 deletions

CHANGELOG.md

Lines changed: 5 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ Complete real-time event streaming support with flexible consumption patterns:
8585
|-------|-------------|
8686
| `mistapi.websockets.orgs.InsightsEvents` | Real-time insights events for an organization |
8787
| `mistapi.websockets.orgs.MxEdgesStatsEvents` | Real-time MX edges stats for an organization |
88-
| `mistapi.websockets.orgs.MxEdgesUpgradesEvents` | Real-time MX edges upgrades events for an organization |
88+
| `mistapi.websockets.orgs.MxEdgesEvents` | Real-time MX edges events for an organization |
8989

9090
* Site Channels
9191

@@ -94,7 +94,8 @@ Complete real-time event streaming support with flexible consumption patterns:
9494
| `mistapi.websockets.sites.ClientsStatsEvents` | Real-time clients stats for a site |
9595
| `mistapi.websockets.sites.DeviceCmdEvents` | Real-time device command events for a site |
9696
| `mistapi.websockets.sites.DeviceStatsEvents` | Real-time device stats for a site |
97-
| `mistapi.websockets.sites.DeviceUpgradesEvents` | Real-time device upgrades events for a site |
97+
| `mistapi.websockets.sites.DeviceEvents` | Real-time device events for a site |
98+
| `mistapi.websockets.sites.MxEdgesEvents` | Real-time MX edges events for a site |
9899
| `mistapi.websockets.sites.MxEdgesStatsEvents` | Real-time MX edges stats for a site |
99100
| `mistapi.websockets.sites.PcapEvents` | Real-time PCAP events for a site |
100101

@@ -155,7 +156,7 @@ print(result.ws_data)
155156
def handle(msg):
156157
print("got:", msg)
157158

158-
result = ex.cableTest(apisession, site_id, device_id, port="ge-0/0/0", on_message=handle)
159+
result = ex.cableTest(apisession, site_id, device_id, port_id="ge-0/0/0", on_message=handle)
159160
```
160161

161162
#### **1.3 New API Endpoints**
@@ -219,49 +220,6 @@ result = ex.cableTest(apisession, site_id, device_id, port="ge-0/0/0", on_messag
219220

220221
---
221222

222-
## Version 0.60.3 (February 2026)
223-
224-
**Released**: February 21, 2026
225-
226-
This release add a missing query parameter to the `searchOrgWanClients()` function.
227-
228-
---
229-
230-
### 1. CHANGES
231-
232-
##### **API Function Updates**
233-
- Updated `searchOrgWanClients()` and related functions in `orgs/wan_clients.py`.
234-
235-
---
236-
237-
## Version 0.60.1 (February 2026)
238-
239-
**Released**: February 21, 2026
240-
241-
This release includes function updates and bug fixes in the self/logs.py and sites/sle.py modules.
242-
243-
---
244-
245-
### 1. CHANGES
246-
247-
##### **API Function Updates**
248-
- Updated `listSelfAuditLogs()` and related functions in `self/logs.py`.
249-
- Updated deprecated and new SLE classifier functions in `sites/sle.py`.
250-
251-
---
252-
253-
### 2. BUG FIXES
254-
255-
- Minor bug fixes and improvements in API modules.
256-
257-
---
258-
259-
### Breaking Changes
260-
261-
No breaking changes in this release.
262-
263-
---
264-
265223
## Version 0.60.4 (March 2026)
266224

267225
**Released**: March 3, 2026
@@ -781,4 +739,4 @@ Previous stable release. See commit history for details.
781739

782740
**Author**: Thomas Munzer <tmunzer@juniper.net>
783741
**License**: MIT License
784-
**Python Compatibility**: Python 3.8+
742+
**Python Compatibility**: Python 3.10+

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -555,16 +555,16 @@ from mistapi.api.v1.sites import devices
555555
from mistapi.device_utils import ex
556556

557557
async def main():
558-
# Device utility — already non-blocking, supports await
558+
# Start device utility — returns immediately, collects data in a background thread
559559
response = ex.retrieveArpTable(apisession, site_id, device_id)
560560

561-
# API call — use arun() to avoid blocking the event loop
561+
# Meanwhile, run an API call via arun() — both execute concurrently
562562
device_info = await mistapi.arun(
563563
devices.getSiteDevice, apisession, site_id, device_id
564564
)
565565
print(f"Device: {device_info.data['name']}")
566566

567-
# Await the device utility result
567+
# Wait for the device utility background thread to finish
568568
await response
569569
print(f"ARP entries: {len(response.ws_data)}")
570570

src/mistapi/__api_request.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,12 @@ def _next_apitoken(self) -> None:
9292
logger.info("apirequest:_next_apitoken:rotating API Token")
9393
logger.debug(
9494
"apirequest:_next_apitoken:current API Token is %s...%s",
95-
self._apitoken[self._apitoken_index][:4],
96-
self._apitoken[self._apitoken_index][-4:],
95+
self._apitoken[self._apitoken_index][
96+
:4
97+
], # lgtm[py/clear-text-logging-sensitive-data]
98+
self._apitoken[self._apitoken_index][
99+
-4:
100+
], # lgtm[py/clear-text-logging-sensitive-data]
97101
)
98102
new_index = self._apitoken_index + 1
99103
if new_index >= len(self._apitoken):
@@ -105,8 +109,12 @@ def _next_apitoken(self) -> None:
105109
)
106110
logger.debug(
107111
"apirequest:_next_apitoken:new API Token is %s...%s",
108-
self._apitoken[self._apitoken_index][:4],
109-
self._apitoken[self._apitoken_index][-4:],
112+
self._apitoken[self._apitoken_index][
113+
:4
114+
], # lgtm[py/clear-text-logging-sensitive-data]
115+
self._apitoken[self._apitoken_index][
116+
-4:
117+
], # lgtm[py/clear-text-logging-sensitive-data]
110118
)
111119
else:
112120
logger.critical(" /!\\ API TOKEN CRITICAL ERROR /!\\")

src/mistapi/__api_session.py

Lines changed: 70 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -278,9 +278,10 @@ def _load_keyring(self, keyring_service) -> None:
278278
for token in mist_apitoken.split(","):
279279
token = token.strip()
280280
LOGGER.info(
281-
"apisession:_load_keyring: Found MIST_APITOKEN=%s...%s",
282-
token[:4],
283-
token[-4:],
281+
"apisession:_load_keyring: Found MIST_APITOKEN=%s",
282+
_apitoken_sanitizer(
283+
token
284+
), # lgtm[py/clear-text-logging-sensitive-data]
284285
)
285286
self.set_api_token(mist_apitoken)
286287
mist_user = keyring.get_password(keyring_service, "MIST_USER")
@@ -536,9 +537,10 @@ def _get_api_token_data(self, apitoken) -> tuple[str | None, list | None]:
536537
)
537538
data_json = data.json()
538539
LOGGER.debug(
539-
"apisession:_get_api_token_data:info retrieved for token %s...%s",
540-
apitoken[:4],
541-
apitoken[-4:],
540+
"apisession:_get_api_token_data:info retrieved for token %s",
541+
_apitoken_sanitizer(
542+
apitoken
543+
), # lgtm[py/clear-text-logging-sensitive-data]
542544
)
543545
except requests.exceptions.ProxyError as proxy_error:
544546
LOGGER.critical("apisession:_get_api_token_data:proxy not valid...")
@@ -554,10 +556,10 @@ def _get_api_token_data(self, apitoken) -> tuple[str | None, list | None]:
554556
) from connexion_error
555557
except Exception:
556558
LOGGER.error(
557-
"apisession:_get_api_token_data:"
558-
"unable to retrieve info for token %s...%s",
559-
apitoken[:4],
560-
apitoken[-4:],
559+
"apisession:_get_api_token_data:unable to retrieve info for token %s",
560+
_apitoken_sanitizer(
561+
apitoken
562+
), # lgtm[py/clear-text-logging-sensitive-data]
561563
)
562564
LOGGER.error(
563565
"apirequest:_get_api_token_data: Exception occurred", exc_info=True
@@ -566,20 +568,21 @@ def _get_api_token_data(self, apitoken) -> tuple[str | None, list | None]:
566568

567569
if data.status_code == 401:
568570
LOGGER.critical(
569-
"apisession:_get_api_token_data:"
570-
"invalid API Token %s...%s: status code %s",
571-
apitoken[:4],
572-
apitoken[-4:],
571+
"apisession:_get_api_token_data:invalid API Token %s: status code %s",
572+
_apitoken_sanitizer(
573+
apitoken
574+
), # lgtm[py/clear-text-logging-sensitive-data]
573575
data.status_code,
574576
)
575577
CONSOLE.critical(
576-
"Invalid API Token %s...%s: status code %s\r\n",
577-
apitoken[:4],
578-
apitoken[-4:],
578+
"Invalid API Token %s: status code %s\r\n",
579+
_apitoken_sanitizer(
580+
apitoken
581+
), # lgtm[py/clear-text-logging-sensitive-data]
579582
data.status_code,
580583
)
581584
raise ValueError(
582-
f"Invalid API Token {apitoken[:4]}...{apitoken[-4:]}: status code {data.status_code}"
585+
f"Invalid API Token {_apitoken_sanitizer(apitoken)}: status code {data.status_code}"
583586
)
584587

585588
if data_json.get("email"):
@@ -604,11 +607,12 @@ def _get_api_token_data(self, apitoken) -> tuple[str | None, list | None]:
604607
LOGGER.error(
605608
"apisession:_check_api_tokens:"
606609
"unable to process privileges %s for the %s "
607-
"token %s...%s",
610+
"token %s",
608611
priv,
609612
token_type,
610-
apitoken[:4],
611-
apitoken[-4:],
613+
_apitoken_sanitizer(
614+
apitoken
615+
), # lgtm[py/clear-text-logging-sensitive-data]
612616
)
613617
return (token_type, token_privileges)
614618

@@ -626,32 +630,38 @@ def _check_api_tokens(self, apitokens) -> list[str]:
626630
primary_token_type: str | None = ""
627631
primary_token_value: str = ""
628632
for token in apitokens:
629-
not_sensitive_data = f"{token[:4]}...{token[-4:]}"
630633
if token in valid_api_tokens:
631634
LOGGER.info(
632635
"apisession:_check_api_tokens:API Token %s is already valid",
633-
not_sensitive_data,
636+
_apitoken_sanitizer(
637+
token
638+
), # lgtm[py/clear-text-logging-sensitive-data]
634639
)
635640
continue
636641
(token_type, token_privileges) = self._get_api_token_data(token)
637642
if token_type is None or token_privileges is None:
638643
LOGGER.error(
639644
"apisession:_check_api_tokens:API Token %s is not valid",
640-
not_sensitive_data,
645+
_apitoken_sanitizer(
646+
token
647+
), # lgtm[py/clear-text-logging-sensitive-data]
641648
)
642649
LOGGER.error(
643650
"API Token %s is not valid and will not be used",
644-
not_sensitive_data,
651+
_apitoken_sanitizer(
652+
token
653+
), # lgtm[py/clear-text-logging-sensitive-data]
645654
)
646655
elif len(primary_token_privileges) == 0 and token_privileges:
647656
primary_token_privileges = token_privileges
648657
primary_token_type = token_type
649-
primary_token_value = not_sensitive_data
650658
valid_api_tokens.append(token)
651659
LOGGER.info(
652660
"apisession:_check_api_tokens:"
653661
"API Token %s set as primary for comparison",
654-
not_sensitive_data,
662+
_apitoken_sanitizer(
663+
token
664+
), # lgtm[py/clear-text-logging-sensitive-data]
655665
)
656666
elif primary_token_privileges == token_privileges:
657667
valid_api_tokens.append(token)
@@ -660,23 +670,33 @@ def _check_api_tokens(self, apitokens) -> list[str]:
660670
"%s API Token %s has same privileges as "
661671
"the %s API Token %s",
662672
token_type,
663-
not_sensitive_data,
673+
_apitoken_sanitizer(
674+
token
675+
), # lgtm[py/clear-text-logging-sensitive-data]
664676
primary_token_type,
665-
primary_token_value,
677+
_apitoken_sanitizer(
678+
token
679+
), # lgtm[py/clear-text-logging-sensitive-data],
666680
)
667681
else:
668682
LOGGER.error(
669683
"apisession:_check_api_tokens:"
670684
"%s API Token %s has different privileges "
671685
"than the %s API Token %s",
672686
token_type,
673-
not_sensitive_data,
687+
_apitoken_sanitizer(
688+
token
689+
), # lgtm[py/clear-text-logging-sensitive-data]
674690
primary_token_type,
675-
primary_token_value,
691+
_apitoken_sanitizer(
692+
token
693+
), # lgtm[py/clear-text-logging-sensitive-data]
676694
)
677695
LOGGER.error(
678696
"API Token %s has different privileges and will not be used",
679-
not_sensitive_data,
697+
_apitoken_sanitizer(
698+
token
699+
), # lgtm[py/clear-text-logging-sensitive-data]
680700
)
681701
return valid_api_tokens
682702

@@ -1233,3 +1253,21 @@ def get_privilege_by_org_id(self, org_id: str):
12331253
"msp_logo_url": resp.data.get("logo_url"),
12341254
}
12351255
return {}
1256+
1257+
1258+
def _apitoken_sanitizer(apitoken: str) -> str:
1259+
"""
1260+
Return a substring of the API token to be used in the logs, to avoid
1261+
logging the full token value.
1262+
1263+
PARAMS
1264+
-----------
1265+
apitoken : str
1266+
API token value
1267+
1268+
RETURN
1269+
-----------
1270+
str
1271+
Substring of the API token to be used in the logs
1272+
"""
1273+
return f"{apitoken[:4]}...{apitoken[-4:]}" # lgtm[py/clear-text-logging-sensitive-data]
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"""
2+
--------------------------------------------------------------------------------
3+
------------------------- Mist API Python CLI Session --------------------------
4+
5+
Written by: Thomas Munzer (tmunzer@juniper.net)
6+
Github : https://github.com/tmunzer/mistapi_python
7+
8+
This package is licensed under the MIT License.
9+
10+
--------------------------------------------------------------------------------
11+
"""
12+
13+
from enum import Enum
14+
15+
16+
class Node(Enum):
17+
"""Node enum for specifying which node to target on dual-node devices."""
18+
19+
NODE0 = "node0"
20+
NODE1 = "node1"

0 commit comments

Comments
 (0)