Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/generation-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Generation Report

## 2026-05-08 | Library v4.1.0b0 | API 1.70.0-beta.0


No Python keyword parameter conflicts detected.


4 changes: 2 additions & 2 deletions meraki/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@
USE_ITERATOR_FOR_GET_PAGES,
VALIDATE_KWARGS,
)
from meraki.session.sync import RestSession
from meraki.rest_session import RestSession
from meraki.exceptions import APIError, APIKeyError, APIResponseError, AsyncAPIError
from meraki._version import __version__ # noqa: F401
from datetime import datetime

__api_version__ = "1.69.0"
__api_version__ = "1.70.0-beta.0"

__all__ = [
"APIError",
Expand Down
2 changes: 1 addition & 1 deletion meraki/_version.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "4.0.0b1"
__version__ = "4.1.0b0"
2 changes: 1 addition & 1 deletion meraki/aio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from meraki.aio.api.switch import AsyncSwitch
from meraki.aio.api.wireless import AsyncWireless
from meraki.aio.api.wirelessController import AsyncWirelessController
from meraki.session.async_ import AsyncRestSession
from meraki.aio.rest_session import AsyncRestSession
from meraki.exceptions import APIKeyError
from datetime import datetime

Expand Down
2 changes: 1 addition & 1 deletion meraki/aio/api/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def clipDeviceCamera(self, serial: str, startTimestamp: str, endTimestamp: str,
- serial (string): Serial
- startTimestamp (string): The start time for the clip. The timestamp is expected to be in ISO 8601 format.
- endTimestamp (string): The end time for the clip. The timestamp is expected to be in ISO 8601 format.
- imagerId (integer): For multi-imager cameras, the imager ID to query. Defaults to '1' if omitted.
- imagerId (integer): The imager ID to query. Required for multi-imager cameras (must be between 1 and the imager count). For single-imager cameras, must be omitted or set to 0.
"""

kwargs.update(locals())
Expand Down
2 changes: 1 addition & 1 deletion meraki/api/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ def clipDeviceCamera(self, serial: str, startTimestamp: str, endTimestamp: str,
- serial (string): Serial
- startTimestamp (string): The start time for the clip. The timestamp is expected to be in ISO 8601 format.
- endTimestamp (string): The end time for the clip. The timestamp is expected to be in ISO 8601 format.
- imagerId (integer): For multi-imager cameras, the imager ID to query. Defaults to '1' if omitted.
- imagerId (integer): The imager ID to query. Required for multi-imager cameras (must be between 1 and the imager count). For single-imager cameras, must be omitted or set to 0.
"""

kwargs.update(locals())
Expand Down
1 change: 0 additions & 1 deletion meraki/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@
SIMULATE_API_CALLS = False

# Number of concurrent API requests for asynchronous class
# Maps to httpx.Limits(max_connections=N) in AsyncRestSession
AIO_MAXIMUM_CONCURRENT_REQUESTS = 8

# Legacy partner identifier for API usage tracking; can also be set as an environment variable BE_GEO_ID
Expand Down
71 changes: 1 addition & 70 deletions meraki/encoding.py
Original file line number Diff line number Diff line change
@@ -1,70 +1 @@
"""Meraki-specific parameter encoding using only stdlib.

This module provides encode_meraki_params(), a pure function replacement for
the monkey-patched requests._encode_params in rest_session.py. Uses only
urllib.parse (no requests dependency). See HTTP-04.
"""

from urllib.parse import urlencode


def encode_meraki_params(data):
"""Encode parameters for Meraki API requests.

Supports:
- str/bytes: passthrough
- file-like (has .read): passthrough
- dict: URL encode with array-of-objects support
- list of 2-tuples: URL encode with array-of-objects support
- other: passthrough

Array-of-objects encoding (Meraki-specific):
{"param[]": [{"key1": "val1"}]} -> "param%5B%5Dkey1=val1"

Args:
data: Parameters to encode. Dict, list of tuples, str, bytes,
file-like object, or any other type (passthrough).

Returns:
URL-encoded string for dict/list inputs, original value for passthrough types.
"""
if isinstance(data, (str, bytes)):
return data
elif hasattr(data, "read"):
return data
elif hasattr(data, "__iter__"):
result = []

# Convert to key-val list (stdlib replacement for requests.utils.to_key_val_list)
if hasattr(data, "items"):
items = list(data.items())
else:
items = list(data)

for k, vs in items:
# Normalize scalar to list
if isinstance(vs, str) or not hasattr(vs, "__iter__"):
vs = [vs]

for v in vs:
if v is not None and not isinstance(v, dict):
# Simple key-value pair
result.append(
(
k.encode("utf-8") if isinstance(k, str) else k,
v.encode("utf-8") if isinstance(v, str) else v,
)
)
elif v is not None:
# Array-of-objects: concatenate dict keys to param name
for k_inner, v_inner in v.items():
result.append(
(
(k + k_inner).encode("utf-8") if isinstance(k, str) else k_inner,
v_inner.encode("utf-8") if isinstance(v_inner, str) else v_inner,
)
)

return urlencode(result, doseq=True)
else:
return data
404: Not Found
51 changes: 14 additions & 37 deletions meraki/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ def __init__(self, metadata, response):
self.tag = metadata["tags"][0]
self.operation = metadata["operation"]
self.status = self.response.status_code if self.response is not None and self.response.status_code else None
self.reason = (
self.response.reason_phrase if self.response is not None and hasattr(self.response, "reason_phrase") else None
)
self.reason = self.response.reason if self.response is not None and self.response.reason else None
try:
self.message = self.response.json() if self.response is not None and self.response.json() else None
except ValueError:
Expand All @@ -55,41 +53,20 @@ def __repr__(self):


# To catch exceptions while making AIO API calls
class AsyncAPIError(APIError):
"""Deprecated: Use APIError for both sync and async exceptions.

This exception is deprecated as of version 4.0. Catch APIError instead,
which now handles both synchronous and asynchronous errors.

Existing code using ``except AsyncAPIError:`` will continue to work
because AsyncAPIError is now a subclass of APIError.
"""

def __init__(self, metadata, response, message=None):
import warnings

warnings.warn(
"AsyncAPIError is deprecated. Catch APIError instead, which now handles both sync and async errors.",
DeprecationWarning,
stacklevel=2,
)
class AsyncAPIError(Exception):
def __init__(self, metadata, response, message):
self.response = response
self.tag = metadata["tags"][0]
self.operation = metadata["operation"]
self.status = response.status if response is not None and response.status else None
self.reason = response.reason if response is not None and response.reason else None
self.message = message
if isinstance(self.message, str):
self.message = self.message.strip()
if self.status == 404 and self.reason == "Not Found":
self.message += "please wait a minute if the key or org was just newly created."

if message is not None:
# Old 3-arg form: replicate original AsyncAPIError logic
self.response = response
self.tag = metadata["tags"][0]
self.operation = metadata["operation"]
self.status = response.status_code if response is not None and hasattr(response, "status_code") else None
self.reason = response.reason_phrase if response is not None and hasattr(response, "reason_phrase") else None
self.message = message
if isinstance(self.message, str):
self.message = self.message.strip()
if self.status == 404 and self.reason == "Not Found":
self.message += "please wait a minute if the key or org was just newly created."
Exception.__init__(self, f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}")
else:
# New 2-arg form: delegate to APIError
super().__init__(metadata, response)
super().__init__(f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}")

def __repr__(self):
return f"{self.tag}, {self.operation} - {self.status} {self.reason}, {self.message}"
Expand Down
8 changes: 1 addition & 7 deletions meraki/session/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1 @@
"""Session implementations for Meraki Dashboard API."""

from meraki.session.base import SessionBase
from meraki.session.sync import RestSession
from meraki.session.async_ import AsyncRestSession

__all__ = ["SessionBase", "RestSession", "AsyncRestSession"]
404: Not Found
Loading
Loading