diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ead578b..342aa532 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,47 @@ # CHANGELOG +## Version 0.64.1 (October 2026) + +This patch release expands organization and site API coverage, updates generated API bindings, and fixes pagination when a next-page request returns no response. + +--- + +### 1. NEW FEATURES + +#### **Organization APIs** +- Added organization security zone list, get, create, update, and delete operations. +- Added organization SCEP event search and organization variable counting. +- Added organization client fingerprint count and search operations. + +#### **Site APIs** +- Added helpers to start and stop site flow capture. +- Added site map stack list, get, create, update, and delete operations. + +--- + +### 2. IMPROVEMENTS + +- Expanded organization inventory search filters. +- Added MAC and AP MAC filters to site call analytics and updated the call rating type. +- Added an optional AP MAC filter to site channel score queries. +- Updated generated organization and site API bindings. + +--- + +### 3. BREAKING CHANGES + +#### **Client Fingerprint API Scope** +Client fingerprint count and search helpers are now exposed at the organization level as `countOrgClientFingerprints()` and `searchOrgClientFingerprints()`. The former site-level helpers have been removed. + +--- + +### 4. BUG FIXES + +#### **Pagination Termination** +`get_all()` now stops when fetching a next page returns `None`, preventing repeated requests against the same next-page link. + +--- + ## Version 0.64.0 (September 2026) **Released**: September 15, 2026 diff --git a/mist_openapi b/mist_openapi index 1135d908..79274e6c 160000 --- a/mist_openapi +++ b/mist_openapi @@ -1 +1 @@ -Subproject commit 1135d9080ec37a98ac0a1dfc23bb9667e4a06de1 +Subproject commit 79274e6cd85327ef51a9ab99a9c40a999d4b669a diff --git a/pyproject.toml b/pyproject.toml index 454552b3..58f852ca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mistapi" -version = "0.64.0" +version = "0.64.1" authors = [{ name = "Thomas Munzer", email = "tmunzer@juniper.net" }] description = "Python package to simplify the Mist System APIs usage" keywords = ["Mist", "Juniper", "API"] diff --git a/src/mistapi/__pagination.py b/src/mistapi/__pagination.py index 567e27f3..795dd040 100644 --- a/src/mistapi/__pagination.py +++ b/src/mistapi/__pagination.py @@ -60,6 +60,8 @@ def get_all(mist_session: _APISession, response: _APIResponse) -> list: if tmp: response = tmp data += response.data + else: + break elif isinstance(response.data, dict) and "results" in response.data: data = response.data["results"].copy() while response.next: @@ -67,4 +69,6 @@ def get_all(mist_session: _APISession, response: _APIResponse) -> list: if tmp: response = tmp data += response.data["results"] + else: + break return data diff --git a/src/mistapi/__version.py b/src/mistapi/__version.py index 3c35ec2b..cb8f30ff 100644 --- a/src/mistapi/__version.py +++ b/src/mistapi/__version.py @@ -1,2 +1,2 @@ -__version__ = "0.64.0" +__version__ = "0.64.1" __author__ = "Thomas Munzer " diff --git a/src/mistapi/api/v1/orgs/__init__.py b/src/mistapi/api/v1/orgs/__init__.py index 6f5f8592..6b213188 100644 --- a/src/mistapi/api/v1/orgs/__init__.py +++ b/src/mistapi/api/v1/orgs/__init__.py @@ -68,6 +68,7 @@ sdktemplates, secintelprofiles, secpolicies, + securityzones, servicepolicies, services, setting, @@ -155,6 +156,7 @@ "sdktemplates", "secintelprofiles", "secpolicies", + "securityzones", "servicepolicies", "services", "setting", diff --git a/src/mistapi/api/v1/orgs/insights.py b/src/mistapi/api/v1/orgs/insights.py index 90b00825..64162444 100644 --- a/src/mistapi/api/v1/orgs/insights.py +++ b/src/mistapi/api/v1/orgs/insights.py @@ -14,6 +14,163 @@ from mistapi.__api_response import APIResponse as _APIResponse +def countOrgClientFingerprints( + mist_session: _APISession, + org_id: str, + distinct: str | None = None, + start: str | None = None, + end: str | None = None, + duration: str | None = None, + limit: int | None = None, +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/nac-fingerprints/count-org-client-fingerprints + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + + QUERY PARAMS + ------------ + distinct : str{'family', 'model', 'os', 'os_type'}, default: family + Field used to group this count response. enum: `family`, `model`, `os`, `os_type` + start : str + Lower bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d` or `-1w` + end : str + Upper bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d`, `-2h`, or `now` + duration : str, default: 1d + Time range duration for the query, using relative units such as `10m`, `7d`, or `2w` + limit : int, default: 100 + Maximum number of results to return per page + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/insights/fingerprints/count" + query_params: dict[str, str] = {} + if distinct: + query_params["distinct"] = str(distinct) + if start: + query_params["start"] = str(start) + if end: + query_params["end"] = str(end) + if duration: + query_params["duration"] = str(duration) + if limit: + query_params["limit"] = str(limit) + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + +def searchOrgClientFingerprints( + mist_session: _APISession, + org_id: str, + family: str | None = None, + client_type: str | None = None, + model: str | None = None, + mfg: str | None = None, + os: str | None = None, + os_type: str | None = None, + mac: str | None = None, + limit: int | None = None, + start: str | None = None, + end: str | None = None, + duration: str | None = None, + interval: str | None = None, + sort: str | None = None, + search_after: str | None = None, +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/nac-fingerprints/search-org-client-fingerprints + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + + QUERY PARAMS + ------------ + family : str + Device Category of the client device + client_type : str{'wireless', 'wired', 'vty'} + Filter results by client type. enum: `wireless`, `wired`, `vty` + model : str + Filter results by device model + mfg : str + Manufacturer name of the client device + os : str + Operating System name and version of the client device + os_type : str + Operating system name of the client device + mac : str + Filter results by MAC address + limit : int, default: 100 + Maximum number of results to return per page + start : str + Lower bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d` or `-1w` + end : str + Upper bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d`, `-2h`, or `now` + duration : str, default: 1d + Time range duration for the query, using relative units such as `10m`, `7d`, or `2w` + interval : str + Aggregation works by giving a time range plus interval (e.g. 1d, 1h, 10m) where aggregation function would be applied to. + sort : str, default: wxid + On which field the list should be sorted, -prefix represents DESC order. + search_after : str + Pagination cursor for retrieving subsequent pages of results. This value is automatically populated by Mist in the `next` URL from the previous response and should not be manually constructed. + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/insights/fingerprints/search" + query_params: dict[str, str] = {} + if family: + query_params["family"] = str(family) + if client_type: + query_params["client_type"] = str(client_type) + if model: + query_params["model"] = str(model) + if mfg: + query_params["mfg"] = str(mfg) + if os: + query_params["os"] = str(os) + if os_type: + query_params["os_type"] = str(os_type) + if mac: + query_params["mac"] = str(mac) + if limit: + query_params["limit"] = str(limit) + if start: + query_params["start"] = str(start) + if end: + query_params["end"] = str(end) + if duration: + query_params["duration"] = str(duration) + if interval: + query_params["interval"] = str(interval) + if sort: + query_params["sort"] = str(sort) + if search_after: + query_params["search_after"] = str(search_after) + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + def getOrgMarvisClientInsights( mist_session: _APISession, org_id: str, diff --git a/src/mistapi/api/v1/orgs/inventory.py b/src/mistapi/api/v1/orgs/inventory.py index 7e09a399..4632461c 100644 --- a/src/mistapi/api/v1/orgs/inventory.py +++ b/src/mistapi/api/v1/orgs/inventory.py @@ -355,6 +355,8 @@ def searchOrgInventory( org_id: str, type: str | None = None, mac: str | None = None, + vc_mac: str | None = None, + master_mac: str | None = None, model: str | None = None, name: str | None = None, site_id: str | None = None, @@ -365,6 +367,8 @@ def searchOrgInventory( version: str | None = None, status: str | None = None, text: str | None = None, + modified_after: int | None = None, + disconnected_before: int | None = None, limit: int | None = None, sort: str | None = None, search_after: str | None = None, @@ -387,6 +391,10 @@ def searchOrgInventory( Filter results by type. enum: `ap`, `gateway`, `switch` mac : str Filter by MAC address. Partial matches may use `*` wildcards (e.g. `*5b35*` matches `5c5b350e0001` and `5c5b35000301`). Accepts multiple comma-separated values. + vc_mac : str + Virtual Chassis MAC address. Accepts multiple comma-separated values. + master_mac : str + MAC address of the Virtual Chassis master device. Accepts multiple comma-separated values. model : str Partial / full Device model. Use `prefix*` for prefix search or `*substring*` for contains search (e.g. `AP4*` and `*P4*` match `AP43`). Suffix-only wildcards (e.g. `*43`) are not supported. Accepts multiple comma-separated values. name : str @@ -407,6 +415,10 @@ def searchOrgInventory( Device status. enum: `connected`, `disconnected`. Accepts multiple comma-separated values. text : str Wildcards for name, mac, serial + modified_after : int + Filter on inventory last modified time, in epoch + disconnected_before : int + Filter results to devices that were last disconnected before this time, in epoch seconds limit : int, default: 100 Maximum number of results to return per page sort : str, default: timestamp @@ -426,6 +438,10 @@ def searchOrgInventory( query_params["type"] = str(type) if mac: query_params["mac"] = str(mac) + if vc_mac: + query_params["vc_mac"] = str(vc_mac) + if master_mac: + query_params["master_mac"] = str(master_mac) if model: query_params["model"] = str(model) if name: @@ -446,6 +462,10 @@ def searchOrgInventory( query_params["status"] = str(status) if text: query_params["text"] = str(text) + if modified_after: + query_params["modified_after"] = str(modified_after) + if disconnected_before: + query_params["disconnected_before"] = str(disconnected_before) if limit: query_params["limit"] = str(limit) if sort: diff --git a/src/mistapi/api/v1/orgs/securityzones.py b/src/mistapi/api/v1/orgs/securityzones.py new file mode 100644 index 00000000..02ff59e4 --- /dev/null +++ b/src/mistapi/api/v1/orgs/securityzones.py @@ -0,0 +1,174 @@ +""" +-------------------------------------------------------------------------------- +------------------------- Mist API Python CLI Session -------------------------- + + Written by: Thomas Munzer (tmunzer@juniper.net) + Github : https://github.com/tmunzer/mistapi_python + + This package is licensed under the MIT License. + +-------------------------------------------------------------------------------- +""" + +from mistapi import APISession as _APISession +from mistapi.__api_response import APIResponse as _APIResponse + + +def listOrgSecurityZones( + mist_session: _APISession, + org_id: str, + limit: int | None = None, + page: int | None = None, +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/security-zones/list-org-security-zones + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + + QUERY PARAMS + ------------ + limit : int, default: 100 + Maximum number of results to return per page + page : int, default: 1 + Select the page number to return when using page-based pagination; starts at `1` + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/securityzones" + query_params: dict[str, str] = {} + if limit: + query_params["limit"] = str(limit) + if page: + query_params["page"] = str(page) + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + +def createOrgSecurityZone( + mist_session: _APISession, org_id: str, body: dict | list +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/security-zones/create-org-security-zone + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + + BODY PARAMS + ----------- + body : dict + JSON object to send to Mist Cloud (see API doc above for more details) + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/securityzones" + resp = mist_session.mist_post(uri=uri, body=body) + return resp + + +def getOrgSecurityZone( + mist_session: _APISession, org_id: str, securityzone_id: str +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/security-zones/get-org-security-zone + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + securityzone_id : str + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/securityzones/{securityzone_id}" + query_params: dict[str, str] = {} + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + +def deleteOrgSecurityZone( + mist_session: _APISession, org_id: str, securityzone_id: str +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/security-zones/delete-org-security-zone + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + securityzone_id : str + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/securityzones/{securityzone_id}" + query_params: dict[str, str] = {} + resp = mist_session.mist_delete(uri=uri, query=query_params) + return resp + + +def updateOrgSecurityZone( + mist_session: _APISession, org_id: str, securityzone_id: str, body: dict +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/security-zones/update-org-security-zone + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + securityzone_id : str + + BODY PARAMS + ----------- + body : dict + JSON object to send to Mist Cloud (see API doc above for more details) + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/securityzones/{securityzone_id}" + resp = mist_session.mist_put(uri=uri, body=body) + return resp diff --git a/src/mistapi/api/v1/orgs/setting.py b/src/mistapi/api/v1/orgs/setting.py index d9dd5a91..906818ec 100644 --- a/src/mistapi/api/v1/orgs/setting.py +++ b/src/mistapi/api/v1/orgs/setting.py @@ -708,6 +708,72 @@ def revokeOrgIssuedClientCertificates( return resp +def searchOrgScepEvents( + mist_session: _APISession, + org_id: str, + type: str | None = None, + cert_provider: str | None = None, + common_name: str | None = None, + device_id: str | None = None, + text: str | None = None, + limit: int | None = None, + page: int | None = None, +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/scep/search-org-scep-events + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + + QUERY PARAMS + ------------ + type : str{'failure', 'success'} + Filter by event type. enum: `failure`, `success` + cert_provider : str + Filter by the MDM or certificate provider that triggered the operation + common_name : str + Partial match against the certificate common name, such as a user UPN + device_id : str + Filter by the device identifier associated with the SCEP operation + text : str + Keyword search within the event reason text + limit : int, default: 100 + Maximum number of results to return per page + page : int, default: 1 + Select the page number to return when using page-based pagination; starts at `1` + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/setting/mist_scep/events/search" + query_params: dict[str, str] = {} + if type: + query_params["type"] = str(type) + if cert_provider: + query_params["cert_provider"] = str(cert_provider) + if common_name: + query_params["common_name"] = str(common_name) + if device_id: + query_params["device_id"] = str(device_id) + if text: + query_params["text"] = str(text) + if limit: + query_params["limit"] = str(limit) + if page: + query_params["page"] = str(page) + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + def setOrgCustomBucket( mist_session: _APISession, org_id: str, body: dict | list ) -> _APIResponse: diff --git a/src/mistapi/api/v1/orgs/vars.py b/src/mistapi/api/v1/orgs/vars.py index 19c111bf..00217b5c 100644 --- a/src/mistapi/api/v1/orgs/vars.py +++ b/src/mistapi/api/v1/orgs/vars.py @@ -14,6 +14,77 @@ from mistapi.__api_response import APIResponse as _APIResponse +def countOrgVars( + mist_session: _APISession, + org_id: str, + distinct: str | None = None, + site_id: str | None = None, + var: str | None = None, + src: str | None = None, + start: str | None = None, + end: str | None = None, + duration: str | None = None, + limit: int | None = None, +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/orgs/vars/count-org-vars + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + org_id : str + + QUERY PARAMS + ------------ + distinct : str{'site_id', 'src', 'var'}, default: var + Field used to group this count response. enum: `site_id`, `src`, `var` + site_id : str + Filter results by site identifier. Accepts multiple comma-separated values. + var : str + Filter variable results by variable name. Accepts multiple comma-separated values. + src : str{'deviceprofile', 'site'} + Filter results by source. enum: `deviceprofile`, `site` + start : str + Lower bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d` or `-1w` + end : str + Upper bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d`, `-2h`, or `now` + duration : str, default: 1d + Time range duration for the query, using relative units such as `10m`, `7d`, or `2w` + limit : int, default: 100 + Maximum number of results to return per page + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/orgs/{org_id}/vars/count" + query_params: dict[str, str] = {} + if distinct: + query_params["distinct"] = str(distinct) + if site_id: + query_params["site_id"] = str(site_id) + if var: + query_params["var"] = str(var) + if src: + query_params["src"] = str(src) + if start: + query_params["start"] = str(start) + if end: + query_params["end"] = str(end) + if duration: + query_params["duration"] = str(duration) + if limit: + query_params["limit"] = str(limit) + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + def searchOrgVars( mist_session: _APISession, org_id: str, diff --git a/src/mistapi/api/v1/sites/__init__.py b/src/mistapi/api/v1/sites/__init__.py index e1344ab0..c3e9cd87 100644 --- a/src/mistapi/api/v1/sites/__init__.py +++ b/src/mistapi/api/v1/sites/__init__.py @@ -30,6 +30,7 @@ devices, events, evpn_topologies, + flow_capture, gatewaytemplates, guests, idpprofiles, @@ -101,6 +102,7 @@ "devices", "events", "evpn_topologies", + "flow_capture", "gatewaytemplates", "guests", "idpprofiles", diff --git a/src/mistapi/api/v1/sites/flow_capture.py b/src/mistapi/api/v1/sites/flow_capture.py new file mode 100644 index 00000000..9472509d --- /dev/null +++ b/src/mistapi/api/v1/sites/flow_capture.py @@ -0,0 +1,95 @@ +""" +-------------------------------------------------------------------------------- +------------------------- Mist API Python CLI Session -------------------------- + + Written by: Thomas Munzer (tmunzer@juniper.net) + Github : https://github.com/tmunzer/mistapi_python + + This package is licensed under the MIT License. + +-------------------------------------------------------------------------------- +""" + +from mistapi import APISession as _APISession +from mistapi.__api_response import APIResponse as _APIResponse + + +def getSiteFlowCaptureStatus(mist_session: _APISession, site_id: str) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/utilities/pcaps/get-site-flow-capture-status + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + site_id : str + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/sites/{site_id}/flow_capture" + query_params: dict[str, str] = {} + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + +def stopSiteFlowCapture(mist_session: _APISession, site_id: str) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/utilities/pcaps/stop-site-flow-capture + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + site_id : str + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/sites/{site_id}/flow_capture" + query_params: dict[str, str] = {} + resp = mist_session.mist_delete(uri=uri, query=query_params) + return resp + + +def startSiteFlowCapture( + mist_session: _APISession, site_id: str, body: dict | list +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/utilities/pcaps/start-site-flow-capture + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + site_id : str + + BODY PARAMS + ----------- + body : dict + JSON object to send to Mist Cloud (see API doc above for more details) + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/sites/{site_id}/flow_capture" + resp = mist_session.mist_post(uri=uri, body=body) + return resp diff --git a/src/mistapi/api/v1/sites/insights.py b/src/mistapi/api/v1/sites/insights.py index f9125b52..07bebe38 100644 --- a/src/mistapi/api/v1/sites/insights.py +++ b/src/mistapi/api/v1/sites/insights.py @@ -287,163 +287,6 @@ def getSiteInsightMetricsForDevice( return resp -def countSiteClientFingerprints( - mist_session: _APISession, - site_id: str, - distinct: str | None = None, - start: str | None = None, - end: str | None = None, - duration: str | None = None, - limit: int | None = None, -) -> _APIResponse: - """ - API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/sites/nac-fingerprints/count-site-client-fingerprints - - PARAMS - ----------- - mistapi.APISession : mist_session - mistapi session including authentication and Mist host information - - PATH PARAMS - ----------- - site_id : str - - QUERY PARAMS - ------------ - distinct : str{'family', 'model', 'os', 'os_type'}, default: family - Field used to group this count response. enum: `family`, `model`, `os`, `os_type` - start : str - Lower bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d` or `-1w` - end : str - Upper bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d`, `-2h`, or `now` - duration : str, default: 1d - Time range duration for the query, using relative units such as `10m`, `7d`, or `2w` - limit : int, default: 100 - Maximum number of results to return per page - - RETURN - ----------- - mistapi.APIResponse - response from the API call - """ - - uri = f"/api/v1/sites/{site_id}/insights/fingerprints/count" - query_params: dict[str, str] = {} - if distinct: - query_params["distinct"] = str(distinct) - if start: - query_params["start"] = str(start) - if end: - query_params["end"] = str(end) - if duration: - query_params["duration"] = str(duration) - if limit: - query_params["limit"] = str(limit) - resp = mist_session.mist_get(uri=uri, query=query_params) - return resp - - -def searchSiteClientFingerprints( - mist_session: _APISession, - site_id: str, - family: str | None = None, - client_type: str | None = None, - model: str | None = None, - mfg: str | None = None, - os: str | None = None, - os_type: str | None = None, - mac: str | None = None, - limit: int | None = None, - start: str | None = None, - end: str | None = None, - duration: str | None = None, - interval: str | None = None, - sort: str | None = None, - search_after: str | None = None, -) -> _APIResponse: - """ - API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/sites/nac-fingerprints/search-site-client-fingerprints - - PARAMS - ----------- - mistapi.APISession : mist_session - mistapi session including authentication and Mist host information - - PATH PARAMS - ----------- - site_id : str - - QUERY PARAMS - ------------ - family : str - Device Category of the client device - client_type : str{'wireless', 'wired', 'vty'} - Filter results by client type. enum: `wireless`, `wired`, `vty` - model : str - Filter results by device model - mfg : str - Manufacturer name of the client device - os : str - Operating System name and version of the client device - os_type : str - Operating system name of the client device - mac : str - Filter results by MAC address - limit : int, default: 100 - Maximum number of results to return per page - start : str - Lower bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d` or `-1w` - end : str - Upper bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d`, `-2h`, or `now` - duration : str, default: 1d - Time range duration for the query, using relative units such as `10m`, `7d`, or `2w` - interval : str - Aggregation works by giving a time range plus interval (e.g. 1d, 1h, 10m) where aggregation function would be applied to. - sort : str, default: wxid - On which field the list should be sorted, -prefix represents DESC order. - search_after : str - Pagination cursor for retrieving subsequent pages of results. This value is automatically populated by Mist in the `next` URL from the previous response and should not be manually constructed. - - RETURN - ----------- - mistapi.APIResponse - response from the API call - """ - - uri = f"/api/v1/sites/{site_id}/insights/fingerprints/search" - query_params: dict[str, str] = {} - if family: - query_params["family"] = str(family) - if client_type: - query_params["client_type"] = str(client_type) - if model: - query_params["model"] = str(model) - if mfg: - query_params["mfg"] = str(mfg) - if os: - query_params["os"] = str(os) - if os_type: - query_params["os_type"] = str(os_type) - if mac: - query_params["mac"] = str(mac) - if limit: - query_params["limit"] = str(limit) - if start: - query_params["start"] = str(start) - if end: - query_params["end"] = str(end) - if duration: - query_params["duration"] = str(duration) - if interval: - query_params["interval"] = str(interval) - if sort: - query_params["sort"] = str(sort) - if search_after: - query_params["search_after"] = str(search_after) - resp = mist_session.mist_get(uri=uri, query=query_params) - return resp - - def getSiteInsightMetricsForGateway( mist_session: _APISession, site_id: str, diff --git a/src/mistapi/api/v1/sites/mapstacks.py b/src/mistapi/api/v1/sites/mapstacks.py index ec3846f8..3b3bfda3 100644 --- a/src/mistapi/api/v1/sites/mapstacks.py +++ b/src/mistapi/api/v1/sites/mapstacks.py @@ -89,3 +89,91 @@ def createSiteMapStack( uri = f"/api/v1/sites/{site_id}/mapstacks" resp = mist_session.mist_post(uri=uri, body=body) return resp + + +def getSiteMapStack( + mist_session: _APISession, site_id: str, mapstack_id: str +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/sites/map-stacks/get-site-map-stack + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + site_id : str + mapstack_id : str + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/sites/{site_id}/mapstacks/{mapstack_id}" + query_params: dict[str, str] = {} + resp = mist_session.mist_get(uri=uri, query=query_params) + return resp + + +def deleteSiteMapStack( + mist_session: _APISession, site_id: str, mapstack_id: str +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/sites/map-stacks/delete-site-map-stack + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + site_id : str + mapstack_id : str + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/sites/{site_id}/mapstacks/{mapstack_id}" + query_params: dict[str, str] = {} + resp = mist_session.mist_delete(uri=uri, query=query_params) + return resp + + +def updateSiteMapStack( + mist_session: _APISession, site_id: str, mapstack_id: str, body: dict +) -> _APIResponse: + """ + API doc: https://www.juniper.net/documentation/us/en/software/mist/api/http/api/sites/map-stacks/update-site-map-stack + + PARAMS + ----------- + mistapi.APISession : mist_session + mistapi session including authentication and Mist host information + + PATH PARAMS + ----------- + site_id : str + mapstack_id : str + + BODY PARAMS + ----------- + body : dict + JSON object to send to Mist Cloud (see API doc above for more details) + + RETURN + ----------- + mistapi.APIResponse + response from the API call + """ + + uri = f"/api/v1/sites/{site_id}/mapstacks/{mapstack_id}" + resp = mist_session.mist_put(uri=uri, body=body) + return resp diff --git a/src/mistapi/api/v1/sites/rrm.py b/src/mistapi/api/v1/sites/rrm.py index d7e9d66f..53e30786 100644 --- a/src/mistapi/api/v1/sites/rrm.py +++ b/src/mistapi/api/v1/sites/rrm.py @@ -18,6 +18,7 @@ def getSiteChannelScores( mist_session: _APISession, site_id: str, band: str, + ap: str | None = None, start: str | None = None, end: str | None = None, ) -> _APIResponse: @@ -37,6 +38,8 @@ def getSiteChannelScores( QUERY PARAMS ------------ + ap : str + AP MAC address. When supplied, returns per-AP channel scores instead of site-wide scores. start : str Lower bound of the time range, as an epoch timestamp in seconds or a relative value such as `-1d` or `-1w` end : str @@ -50,6 +53,8 @@ def getSiteChannelScores( uri = f"/api/v1/sites/{site_id}/rrm/channel_scores/band/{band}" query_params: dict[str, str] = {} + if ap: + query_params["ap"] = str(ap) if start: query_params["start"] = str(start) if end: diff --git a/src/mistapi/api/v1/sites/sle.py b/src/mistapi/api/v1/sites/sle.py index 4b9b5c70..3ae23468 100644 --- a/src/mistapi/api/v1/sites/sle.py +++ b/src/mistapi/api/v1/sites/sle.py @@ -18,7 +18,7 @@ @deprecation.deprecated( deprecated_in="0.59.2", removed_in="0.65.0", - current_version="0.64.0", + current_version="0.64.1", details="function replaced with getSiteSleClassifierSummaryTrend", ) def getSiteSleClassifierDetails( @@ -764,7 +764,7 @@ def listSiteSleImpactedWirelessClients( @deprecation.deprecated( deprecated_in="0.59.2", removed_in="0.65.0", - current_version="0.64.0", + current_version="0.64.1", details="function replaced with getSiteSleSummaryTrend", ) def getSiteSleSummary( diff --git a/src/mistapi/api/v1/sites/stats.py b/src/mistapi/api/v1/sites/stats.py index 37e226f6..bce91c2c 100644 --- a/src/mistapi/api/v1/sites/stats.py +++ b/src/mistapi/api/v1/sites/stats.py @@ -682,7 +682,9 @@ def countSiteCalls( mist_session: _APISession, site_id: str, distinct: str | None = None, - rating: int | None = None, + mac: str | None = None, + ap_mac: str | None = None, + rating: str | None = None, app: str | None = None, start: str | None = None, end: str | None = None, @@ -704,8 +706,12 @@ def countSiteCalls( ------------ distinct : str{'mac'}, default: mac Field used to group this count response. enum: `mac` - rating : int - Feedback rating (e.g. "rating=1" or "rating=1,2") + mac : str + Filter results by client MAC address + ap_mac : str + Filter results by AP MAC address + rating : str + Feedback rating, from 1 to 5; accepts comma-separated values such as "1,2" app : str Filter application statistics by application name start : str @@ -725,6 +731,10 @@ def countSiteCalls( query_params: dict[str, str] = {} if distinct: query_params["distinct"] = str(distinct) + if mac: + query_params["mac"] = str(mac) + if ap_mac: + query_params["ap_mac"] = str(ap_mac) if rating: query_params["rating"] = str(rating) if app: @@ -856,7 +866,7 @@ def getSiteCallsSummary( query_params["ap_mac"] = str(ap_mac) if app: query_params["app"] = str(app) - if wired is not None: + if wired: query_params["wired"] = str(wired) if start: query_params["start"] = str(start) diff --git a/tests/unit/test_pagination.py b/tests/unit/test_pagination.py index 1cdfee56..67d71f8a 100644 --- a/tests/unit/test_pagination.py +++ b/tests/unit/test_pagination.py @@ -131,6 +131,23 @@ def test_empty_list_with_next_follows_links(self): assert result == [{"id": "1"}] + def test_stops_when_fetching_next_page_returns_none(self): + """get_all() should stop when fetching a next page returns None.""" + session = Mock() + session.mist_get.side_effect = [ + None, + AssertionError("get_all() retried a page fetch that returned None"), + ] + response = _make_response( + data=[{"id": "1"}], + next_url="/api/v1/items?page=2", + ) + + result = get_all(session, response) + + assert result == [{"id": "1"}] + session.mist_get.assert_called_once_with("/api/v1/items?page=2") + class TestGetAllDict: """Tests for get_all() when response.data is a dict with 'results' key.""" @@ -217,6 +234,23 @@ def test_empty_results_with_next_follows_links(self): assert result == [{"id": "1"}] + def test_stops_when_fetching_next_page_returns_none(self): + """get_all() should stop when fetching a next page returns None.""" + session = Mock() + session.mist_get.side_effect = [ + None, + AssertionError("get_all() retried a page fetch that returned None"), + ] + response = _make_response( + data={"results": [{"id": "1"}]}, + next_url="/api/v1/items?page=2", + ) + + result = get_all(session, response) + + assert result == [{"id": "1"}] + session.mist_get.assert_called_once_with("/api/v1/items?page=2") + class TestGetAllEdgeCases: """Tests for get_all() edge cases and unsupported data types.""" diff --git a/uv.lock b/uv.lock index 2e6b3edc..e95a0221 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.10" [[package]] @@ -658,7 +658,7 @@ wheels = [ [[package]] name = "mistapi" -version = "0.64.0" +version = "0.64.1" source = { editable = "." } dependencies = [ { name = "deprecation" },