diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index ccd45cc9..26c511b1 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -12,8 +12,9 @@ on: types: [completed] branches: ['main', 'beta', 'httpx'] -# The release is created with a GitHub App token; the default token only needs -# read access for checkout and `gh release view`. +# The release is created with a GitHub App token. This workflow no longer +# commits anything back to the branch (the changelog is rendered in the +# regeneration PR), so the default token only needs read access for checkout. permissions: contents: read @@ -80,6 +81,12 @@ jobs: exit 1 fi + # CHANGELOG.md is rendered from changelog.d/ fragments inside the + # regeneration PR (see regenerate-library.yml), so it is already on the + # branch by the time the release is cut. Committing/pushing here would + # bypass the branch's required status checks and be rejected by the + # ruleset, so no git write happens in this workflow. --generate-notes + # below produces the GitHub release body. - name: Create release if: steps.check-release.outputs.exists == 'false' env: diff --git a/.github/workflows/regenerate-library.yml b/.github/workflows/regenerate-library.yml index 9534bed2..4b73b6c5 100644 --- a/.github/workflows/regenerate-library.yml +++ b/.github/workflows/regenerate-library.yml @@ -101,6 +101,20 @@ jobs: sed -i "s/^version = \".*\"/version = \"$LIBRARY_VERSION\"/" pyproject.toml uv lock + # Render changelog.d/ fragments into CHANGELOG.md here so the update rides + # the regeneration PR (and its required checks) instead of being pushed + # straight to a protected branch later. towncrier is in the dev group, so + # `uv sync --group generator` (default-groups = dev) already installed it. + - name: Build changelog + env: + LIBRARY_VERSION: ${{ github.event.inputs.library_version }} + run: | + if ls changelog.d/*.md >/dev/null 2>&1; then + uv run towncrier build --yes --version "$LIBRARY_VERSION" + else + echo "No changelog fragments; skipping towncrier build." + fi + - name: Commit changes to new branch uses: EndBug/add-and-commit@v10 with: diff --git a/.github/workflows/watch-openapi-release.yml b/.github/workflows/watch-openapi-release.yml index 0c388f3b..70ffd7d6 100644 --- a/.github/workflows/watch-openapi-release.yml +++ b/.github/workflows/watch-openapi-release.yml @@ -25,7 +25,18 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - LATEST=$(gh release list --repo meraki/openapi --exclude-pre-releases --limit 1 --json tagName -q '.[0].tagName') + # Retry: api.github.com occasionally 401s a job's freshly-minted token. + for attempt in 1 2 3 4 5; do + if LATEST=$(gh release list --repo meraki/openapi --exclude-pre-releases --limit 1 --json tagName -q '.[0].tagName'); then + break + fi + if [ "$attempt" = 5 ]; then + echo "::error::gh release list failed after 5 attempts" + exit 1 + fi + echo "::warning::gh release list failed (attempt $attempt/5); retrying in $((attempt * 3))s" + sleep $((attempt * 3)) + done API_VERSION="${LATEST#v}" # Reject anything that isn't a clean semver tag before it flows into # downstream run: steps / workflow_dispatch inputs (script injection guard). @@ -68,7 +79,18 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - LATEST=$(gh release list --repo meraki/openapi --limit 20 --json tagName,isPrerelease -q '[.[] | select(.isPrerelease)] | .[0].tagName') + # Retry: api.github.com occasionally 401s a job's freshly-minted token. + for attempt in 1 2 3 4 5; do + if LATEST=$(gh release list --repo meraki/openapi --limit 20 --json tagName,isPrerelease -q '[.[] | select(.isPrerelease)] | .[0].tagName'); then + break + fi + if [ "$attempt" = 5 ]; then + echo "::error::gh release list failed after 5 attempts" + exit 1 + fi + echo "::warning::gh release list failed (attempt $attempt/5); retrying in $((attempt * 3))s" + sleep $((attempt * 3)) + done if [ -z "$LATEST" ]; then echo "has_new_release=false" >> "$GITHUB_OUTPUT" exit 0 @@ -132,7 +154,18 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - LATEST=$(gh release list --repo meraki/openapi --limit 20 --json tagName,isPrerelease -q '[.[] | select(.isPrerelease)] | .[0].tagName') + # Retry: api.github.com occasionally 401s a job's freshly-minted token. + for attempt in 1 2 3 4 5; do + if LATEST=$(gh release list --repo meraki/openapi --limit 20 --json tagName,isPrerelease -q '[.[] | select(.isPrerelease)] | .[0].tagName'); then + break + fi + if [ "$attempt" = 5 ]; then + echo "::error::gh release list failed after 5 attempts" + exit 1 + fi + echo "::warning::gh release list failed (attempt $attempt/5); retrying in $((attempt * 3))s" + sleep $((attempt * 3)) + done if [ -z "$LATEST" ]; then echo "has_new_release=false" >> "$GITHUB_OUTPUT" exit 0 diff --git a/.gitignore b/.gitignore index 56e0d59b..261f6fac 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,7 @@ venv/ .DS_Store # Project -.scratch/ \ No newline at end of file +.scratch/ + +# Git worktrees +.worktrees/ \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d9586a94 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog + +All notable changes to this library are documented here. + +This file is maintained with [towncrier](https://towncrier.readthedocs.io/); +add a news fragment under `changelog.d/` for every user-facing change. See +[CONTRIBUTING.md](CONTRIBUTING.md) for the fragment format. + + + +## 3.3.0 (2026-07-01) + +### Added + +- On 5xx responses, the SDK now logs the Meraki `X-Request-Id` response header so it can be shared with Meraki to look up the request in server-side logs. If the header is absent, `none` is logged in its place. After retries are exhausted, the request ID is also logged at error level. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 343ad828..f208d604 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,6 +39,27 @@ uv sync - Generated API scope files (`meraki/api/`, `meraki/aio/api/`) are auto-generated from the OpenAPI spec. Changes here will be overwritten. Fix the generator instead. - Do not vendor or bundle dependencies. +## Changelog Fragments + +Every user-facing change needs a news fragment in `changelog.d/`. Do not edit `CHANGELOG.md` by hand; towncrier builds it at release time. + +File name: `changelog.d/{issue}.{type}.md`. With no issue, use a slug prefixed with `+`: `changelog.d/+httpx-migration.changed.md`. + +Types: `added`, `changed`, `deprecated`, `removed`, `fixed`, `security`. + +```bash +# Issue-linked +echo "Retry on 429 now honors Retry-After." > changelog.d/1234.fixed.md + +# Or via towncrier +uv run towncrier create -c "Retry on 429 now honors Retry-After." 1234.fixed.md + +# Preview the rendered changelog without consuming fragments +uv run towncrier build --draft --version "$(grep '^version' pyproject.toml | sed 's/version = "\(.*\)"/\1/')" +``` + +One fragment per change. Multiple fragments per issue are fine (e.g. `1234.added.md` and `1234.fixed.md`). Fragments are not wiped by library regeneration; they survive until the next release build. + ## Running the Generator ```bash diff --git a/changelog.d/.gitignore b/changelog.d/.gitignore new file mode 100644 index 00000000..f935021a --- /dev/null +++ b/changelog.d/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/docs/generation-report.md b/docs/generation-report.md index dea19b9e..5f480fc9 100644 --- a/docs/generation-report.md +++ b/docs/generation-report.md @@ -1,5 +1,11 @@ # Generation Report +## 2026-07-01 | Library v3.3.0 | API 1.72.0 + + +No Python keyword parameter conflicts detected. + + ## 2026-06-04 | Library v3.2.0 | API 1.71.0 diff --git a/docs/releasing.md b/docs/releasing.md index 4de91678..76e0862c 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -46,6 +46,14 @@ Starting from `main` at `3.1.0`, `beta` at `3.1.0`, `httpx` at `4.0.0`: When a new prerelease is detected, the `enable-early-access.yml` workflow runs first (waited on synchronously). This enables early access features on the test organizations so the generated beta and dev SDKs can be tested against them. +## Changelog management + +`CHANGELOG.md` is generated by [towncrier](https://towncrier.readthedocs.io/) from news fragments in `changelog.d/`. Contributors add one fragment per user-facing change (see [CONTRIBUTING.md](../CONTRIBUTING.md) for the format); nobody edits `CHANGELOG.md` directly. + +The `create-release.yml` workflow runs `towncrier build --yes --version ` after version validation and before `gh release create`. The version comes from the same `pyproject.toml` read that drives the release tag, so there is no separate towncrier version to keep in sync. The build renders the fragments into `CHANGELOG.md`, deletes the consumed fragments, and commits both changes to the release branch before the GitHub release is cut. + +This runs only at release time, not during regeneration. `regenerate-library.yml` rewrites generated code under `meraki/` but never touches `changelog.d/` or `CHANGELOG.md`, so fragments added on `main`, `beta`, or `httpx` survive regeneration PRs and are consumed only by the next release on that branch. Each branch keeps its own fragments and accumulates them independently across the three-branch model. + ## Release branches and PRs | Stage | Source branch | Release branch | PR target | diff --git a/generator/generate_library.py b/generator/generate_library.py index 057b2dfa..d1d4268b 100644 --- a/generator/generate_library.py +++ b/generator/generate_library.py @@ -126,6 +126,7 @@ def generate_library( "nac", "users", "support", + "assistant", ] tags = spec["tags"] diff --git a/meraki/__init__.py b/meraki/__init__.py index 9cc2ecae..83fc9508 100644 --- a/meraki/__init__.py +++ b/meraki/__init__.py @@ -52,7 +52,7 @@ from meraki._version import __version__ # noqa: F401 from datetime import datetime -__api_version__ = "1.71.0" +__api_version__ = "1.72.0" __all__ = [ "APIError", diff --git a/meraki/_version.py b/meraki/_version.py index 11731085..88c513ea 100644 --- a/meraki/_version.py +++ b/meraki/_version.py @@ -1 +1 @@ -__version__ = "3.2.0" +__version__ = "3.3.0" diff --git a/meraki/aio/api/appliance.py b/meraki/aio/api/appliance.py index 2c29b70c..09c98051 100644 --- a/meraki/aio/api/appliance.py +++ b/meraki/aio/api/appliance.py @@ -66,7 +66,7 @@ def createDeviceApplianceInterfacesPortsUpdate(self, serial: str, **kwargs): def getDeviceAppliancePerformance(self, serial: str, **kwargs): """ - **Return the performance score for a single MX** + **Return the performance score for a single Secure Appliance or Secure Router** https://developer.cisco.com/meraki/api-v1/#!get-device-appliance-performance - serial (string): Serial @@ -2673,7 +2673,7 @@ def getNetworkApplianceUplinksUsageHistory(self, networkId: str, **kwargs): https://developer.cisco.com/meraki/api-v1/#!get-network-appliance-uplinks-usage-history - networkId (string): Network ID - - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 365 days from today. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 30 days from today. - t1 (string): The end of the timespan for the data. t1 can be a maximum of 31 days after t0. - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be less than or equal to 31 days. The default is 10 minutes. - resolution (integer): The time resolution in seconds for returned data. The valid resolutions are: 60, 300, 600, 1800, 3600, 86400. The default is 60. @@ -2747,7 +2747,7 @@ def createNetworkApplianceVlan(self, networkId: str, id: str, name: str, **kwarg - dhcpBootFilename (string): DHCP boot option for boot filename - dhcpOptions (array): The list of DHCP options that will be included in DHCP responses. Each object in the list should have "code", "type", and "value" properties. - sgt (object): Security Group Tag settings for the VLAN. - - vrf (object): VRF configuration on the VLAN + - vrf (object): VRF configuration on the VLAN. - uplinks (array): Per-uplink NAT exception override configuration on the VLAN. Applicable only for networks that support NAT exceptions. """ @@ -2903,7 +2903,7 @@ def updateNetworkApplianceVlan(self, networkId: str, vlanId: str, **kwargs): - ipv6 (object): IPv6 configuration on the VLAN - mandatoryDhcp (object): Mandatory DHCP will enforce that clients connecting to this VLAN must use the IP address assigned by the DHCP server. Clients who use a static IP address won't be able to associate. Only available on firmware versions 17.0 and above - sgt (object): Security Group Tag settings for the VLAN. - - vrf (object): VRF configuration on the VLAN + - vrf (object): VRF configuration on the VLAN. - uplinks (array): Per-uplink NAT exception override configuration on the VLAN. Applicable only for networks that support NAT exceptions. """ @@ -3011,7 +3011,7 @@ def updateNetworkApplianceVpnBgp(self, networkId: str, enabled: bool, **kwargs): - networkId (string): Network ID - enabled (boolean): Boolean value to enable or disable the BGP configuration. When BGP is enabled, the asNumber (ASN) will be autopopulated with the preconfigured ASN at other Hubs or a default value if there is no ASN configured. - - asNumber (integer): An Autonomous System Number (ASN) is required if you are to run BGP and peer with another BGP Speaker outside of the Auto VPN domain. This ASN will be applied to the entire Auto VPN domain. The entire 4-byte ASN range is supported. So, the ASN must be an integer between 1 and 4294967295. When absent, this field is not updated. If no value exists then it defaults to 64512. + - asNumber (integer): An Autonomous System Number (ASN) is required if you are to run BGP and peer with another BGP Speaker outside of the Auto VPN domain. This ASN will be applied to the entire Auto VPN domain and is only configurable for Auto VPN BGP networks. The entire 4-byte ASN range is supported. So, the ASN must be an integer between 1 and 4294967295. When absent, this field is not updated. If no value exists then it defaults to 64512. - ibgpHoldTimer (integer): The iBGP holdtimer in seconds. The iBGP holdtimer must be an integer between 12 and 240. When absent, this field is not updated. If no value exists then it defaults to 240. - routerId (string): The router ID of the appliance - neighbors (array): List of BGP neighbors. This list replaces the existing set of neighbors. When absent, this field is not updated. diff --git a/meraki/aio/api/devices.py b/meraki/aio/api/devices.py index 4925826a..f07f3436 100644 --- a/meraki/aio/api/devices.py +++ b/meraki/aio/api/devices.py @@ -160,7 +160,7 @@ def updateDeviceCellularSims(self, serial: str, **kwargs): - serial (string): Serial - sims (array): List of SIMs. If a SIM was previously configured and not specified in this request, it will remain unchanged. - - simOrdering (array): Specifies the ordering of all SIMs for an MG: primary, secondary, and not-in-use (when applicable). It's required for devices with 3 or more SIMs and can be used in place of 'isPrimary' for dual-SIM devices. To indicate eSIM, use 'sim3'. Sim failover will occur only between primary and secondary sim slots. + - simOrdering (array): Specifies the ordering of all SIMs for an MG: primary, secondary, and not-in-use (when applicable). It's required for devices with 3 or more SIMs and can be used in place of 'isPrimary' for dual-SIM devices. Use the raw eSIM slot value for the device, such as 'sim2' or 'sim3'. Sim failover will occur only between primary and secondary sim slots. - simFailover (object): SIM Failover settings. """ @@ -196,7 +196,7 @@ def createDeviceCellularUplinksBandsMasksUpdate(self, serial: str, slot: str, ty - serial (string): Serial - slot (string): Required parameter for the SIM slot to update the cellular band mask for - type (string): Required parameter for the signal type to update the cellular band mask for - - masked (array): Required parameter for the band identifiers to mask for the given SIM slot and signal type. For LTE use bands identifiers like '30' and for 5G use band identifiers like 'n30'. Maximum 256 bands. + - masked (array): Required parameter for the band identifiers to mask for the given SIM slot and signal type. For LTE use bands identifiers like '30', for 5G use band identifiers like 'n30', or use 'all' to mask all bands for that signal type. Maximum 256 bands. """ kwargs = locals() diff --git a/meraki/aio/api/networks.py b/meraki/aio/api/networks.py index 666028fa..83c085c0 100644 --- a/meraki/aio/api/networks.py +++ b/meraki/aio/api/networks.py @@ -886,6 +886,37 @@ def removeNetworkDevices(self, networkId: str, serial: str, **kwargs): return self._session.post(metadata, resource, payload) + def updateNetworkDevicesSyslogServers(self, networkId: str, servers: list, **kwargs): + """ + **Updates the syslog servers configuration for a network.** + https://developer.cisco.com/meraki/api-v1/#!update-network-devices-syslog-servers + + - networkId (string): Network ID + - servers (array): A list of the syslog servers for this network; suggested maximum array size is 10 + """ + + kwargs = locals() + + metadata = { + "tags": ["networks", "configure", "devices", "syslog", "servers"], + "operation": "updateNetworkDevicesSyslogServers", + } + networkId = urllib.parse.quote(str(networkId), safe="") + resource = f"/networks/{networkId}/devices/syslog/servers" + + body_params = [ + "servers", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + + if self._session._validate_kwargs: + all_params = [] + body_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning(f"updateNetworkDevicesSyslogServers: ignoring unrecognized kwargs: {invalid}") + + return self._session.put(metadata, resource, payload) + def getNetworkEvents(self, networkId: str, total_pages=1, direction="prev", event_log_end_time=None, **kwargs): """ **List the events for the network** diff --git a/meraki/aio/api/organizations.py b/meraki/aio/api/organizations.py index 1ef07871..8b8edcc3 100644 --- a/meraki/aio/api/organizations.py +++ b/meraki/aio/api/organizations.py @@ -1469,7 +1469,7 @@ def dismissOrganizationAssuranceAlerts(self, organizationId: str, alertIds: list https://developer.cisco.com/meraki/api-v1/#!dismiss-organization-assurance-alerts - organizationId (string): Organization ID - - alertIds (array): Array of alert IDs to dismiss + - alertIds (array): Array of alert IDs in this organization to dismiss. Missing or inaccessible alert IDs return 404. """ kwargs = locals() @@ -1830,7 +1830,7 @@ def restoreOrganizationAssuranceAlerts(self, organizationId: str, alertIds: list https://developer.cisco.com/meraki/api-v1/#!restore-organization-assurance-alerts - organizationId (string): Organization ID - - alertIds (array): Array of alert IDs to restore + - alertIds (array): Array of alert IDs in this organization to restore. Missing or inaccessible alert IDs return 404. """ kwargs = locals() @@ -4192,6 +4192,106 @@ def getOrganizationDevicesStatusesOverview(self, organizationId: str, **kwargs): return self._session.get(metadata, resource, params) + def getOrganizationDevicesSyslogServersByNetwork(self, organizationId: str, total_pages=1, direction="next", **kwargs): + """ + **Returns syslog servers configured for the networks within an organization.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-devices-syslog-servers-by-network + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 10. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - networkIds (array): IDs of the networks for which to fetch syslog servers; suggested maximum array size is 100 + """ + + kwargs.update(locals()) + + metadata = { + "tags": ["organizations", "configure", "devices", "syslog", "servers", "byNetwork"], + "operation": "getOrganizationDevicesSyslogServersByNetwork", + } + organizationId = urllib.parse.quote(str(organizationId), safe="") + resource = f"/organizations/{organizationId}/devices/syslog/servers/byNetwork" + + query_params = [ + "perPage", + "startingAfter", + "endingBefore", + "networkIds", + ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = [ + "networkIds", + ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f"{k.strip()}[]"] = kwargs[f"{k}"] + params.pop(k.strip()) + + if self._session._validate_kwargs: + all_params = query_params + array_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning( + f"getOrganizationDevicesSyslogServersByNetwork: ignoring unrecognized kwargs: {invalid}" + ) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + + def getOrganizationDevicesSyslogServersRolesByNetwork( + self, organizationId: str, total_pages=1, direction="next", **kwargs + ): + """ + **Returns roles that can be assigned to a syslog server for a given network.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-devices-syslog-servers-roles-by-network + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 10. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - networkIds (array): IDs of the networks for which to fetch valid syslog server roles; suggested maximum array size is 100 + """ + + kwargs.update(locals()) + + metadata = { + "tags": ["organizations", "configure", "devices", "syslog", "servers", "roles", "byNetwork"], + "operation": "getOrganizationDevicesSyslogServersRolesByNetwork", + } + organizationId = urllib.parse.quote(str(organizationId), safe="") + resource = f"/organizations/{organizationId}/devices/syslog/servers/roles/byNetwork" + + query_params = [ + "perPage", + "startingAfter", + "endingBefore", + "networkIds", + ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = [ + "networkIds", + ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f"{k.strip()}[]"] = kwargs[f"{k}"] + params.pop(k.strip()) + + if self._session._validate_kwargs: + all_params = query_params + array_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning( + f"getOrganizationDevicesSyslogServersRolesByNetwork: ignoring unrecognized kwargs: {invalid}" + ) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + def getOrganizationDevicesSystemMemoryUsageHistoryByInterval( self, organizationId: str, total_pages=1, direction="next", **kwargs ): @@ -6641,22 +6741,26 @@ def getOrganizationPolicyObjects(self, organizationId: str, total_pages=1, direc def createOrganizationPolicyObject(self, organizationId: str, name: str, category: str, type: str, **kwargs): """ - **Creates a new Policy Object.** + **Creates a new Policy Object** https://developer.cisco.com/meraki/api-v1/#!create-organization-policy-object - organizationId (string): Organization ID - name (string): Name of a policy object, unique within the organization (alphanumeric, space, dash, or underscore characters only) - category (string): Category of a policy object (one of: adaptivePolicy, network) - - type (string): Type of a policy object (one of: adaptivePolicyIpv4Cidr, cidr, fqdn, ipAndMask) + - type (string): Type of a policy object (one of: adaptivePolicyIpv4Cidr, cidr, fqdn). DEPRECATED: `ipAndMask` is deprecated and will be removed in a future release. Use `cidr` instead. - cidr (string): CIDR Value of a policy object (e.g. 10.11.12.1/24") - fqdn (string): Fully qualified domain name of policy object (e.g. "example.com") - - mask (string): Mask of a policy object (e.g. "255.255.0.0") - - ip (string): IP Address of a policy object (e.g. "1.2.3.4") + - mask (string): Mask of a policy object (e.g. "255.255.0.0"). Used only with deprecated `type=ipAndMask`. + - ip (string): IP Address of a policy object (e.g. "1.2.3.4"). Used only with deprecated `type=ipAndMask`. - groupIds (array): The IDs of policy object groups the policy object belongs to """ kwargs.update(locals()) + if "type" in kwargs: + options = ["adaptivePolicyIpv4Cidr", "cidr", "fqdn"] + assert kwargs["type"] in options, f'''"type" cannot be "{kwargs["type"]}", & must be set to one of: {options}''' + metadata = { "tags": ["organizations", "configure", "policyObjects"], "operation": "createOrganizationPolicyObject", @@ -6850,7 +6954,7 @@ def getOrganizationPolicyObject(self, organizationId: str, policyObjectId: str): def updateOrganizationPolicyObject(self, organizationId: str, policyObjectId: str, **kwargs): """ - **Updates a Policy Object.** + **Updates a Policy Object** https://developer.cisco.com/meraki/api-v1/#!update-organization-policy-object - organizationId (string): Organization ID @@ -6858,8 +6962,8 @@ def updateOrganizationPolicyObject(self, organizationId: str, policyObjectId: st - name (string): Name of a policy object, unique within the organization (alphanumeric, space, dash, or underscore characters only) - cidr (string): CIDR Value of a policy object (e.g. 10.11.12.1/24") - fqdn (string): Fully qualified domain name of policy object (e.g. "example.com") - - mask (string): Mask of a policy object (e.g. "255.255.0.0") - - ip (string): IP Address of a policy object (e.g. "1.2.3.4") + - mask (string): Mask of a policy object (e.g. "255.255.0.0"). Used only with deprecated `type=ipAndMask`. + - ip (string): IP Address of a policy object (e.g. "1.2.3.4"). Used only with deprecated `type=ipAndMask`. - groupIds (array): The IDs of policy object groups the policy object belongs to """ @@ -7449,7 +7553,6 @@ def attachOrganizationSaseSites(self, organizationId: str, **kwargs): - organizationId (string): Organization ID - items (array): List of Meraki SD-WAN sites with the associated regions to be attached. - - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -7463,7 +7566,6 @@ def attachOrganizationSaseSites(self, organizationId: str, **kwargs): body_params = [ "items", - "callback", ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} @@ -7542,7 +7644,6 @@ def detachOrganizationSaseSites(self, organizationId: str, **kwargs): - organizationId (string): Organization ID - items (array): List of Secure Access sites to be detached. - - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) diff --git a/meraki/aio/api/switch.py b/meraki/aio/api/switch.py index b164485f..21dcc763 100644 --- a/meraki/aio/api/switch.py +++ b/meraki/aio/api/switch.py @@ -2225,6 +2225,41 @@ def createNetworkSwitchStack(self, networkId: str, name: str, serials: list, **k return self._session.post(metadata, resource, payload) + def updateNetworkSwitchStack(self, networkId: str, switchStackId: str, **kwargs): + """ + **Update a switch stack** + https://developer.cisco.com/meraki/api-v1/#!update-network-switch-stack + + - networkId (string): Network ID + - switchStackId (string): Switch stack ID + - name (string): The name of the switch stack + - members (array): The complete list of switches that should be in the stack. Minimum 2 and maximum 8 members. Omitting this field leaves stack membership unchanged. + """ + + kwargs.update(locals()) + + metadata = { + "tags": ["switch", "configure", "stacks"], + "operation": "updateNetworkSwitchStack", + } + networkId = urllib.parse.quote(str(networkId), safe="") + switchStackId = urllib.parse.quote(str(switchStackId), safe="") + resource = f"/networks/{networkId}/switch/stacks/{switchStackId}" + + body_params = [ + "name", + "members", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + + if self._session._validate_kwargs: + all_params = [] + body_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning(f"updateNetworkSwitchStack: ignoring unrecognized kwargs: {invalid}") + + return self._session.put(metadata, resource, payload) + def getNetworkSwitchStack(self, networkId: str, switchStackId: str): """ **Show a switch stack** diff --git a/meraki/aio/api/wireless.py b/meraki/aio/api/wireless.py index 8216c1b2..b9ece30d 100644 --- a/meraki/aio/api/wireless.py +++ b/meraki/aio/api/wireless.py @@ -2312,6 +2312,7 @@ def updateNetworkWirelessSsid(self, networkId: str, number: str, **kwargs): - dnsRewrite (object): DNS servers rewrite settings - speedBurst (object): The SpeedBurst setting for this SSID' - namedVlans (object): Named VLAN settings. + - security (object): Security settings for the SSID - localAuthFallback (object): The current configuration for Local Authentication Fallback. Enables the Access Point (AP) to store client authentication data for a specified duration that can be adjusted as needed. - radiusAccountingStartDelay (integer): The delay (in seconds) before sending the first RADIUS accounting start message. Must be between 0 and 60 seconds. """ @@ -2463,6 +2464,7 @@ def updateNetworkWirelessSsid(self, networkId: str, number: str, **kwargs): "dnsRewrite", "speedBurst", "namedVlans", + "security", "localAuthFallback", "radiusAccountingStartDelay", ] diff --git a/meraki/aio/rest_session.py b/meraki/aio/rest_session.py index 9eb35572..aba94507 100644 --- a/meraki/aio/rest_session.py +++ b/meraki/aio/rest_session.py @@ -230,8 +230,17 @@ async def _request(self, metadata, method, url, **kwargs): await asyncio.sleep(wait) # 5XX errors elif status >= 500: + request_id = response.headers.get("X-Request-Id") or "none" if self._logger: - self._logger.warning(f"{tag}, {operation} > {abs_url} - {status} {reason}, retrying in 1 second") + self._logger.warning( + f"{tag}, {operation} > {abs_url} - {status} {reason} " + f"(X-Request-Id: {request_id}), retrying in 1 second" + ) + if _attempt == retries - 1: + self._logger.error( + f"{tag}, {operation} > {abs_url} - {status} {reason} failed after retries. " + f"Provide this X-Request-Id to Meraki for log lookup: {request_id}" + ) await asyncio.sleep(1) # 4XX errors else: diff --git a/meraki/api/appliance.py b/meraki/api/appliance.py index 14957aa0..82023191 100644 --- a/meraki/api/appliance.py +++ b/meraki/api/appliance.py @@ -66,7 +66,7 @@ def createDeviceApplianceInterfacesPortsUpdate(self, serial: str, **kwargs): def getDeviceAppliancePerformance(self, serial: str, **kwargs): """ - **Return the performance score for a single MX** + **Return the performance score for a single Secure Appliance or Secure Router** https://developer.cisco.com/meraki/api-v1/#!get-device-appliance-performance - serial (string): Serial @@ -2673,7 +2673,7 @@ def getNetworkApplianceUplinksUsageHistory(self, networkId: str, **kwargs): https://developer.cisco.com/meraki/api-v1/#!get-network-appliance-uplinks-usage-history - networkId (string): Network ID - - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 365 days from today. + - t0 (string): The beginning of the timespan for the data. The maximum lookback period is 30 days from today. - t1 (string): The end of the timespan for the data. t1 can be a maximum of 31 days after t0. - timespan (number): The timespan for which the information will be fetched. If specifying timespan, do not specify parameters t0 and t1. The value must be in seconds and be less than or equal to 31 days. The default is 10 minutes. - resolution (integer): The time resolution in seconds for returned data. The valid resolutions are: 60, 300, 600, 1800, 3600, 86400. The default is 60. @@ -2747,7 +2747,7 @@ def createNetworkApplianceVlan(self, networkId: str, id: str, name: str, **kwarg - dhcpBootFilename (string): DHCP boot option for boot filename - dhcpOptions (array): The list of DHCP options that will be included in DHCP responses. Each object in the list should have "code", "type", and "value" properties. - sgt (object): Security Group Tag settings for the VLAN. - - vrf (object): VRF configuration on the VLAN + - vrf (object): VRF configuration on the VLAN. - uplinks (array): Per-uplink NAT exception override configuration on the VLAN. Applicable only for networks that support NAT exceptions. """ @@ -2903,7 +2903,7 @@ def updateNetworkApplianceVlan(self, networkId: str, vlanId: str, **kwargs): - ipv6 (object): IPv6 configuration on the VLAN - mandatoryDhcp (object): Mandatory DHCP will enforce that clients connecting to this VLAN must use the IP address assigned by the DHCP server. Clients who use a static IP address won't be able to associate. Only available on firmware versions 17.0 and above - sgt (object): Security Group Tag settings for the VLAN. - - vrf (object): VRF configuration on the VLAN + - vrf (object): VRF configuration on the VLAN. - uplinks (array): Per-uplink NAT exception override configuration on the VLAN. Applicable only for networks that support NAT exceptions. """ @@ -3011,7 +3011,7 @@ def updateNetworkApplianceVpnBgp(self, networkId: str, enabled: bool, **kwargs): - networkId (string): Network ID - enabled (boolean): Boolean value to enable or disable the BGP configuration. When BGP is enabled, the asNumber (ASN) will be autopopulated with the preconfigured ASN at other Hubs or a default value if there is no ASN configured. - - asNumber (integer): An Autonomous System Number (ASN) is required if you are to run BGP and peer with another BGP Speaker outside of the Auto VPN domain. This ASN will be applied to the entire Auto VPN domain. The entire 4-byte ASN range is supported. So, the ASN must be an integer between 1 and 4294967295. When absent, this field is not updated. If no value exists then it defaults to 64512. + - asNumber (integer): An Autonomous System Number (ASN) is required if you are to run BGP and peer with another BGP Speaker outside of the Auto VPN domain. This ASN will be applied to the entire Auto VPN domain and is only configurable for Auto VPN BGP networks. The entire 4-byte ASN range is supported. So, the ASN must be an integer between 1 and 4294967295. When absent, this field is not updated. If no value exists then it defaults to 64512. - ibgpHoldTimer (integer): The iBGP holdtimer in seconds. The iBGP holdtimer must be an integer between 12 and 240. When absent, this field is not updated. If no value exists then it defaults to 240. - routerId (string): The router ID of the appliance - neighbors (array): List of BGP neighbors. This list replaces the existing set of neighbors. When absent, this field is not updated. diff --git a/meraki/api/batch/appliance.py b/meraki/api/batch/appliance.py index 93f68389..9202a10e 100644 --- a/meraki/api/batch/appliance.py +++ b/meraki/api/batch/appliance.py @@ -1064,7 +1064,7 @@ def createNetworkApplianceVlan(self, networkId: str, id: str, name: str, **kwarg - dhcpBootFilename (string): DHCP boot option for boot filename - dhcpOptions (array): The list of DHCP options that will be included in DHCP responses. Each object in the list should have "code", "type", and "value" properties. - sgt (object): Security Group Tag settings for the VLAN. - - vrf (object): VRF configuration on the VLAN + - vrf (object): VRF configuration on the VLAN. - uplinks (array): Per-uplink NAT exception override configuration on the VLAN. Applicable only for networks that support NAT exceptions. """ @@ -1172,7 +1172,7 @@ def updateNetworkApplianceVlan(self, networkId: str, vlanId: str, **kwargs): - ipv6 (object): IPv6 configuration on the VLAN - mandatoryDhcp (object): Mandatory DHCP will enforce that clients connecting to this VLAN must use the IP address assigned by the DHCP server. Clients who use a static IP address won't be able to associate. Only available on firmware versions 17.0 and above - sgt (object): Security Group Tag settings for the VLAN. - - vrf (object): VRF configuration on the VLAN + - vrf (object): VRF configuration on the VLAN. - uplinks (array): Per-uplink NAT exception override configuration on the VLAN. Applicable only for networks that support NAT exceptions. """ @@ -1257,7 +1257,7 @@ def updateNetworkApplianceVpnBgp(self, networkId: str, enabled: bool, **kwargs): - networkId (string): Network ID - enabled (boolean): Boolean value to enable or disable the BGP configuration. When BGP is enabled, the asNumber (ASN) will be autopopulated with the preconfigured ASN at other Hubs or a default value if there is no ASN configured. - - asNumber (integer): An Autonomous System Number (ASN) is required if you are to run BGP and peer with another BGP Speaker outside of the Auto VPN domain. This ASN will be applied to the entire Auto VPN domain. The entire 4-byte ASN range is supported. So, the ASN must be an integer between 1 and 4294967295. When absent, this field is not updated. If no value exists then it defaults to 64512. + - asNumber (integer): An Autonomous System Number (ASN) is required if you are to run BGP and peer with another BGP Speaker outside of the Auto VPN domain. This ASN will be applied to the entire Auto VPN domain and is only configurable for Auto VPN BGP networks. The entire 4-byte ASN range is supported. So, the ASN must be an integer between 1 and 4294967295. When absent, this field is not updated. If no value exists then it defaults to 64512. - ibgpHoldTimer (integer): The iBGP holdtimer in seconds. The iBGP holdtimer must be an integer between 12 and 240. When absent, this field is not updated. If no value exists then it defaults to 240. - routerId (string): The router ID of the appliance - neighbors (array): List of BGP neighbors. This list replaces the existing set of neighbors. When absent, this field is not updated. diff --git a/meraki/api/batch/devices.py b/meraki/api/batch/devices.py index 8df0ff15..c970b84e 100644 --- a/meraki/api/batch/devices.py +++ b/meraki/api/batch/devices.py @@ -79,7 +79,7 @@ def createDeviceCellularUplinksBandsMasksUpdate(self, serial: str, slot: str, ty - serial (string): Serial - slot (string): Required parameter for the SIM slot to update the cellular band mask for - type (string): Required parameter for the signal type to update the cellular band mask for - - masked (array): Required parameter for the band identifiers to mask for the given SIM slot and signal type. For LTE use bands identifiers like '30' and for 5G use band identifiers like 'n30'. Maximum 256 bands. + - masked (array): Required parameter for the band identifiers to mask for the given SIM slot and signal type. For LTE use bands identifiers like '30', for 5G use band identifiers like 'n30', or use 'all' to mask all bands for that signal type. Maximum 256 bands. """ kwargs = locals() diff --git a/meraki/api/batch/networks.py b/meraki/api/batch/networks.py index 5fd9dc9f..58672c98 100644 --- a/meraki/api/batch/networks.py +++ b/meraki/api/batch/networks.py @@ -203,6 +203,31 @@ def removeNetworkDevices(self, networkId: str, serial: str, **kwargs): } return action + def updateNetworkDevicesSyslogServers(self, networkId: str, servers: list, **kwargs): + """ + **Updates the syslog servers configuration for a network.** + https://developer.cisco.com/meraki/api-v1/#!update-network-devices-syslog-servers + + - networkId (string): Network ID + - servers (array): A list of the syslog servers for this network; suggested maximum array size is 10 + """ + + kwargs = locals() + + networkId = urllib.parse.quote(networkId, safe="") + resource = f"/networks/{networkId}/devices/syslog/servers" + + body_params = [ + "servers", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + action = { + "resource": resource, + "operation": "servers", + "body": payload, + } + return action + def updateNetworkFirmwareUpgrades(self, networkId: str, **kwargs): """ **Update firmware upgrade information for a network** diff --git a/meraki/api/batch/organizations.py b/meraki/api/batch/organizations.py index 127df4d9..172a8b98 100644 --- a/meraki/api/batch/organizations.py +++ b/meraki/api/batch/organizations.py @@ -5,6 +5,37 @@ class ActionBatchOrganizations(object): def __init__(self): super(ActionBatchOrganizations, self).__init__() + def updateOrganization(self, organizationId: str, **kwargs): + """ + **Update an organization** + https://developer.cisco.com/meraki/api-v1/#!update-organization + + - organizationId (string): Organization ID + - name (string): The name of the organization + - management (object): Information about the organization's management system + - api (object): API-specific settings + - privacy (object): Privacy-related settings for the organization. + """ + + kwargs.update(locals()) + + organizationId = urllib.parse.quote(organizationId, safe="") + resource = f"/organizations/{organizationId}" + + body_params = [ + "name", + "management", + "api", + "privacy", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + action = { + "resource": resource, + "operation": "update", + "body": payload, + } + return action + def createOrganizationAdaptivePolicyAcl(self, organizationId: str, name: str, rules: list, ipVersion: str, **kwargs): """ **Creates new adaptive policy ACL** @@ -1717,22 +1748,26 @@ def deleteOrganizationPoliciesGlobalGroupPolicy(self, organizationId: str, polic def createOrganizationPolicyObject(self, organizationId: str, name: str, category: str, type: str, **kwargs): """ - **Creates a new Policy Object.** + **Creates a new Policy Object. Note: type `ipAndMask` is deprecated; use `cidr`.** https://developer.cisco.com/meraki/api-v1/#!create-organization-policy-object - organizationId (string): Organization ID - name (string): Name of a policy object, unique within the organization (alphanumeric, space, dash, or underscore characters only) - category (string): Category of a policy object (one of: adaptivePolicy, network) - - type (string): Type of a policy object (one of: adaptivePolicyIpv4Cidr, cidr, fqdn, ipAndMask) + - type (string): Type of a policy object (one of: adaptivePolicyIpv4Cidr, cidr, fqdn). DEPRECATED: `ipAndMask` is deprecated and will be removed in a future release. Use `cidr` instead. - cidr (string): CIDR Value of a policy object (e.g. 10.11.12.1/24") - fqdn (string): Fully qualified domain name of policy object (e.g. "example.com") - - mask (string): Mask of a policy object (e.g. "255.255.0.0") - - ip (string): IP Address of a policy object (e.g. "1.2.3.4") + - mask (string): Mask of a policy object (e.g. "255.255.0.0"). Used only with deprecated `type=ipAndMask`. + - ip (string): IP Address of a policy object (e.g. "1.2.3.4"). Used only with deprecated `type=ipAndMask`. - groupIds (array): The IDs of policy object groups the policy object belongs to """ kwargs.update(locals()) + if "type" in kwargs: + options = ["adaptivePolicyIpv4Cidr", "cidr", "fqdn"] + assert kwargs["type"] in options, f'''"type" cannot be "{kwargs["type"]}", & must be set to one of: {options}''' + organizationId = urllib.parse.quote(organizationId, safe="") resource = f"/organizations/{organizationId}/policyObjects" @@ -1833,7 +1868,7 @@ def deleteOrganizationPolicyObjectsGroup(self, organizationId: str, policyObject def updateOrganizationPolicyObject(self, organizationId: str, policyObjectId: str, **kwargs): """ - **Updates a Policy Object.** + **Updates a Policy Object. Note: type `ipAndMask` is deprecated; use `cidr`.** https://developer.cisco.com/meraki/api-v1/#!update-organization-policy-object - organizationId (string): Organization ID @@ -1841,8 +1876,8 @@ def updateOrganizationPolicyObject(self, organizationId: str, policyObjectId: st - name (string): Name of a policy object, unique within the organization (alphanumeric, space, dash, or underscore characters only) - cidr (string): CIDR Value of a policy object (e.g. 10.11.12.1/24") - fqdn (string): Fully qualified domain name of policy object (e.g. "example.com") - - mask (string): Mask of a policy object (e.g. "255.255.0.0") - - ip (string): IP Address of a policy object (e.g. "1.2.3.4") + - mask (string): Mask of a policy object (e.g. "255.255.0.0"). Used only with deprecated `type=ipAndMask`. + - ip (string): IP Address of a policy object (e.g. "1.2.3.4"). Used only with deprecated `type=ipAndMask`. - groupIds (array): The IDs of policy object groups the policy object belongs to """ @@ -2042,7 +2077,6 @@ def attachOrganizationSaseSites(self, organizationId: str, **kwargs): - organizationId (string): Organization ID - items (array): List of Meraki SD-WAN sites with the associated regions to be attached. - - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -2052,7 +2086,6 @@ def attachOrganizationSaseSites(self, organizationId: str, **kwargs): body_params = [ "items", - "callback", ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} action = { @@ -2069,7 +2102,6 @@ def detachOrganizationSaseSites(self, organizationId: str, **kwargs): - organizationId (string): Organization ID - items (array): List of Secure Access sites to be detached. - - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -2111,6 +2143,54 @@ def updateOrganizationSaseSite(self, organizationId: str, siteId: str, **kwargs) } return action + def updateOrganizationSnmp(self, organizationId: str, **kwargs): + """ + **Update the SNMP settings for an organization** + https://developer.cisco.com/meraki/api-v1/#!update-organization-snmp + + - organizationId (string): Organization ID + - v2cEnabled (boolean): Boolean indicating whether SNMP version 2c is enabled for the organization. + - v3Enabled (boolean): Boolean indicating whether SNMP version 3 is enabled for the organization. + - v3AuthMode (string): The SNMP version 3 authentication mode. Can be either 'MD5' or 'SHA'. + - v3AuthPass (string): The SNMP version 3 authentication password. Must be at least 8 characters if specified. + - v3PrivMode (string): The SNMP version 3 privacy mode. Can be either 'DES' or 'AES128'. + - v3PrivPass (string): The SNMP version 3 privacy password. Must be at least 8 characters if specified. + - peerIps (array): The list of IPv4 addresses that are allowed to access the SNMP server. + """ + + kwargs.update(locals()) + + if "v3AuthMode" in kwargs: + options = ["MD5", "SHA"] + assert kwargs["v3AuthMode"] in options, ( + f'''"v3AuthMode" cannot be "{kwargs["v3AuthMode"]}", & must be set to one of: {options}''' + ) + if "v3PrivMode" in kwargs: + options = ["AES128", "DES"] + assert kwargs["v3PrivMode"] in options, ( + f'''"v3PrivMode" cannot be "{kwargs["v3PrivMode"]}", & must be set to one of: {options}''' + ) + + organizationId = urllib.parse.quote(organizationId, safe="") + resource = f"/organizations/{organizationId}/snmp" + + body_params = [ + "v2cEnabled", + "v3Enabled", + "v3AuthMode", + "v3AuthPass", + "v3PrivMode", + "v3PrivPass", + "peerIps", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + action = { + "resource": resource, + "operation": "update", + "body": payload, + } + return action + def deleteOrganizationSplashAsset(self, organizationId: str, id: str): """ **Delete a Splash Theme Asset** diff --git a/meraki/api/batch/switch.py b/meraki/api/batch/switch.py index c469c7ba..092b9a51 100644 --- a/meraki/api/batch/switch.py +++ b/meraki/api/batch/switch.py @@ -1082,7 +1082,7 @@ def updateNetworkSwitchRoutingMulticast(self, networkId: str, **kwargs): payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} action = { "resource": resource, - "operation": "update", + "operation": "ms/multicast/actions/update", "body": payload, } return action @@ -1245,6 +1245,35 @@ def updateNetworkSwitchSettings(self, networkId: str, **kwargs): } return action + def updateNetworkSwitchStack(self, networkId: str, switchStackId: str, **kwargs): + """ + **Update a switch stack. At least one of 'name' or 'members' must be provided. If 'members' is provided, it replaces the entire stack membership.** + https://developer.cisco.com/meraki/api-v1/#!update-network-switch-stack + + - networkId (string): Network ID + - switchStackId (string): Switch stack ID + - name (string): The name of the switch stack + - members (array): The complete list of switches that should be in the stack. Minimum 2 and maximum 8 members. Omitting this field leaves stack membership unchanged. + """ + + kwargs.update(locals()) + + networkId = urllib.parse.quote(networkId, safe="") + switchStackId = urllib.parse.quote(switchStackId, safe="") + resource = f"/networks/{networkId}/switch/stacks/{switchStackId}" + + body_params = [ + "name", + "members", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + action = { + "resource": resource, + "operation": "stacks/actions/update", + "body": payload, + } + return action + def createNetworkSwitchStackRoutingInterface(self, networkId: str, switchStackId: str, name: str, **kwargs): """ **Create a layer 3 interface for a switch stack** diff --git a/meraki/api/batch/wireless.py b/meraki/api/batch/wireless.py index 72ff3912..21684c44 100644 --- a/meraki/api/batch/wireless.py +++ b/meraki/api/batch/wireless.py @@ -763,6 +763,7 @@ def updateNetworkWirelessSsid(self, networkId: str, number: str, **kwargs): - dnsRewrite (object): DNS servers rewrite settings - speedBurst (object): The SpeedBurst setting for this SSID' - namedVlans (object): Named VLAN settings. + - security (object): Security settings for the SSID - localAuthFallback (object): The current configuration for Local Authentication Fallback. Enables the Access Point (AP) to store client authentication data for a specified duration that can be adjusted as needed. - radiusAccountingStartDelay (integer): The delay (in seconds) before sending the first RADIUS accounting start message. Must be between 0 and 60 seconds. """ @@ -910,6 +911,7 @@ def updateNetworkWirelessSsid(self, networkId: str, number: str, **kwargs): "dnsRewrite", "speedBurst", "namedVlans", + "security", "localAuthFallback", "radiusAccountingStartDelay", ] diff --git a/meraki/api/devices.py b/meraki/api/devices.py index 79865603..4babf70f 100644 --- a/meraki/api/devices.py +++ b/meraki/api/devices.py @@ -160,7 +160,7 @@ def updateDeviceCellularSims(self, serial: str, **kwargs): - serial (string): Serial - sims (array): List of SIMs. If a SIM was previously configured and not specified in this request, it will remain unchanged. - - simOrdering (array): Specifies the ordering of all SIMs for an MG: primary, secondary, and not-in-use (when applicable). It's required for devices with 3 or more SIMs and can be used in place of 'isPrimary' for dual-SIM devices. To indicate eSIM, use 'sim3'. Sim failover will occur only between primary and secondary sim slots. + - simOrdering (array): Specifies the ordering of all SIMs for an MG: primary, secondary, and not-in-use (when applicable). It's required for devices with 3 or more SIMs and can be used in place of 'isPrimary' for dual-SIM devices. Use the raw eSIM slot value for the device, such as 'sim2' or 'sim3'. Sim failover will occur only between primary and secondary sim slots. - simFailover (object): SIM Failover settings. """ @@ -196,7 +196,7 @@ def createDeviceCellularUplinksBandsMasksUpdate(self, serial: str, slot: str, ty - serial (string): Serial - slot (string): Required parameter for the SIM slot to update the cellular band mask for - type (string): Required parameter for the signal type to update the cellular band mask for - - masked (array): Required parameter for the band identifiers to mask for the given SIM slot and signal type. For LTE use bands identifiers like '30' and for 5G use band identifiers like 'n30'. Maximum 256 bands. + - masked (array): Required parameter for the band identifiers to mask for the given SIM slot and signal type. For LTE use bands identifiers like '30', for 5G use band identifiers like 'n30', or use 'all' to mask all bands for that signal type. Maximum 256 bands. """ kwargs = locals() diff --git a/meraki/api/networks.py b/meraki/api/networks.py index 9acaa98a..c1858a71 100644 --- a/meraki/api/networks.py +++ b/meraki/api/networks.py @@ -886,6 +886,37 @@ def removeNetworkDevices(self, networkId: str, serial: str, **kwargs): return self._session.post(metadata, resource, payload) + def updateNetworkDevicesSyslogServers(self, networkId: str, servers: list, **kwargs): + """ + **Updates the syslog servers configuration for a network.** + https://developer.cisco.com/meraki/api-v1/#!update-network-devices-syslog-servers + + - networkId (string): Network ID + - servers (array): A list of the syslog servers for this network; suggested maximum array size is 10 + """ + + kwargs = locals() + + metadata = { + "tags": ["networks", "configure", "devices", "syslog", "servers"], + "operation": "updateNetworkDevicesSyslogServers", + } + networkId = urllib.parse.quote(str(networkId), safe="") + resource = f"/networks/{networkId}/devices/syslog/servers" + + body_params = [ + "servers", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + + if self._session._validate_kwargs: + all_params = [] + body_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning(f"updateNetworkDevicesSyslogServers: ignoring unrecognized kwargs: {invalid}") + + return self._session.put(metadata, resource, payload) + def getNetworkEvents(self, networkId: str, total_pages=1, direction="prev", event_log_end_time=None, **kwargs): """ **List the events for the network** diff --git a/meraki/api/organizations.py b/meraki/api/organizations.py index 713c6f39..4331a1ce 100644 --- a/meraki/api/organizations.py +++ b/meraki/api/organizations.py @@ -1469,7 +1469,7 @@ def dismissOrganizationAssuranceAlerts(self, organizationId: str, alertIds: list https://developer.cisco.com/meraki/api-v1/#!dismiss-organization-assurance-alerts - organizationId (string): Organization ID - - alertIds (array): Array of alert IDs to dismiss + - alertIds (array): Array of alert IDs in this organization to dismiss. Missing or inaccessible alert IDs return 404. """ kwargs = locals() @@ -1830,7 +1830,7 @@ def restoreOrganizationAssuranceAlerts(self, organizationId: str, alertIds: list https://developer.cisco.com/meraki/api-v1/#!restore-organization-assurance-alerts - organizationId (string): Organization ID - - alertIds (array): Array of alert IDs to restore + - alertIds (array): Array of alert IDs in this organization to restore. Missing or inaccessible alert IDs return 404. """ kwargs = locals() @@ -4192,6 +4192,106 @@ def getOrganizationDevicesStatusesOverview(self, organizationId: str, **kwargs): return self._session.get(metadata, resource, params) + def getOrganizationDevicesSyslogServersByNetwork(self, organizationId: str, total_pages=1, direction="next", **kwargs): + """ + **Returns syslog servers configured for the networks within an organization.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-devices-syslog-servers-by-network + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 10. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - networkIds (array): IDs of the networks for which to fetch syslog servers; suggested maximum array size is 100 + """ + + kwargs.update(locals()) + + metadata = { + "tags": ["organizations", "configure", "devices", "syslog", "servers", "byNetwork"], + "operation": "getOrganizationDevicesSyslogServersByNetwork", + } + organizationId = urllib.parse.quote(str(organizationId), safe="") + resource = f"/organizations/{organizationId}/devices/syslog/servers/byNetwork" + + query_params = [ + "perPage", + "startingAfter", + "endingBefore", + "networkIds", + ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = [ + "networkIds", + ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f"{k.strip()}[]"] = kwargs[f"{k}"] + params.pop(k.strip()) + + if self._session._validate_kwargs: + all_params = query_params + array_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning( + f"getOrganizationDevicesSyslogServersByNetwork: ignoring unrecognized kwargs: {invalid}" + ) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + + def getOrganizationDevicesSyslogServersRolesByNetwork( + self, organizationId: str, total_pages=1, direction="next", **kwargs + ): + """ + **Returns roles that can be assigned to a syslog server for a given network.** + https://developer.cisco.com/meraki/api-v1/#!get-organization-devices-syslog-servers-roles-by-network + + - organizationId (string): Organization ID + - total_pages (integer or string): use with perPage to get total results up to total_pages*perPage; -1 or "all" for all pages + - direction (string): direction to paginate, either "next" (default) or "prev" page + - perPage (integer): The number of entries per page returned. Acceptable range is 3 - 1000. Default is 10. + - startingAfter (string): A token used by the server to indicate the start of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - endingBefore (string): A token used by the server to indicate the end of the page. Often this is a timestamp or an ID but it is not limited to those. This parameter should not be defined by client applications. The link for the first, last, prev, or next page in the HTTP Link header should define it. + - networkIds (array): IDs of the networks for which to fetch valid syslog server roles; suggested maximum array size is 100 + """ + + kwargs.update(locals()) + + metadata = { + "tags": ["organizations", "configure", "devices", "syslog", "servers", "roles", "byNetwork"], + "operation": "getOrganizationDevicesSyslogServersRolesByNetwork", + } + organizationId = urllib.parse.quote(str(organizationId), safe="") + resource = f"/organizations/{organizationId}/devices/syslog/servers/roles/byNetwork" + + query_params = [ + "perPage", + "startingAfter", + "endingBefore", + "networkIds", + ] + params = {k.strip(): v for k, v in kwargs.items() if k.strip() in query_params} + + array_params = [ + "networkIds", + ] + for k, v in kwargs.items(): + if k.strip() in array_params: + params[f"{k.strip()}[]"] = kwargs[f"{k}"] + params.pop(k.strip()) + + if self._session._validate_kwargs: + all_params = query_params + array_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning( + f"getOrganizationDevicesSyslogServersRolesByNetwork: ignoring unrecognized kwargs: {invalid}" + ) + + return self._session.get_pages(metadata, resource, params, total_pages, direction) + def getOrganizationDevicesSystemMemoryUsageHistoryByInterval( self, organizationId: str, total_pages=1, direction="next", **kwargs ): @@ -6641,22 +6741,26 @@ def getOrganizationPolicyObjects(self, organizationId: str, total_pages=1, direc def createOrganizationPolicyObject(self, organizationId: str, name: str, category: str, type: str, **kwargs): """ - **Creates a new Policy Object.** + **Creates a new Policy Object** https://developer.cisco.com/meraki/api-v1/#!create-organization-policy-object - organizationId (string): Organization ID - name (string): Name of a policy object, unique within the organization (alphanumeric, space, dash, or underscore characters only) - category (string): Category of a policy object (one of: adaptivePolicy, network) - - type (string): Type of a policy object (one of: adaptivePolicyIpv4Cidr, cidr, fqdn, ipAndMask) + - type (string): Type of a policy object (one of: adaptivePolicyIpv4Cidr, cidr, fqdn). DEPRECATED: `ipAndMask` is deprecated and will be removed in a future release. Use `cidr` instead. - cidr (string): CIDR Value of a policy object (e.g. 10.11.12.1/24") - fqdn (string): Fully qualified domain name of policy object (e.g. "example.com") - - mask (string): Mask of a policy object (e.g. "255.255.0.0") - - ip (string): IP Address of a policy object (e.g. "1.2.3.4") + - mask (string): Mask of a policy object (e.g. "255.255.0.0"). Used only with deprecated `type=ipAndMask`. + - ip (string): IP Address of a policy object (e.g. "1.2.3.4"). Used only with deprecated `type=ipAndMask`. - groupIds (array): The IDs of policy object groups the policy object belongs to """ kwargs.update(locals()) + if "type" in kwargs: + options = ["adaptivePolicyIpv4Cidr", "cidr", "fqdn"] + assert kwargs["type"] in options, f'''"type" cannot be "{kwargs["type"]}", & must be set to one of: {options}''' + metadata = { "tags": ["organizations", "configure", "policyObjects"], "operation": "createOrganizationPolicyObject", @@ -6850,7 +6954,7 @@ def getOrganizationPolicyObject(self, organizationId: str, policyObjectId: str): def updateOrganizationPolicyObject(self, organizationId: str, policyObjectId: str, **kwargs): """ - **Updates a Policy Object.** + **Updates a Policy Object** https://developer.cisco.com/meraki/api-v1/#!update-organization-policy-object - organizationId (string): Organization ID @@ -6858,8 +6962,8 @@ def updateOrganizationPolicyObject(self, organizationId: str, policyObjectId: st - name (string): Name of a policy object, unique within the organization (alphanumeric, space, dash, or underscore characters only) - cidr (string): CIDR Value of a policy object (e.g. 10.11.12.1/24") - fqdn (string): Fully qualified domain name of policy object (e.g. "example.com") - - mask (string): Mask of a policy object (e.g. "255.255.0.0") - - ip (string): IP Address of a policy object (e.g. "1.2.3.4") + - mask (string): Mask of a policy object (e.g. "255.255.0.0"). Used only with deprecated `type=ipAndMask`. + - ip (string): IP Address of a policy object (e.g. "1.2.3.4"). Used only with deprecated `type=ipAndMask`. - groupIds (array): The IDs of policy object groups the policy object belongs to """ @@ -7449,7 +7553,6 @@ def attachOrganizationSaseSites(self, organizationId: str, **kwargs): - organizationId (string): Organization ID - items (array): List of Meraki SD-WAN sites with the associated regions to be attached. - - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) @@ -7463,7 +7566,6 @@ def attachOrganizationSaseSites(self, organizationId: str, **kwargs): body_params = [ "items", - "callback", ] payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} @@ -7542,7 +7644,6 @@ def detachOrganizationSaseSites(self, organizationId: str, **kwargs): - organizationId (string): Organization ID - items (array): List of Secure Access sites to be detached. - - callback (object): Details for the callback. Please include either an httpServerId OR url and sharedSecret """ kwargs.update(locals()) diff --git a/meraki/api/switch.py b/meraki/api/switch.py index d5cbf2dd..86ac6cc3 100644 --- a/meraki/api/switch.py +++ b/meraki/api/switch.py @@ -2225,6 +2225,41 @@ def createNetworkSwitchStack(self, networkId: str, name: str, serials: list, **k return self._session.post(metadata, resource, payload) + def updateNetworkSwitchStack(self, networkId: str, switchStackId: str, **kwargs): + """ + **Update a switch stack** + https://developer.cisco.com/meraki/api-v1/#!update-network-switch-stack + + - networkId (string): Network ID + - switchStackId (string): Switch stack ID + - name (string): The name of the switch stack + - members (array): The complete list of switches that should be in the stack. Minimum 2 and maximum 8 members. Omitting this field leaves stack membership unchanged. + """ + + kwargs.update(locals()) + + metadata = { + "tags": ["switch", "configure", "stacks"], + "operation": "updateNetworkSwitchStack", + } + networkId = urllib.parse.quote(str(networkId), safe="") + switchStackId = urllib.parse.quote(str(switchStackId), safe="") + resource = f"/networks/{networkId}/switch/stacks/{switchStackId}" + + body_params = [ + "name", + "members", + ] + payload = {k.strip(): v for k, v in kwargs.items() if k.strip() in body_params} + + if self._session._validate_kwargs: + all_params = [] + body_params + invalid = [k for k in kwargs if k.strip() not in all_params and k != "self"] + if invalid and self._session._logger: + self._session._logger.warning(f"updateNetworkSwitchStack: ignoring unrecognized kwargs: {invalid}") + + return self._session.put(metadata, resource, payload) + def getNetworkSwitchStack(self, networkId: str, switchStackId: str): """ **Show a switch stack** diff --git a/meraki/api/wireless.py b/meraki/api/wireless.py index 00afe6d7..38af3999 100644 --- a/meraki/api/wireless.py +++ b/meraki/api/wireless.py @@ -2312,6 +2312,7 @@ def updateNetworkWirelessSsid(self, networkId: str, number: str, **kwargs): - dnsRewrite (object): DNS servers rewrite settings - speedBurst (object): The SpeedBurst setting for this SSID' - namedVlans (object): Named VLAN settings. + - security (object): Security settings for the SSID - localAuthFallback (object): The current configuration for Local Authentication Fallback. Enables the Access Point (AP) to store client authentication data for a specified duration that can be adjusted as needed. - radiusAccountingStartDelay (integer): The delay (in seconds) before sending the first RADIUS accounting start message. Must be between 0 and 60 seconds. """ @@ -2463,6 +2464,7 @@ def updateNetworkWirelessSsid(self, networkId: str, number: str, **kwargs): "dnsRewrite", "speedBurst", "namedVlans", + "security", "localAuthFallback", "radiusAccountingStartDelay", ] diff --git a/meraki/rest_session.py b/meraki/rest_session.py index 889f6693..0832410e 100644 --- a/meraki/rest_session.py +++ b/meraki/rest_session.py @@ -306,11 +306,19 @@ def request(self, metadata, method, url, **kwargs): raise APIError(metadata, response) # Handle 5xx errors case status if 500 <= status: + request_id = response.headers.get("X-Request-Id") or "none" if self._logger: - self._logger.warning(f"{tag}, {operation} - {status} {reason}, retrying in 1 second") + self._logger.warning( + f"{tag}, {operation} - {status} {reason} (X-Request-Id: {request_id}), retrying in 1 second" + ) time.sleep(1) retries -= 1 if retries == 0: + if self._logger: + self._logger.error( + f"{tag}, {operation} - {status} {reason} failed after retries. " + f"Provide this X-Request-Id to Meraki for log lookup: {request_id}" + ) raise APIError(metadata, response) # Handle other 4xx errors case status if status != 429 and 400 <= status < 500: diff --git a/pyproject.toml b/pyproject.toml index bd3071a4..5f5f5a1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "meraki" -version = "3.2.0" +version = "3.3.0" description = "Cisco Meraki Dashboard API library" authors = [ {name = "Cisco Meraki", email = "api-feedback@meraki.net"} @@ -41,6 +41,7 @@ dev = [ "flake8>=7.0,<8", "pre-commit>=4.6.0", "ruff>=0.15.12", + "towncrier>=24.8.0", ] generator = ["jinja2==3.1.6"] @@ -63,3 +64,46 @@ omit = ["meraki/api/*", "meraki/aio/api/*", "meraki/aio/__init__.py"] fail_under = 90 show_missing = true exclude_lines = ["pragma: no cover", "if __name__"] + +[tool.towncrier] +directory = "changelog.d" +filename = "CHANGELOG.md" +name = "meraki" +title_format = "## {version} ({project_date})" +issue_format = "[#{issue}](https://github.com/meraki/dashboard-api-python/issues/{issue})" +start_string = "\n" +wrap = false +all_bullets = true + +# Version is passed via `--version` from a single source of truth (pyproject +# [project].version) at build time, so there is no version pinned here to drift. + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true diff --git a/tests/generator/test_semantic_diff.py b/tests/generator/test_semantic_diff.py deleted file mode 100644 index 7f2f7f36..00000000 --- a/tests/generator/test_semantic_diff.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Unit tests for semantic_diff_v2_v3.py core functions.""" - -import sys -from pathlib import Path - - -SCRIPTS_DIR = Path(__file__).resolve().parent.parent.parent / "scripts" -sys.path.insert(0, str(SCRIPTS_DIR)) - -from semantic_diff_v2_v3 import extract_methods, compare_modules, check_body_wiring # noqa: E402 - - -class TestExtractMethods: - def test_extracts_simple_method(self): - content = " def getNetwork(self, networkId: str):\n pass\n" - methods = extract_methods(content) - assert "getNetwork" in methods - assert methods["getNetwork"]["params"] == {"networkId": "str"} - - def test_extracts_multiple_params(self): - content = " def updateNetwork(self, networkId: str, name: str, **kwargs):\n pass\n" - methods = extract_methods(content) - assert "updateNetwork" in methods - assert "networkId" in methods["updateNetwork"]["params"] - assert "name" in methods["updateNetwork"]["params"] - assert "**kwargs" in methods["updateNetwork"]["params"] - - def test_ignores_init(self): - content = " def __init__(self, session):\n pass\n def getNetwork(self, id: str):\n pass\n" - methods = extract_methods(content) - assert "__init__" not in methods - assert "getNetwork" in methods - - def test_handles_default_values(self): - content = " def listDevices(self, total_pages=1, direction='next'):\n pass\n" - methods = extract_methods(content) - assert "listDevices" in methods - assert "total_pages" in methods["listDevices"]["params"] - assert "direction" in methods["listDevices"]["params"] - - def test_empty_content(self): - methods = extract_methods("") - assert methods == {} - - -class TestCompareModules: - def test_identical_modules(self): - content = " def getNetwork(self, networkId: str):\n pass\n" - drifts = compare_modules(content, content, "networks") - assert len(drifts) == 0 - - def test_missing_in_v3(self): - v2 = " def getNetwork(self, id: str):\n pass\n def deleteNetwork(self, id: str):\n pass\n" - v3 = " def getNetwork(self, id: str):\n pass\n" - drifts = compare_modules(v2, v3, "networks") - missing = [d for d in drifts if d["type"] == "MISSING_IN_V3"] - assert len(missing) == 1 - assert missing[0]["method"] == "deleteNetwork" - - def test_extra_in_v3(self): - v2 = " def getNetwork(self, id: str):\n pass\n" - v3 = " def getNetwork(self, id: str):\n pass\n def newMethod(self, id: str):\n pass\n" - drifts = compare_modules(v2, v3, "networks") - extra = [d for d in drifts if d["type"] == "MISSING_IN_V2"] - assert len(extra) == 1 - assert extra[0]["method"] == "newMethod" - - def test_param_diff(self): - v2 = " def getNetwork(self, networkId: str, orgId: str):\n pass\n" - v3 = " def getNetwork(self, networkId: str):\n pass\n" - drifts = compare_modules(v2, v3, "networks") - param_diffs = [d for d in drifts if d["type"] == "PARAM_DIFF"] - assert len(param_diffs) == 1 - - def test_type_diff(self): - v2 = " def getNetwork(self, count: str):\n pass\n" - v3 = " def getNetwork(self, count: int):\n pass\n" - drifts = compare_modules(v2, v3, "networks") - type_diffs = [d for d in drifts if d["type"] == "TYPE_DIFF"] - assert len(type_diffs) == 1 - assert "count" in type_diffs[0]["detail"] - - def test_kwargs_ignored(self): - v2 = " def getNetwork(self, id: str, **kwargs):\n pass\n" - v3 = " def getNetwork(self, id: str):\n pass\n" - drifts = compare_modules(v2, v3, "networks") - # **kwargs difference should not trigger PARAM_DIFF - param_diffs = [d for d in drifts if d["type"] == "PARAM_DIFF"] - assert len(param_diffs) == 0 - - -class TestCheckBodyWiring: - def test_detects_missing_kwargs_merge(self): - content = ( - " def createNetwork(self, orgId: str, name: str, productTypes: list, **kwargs):\n" - " metadata = {}\n" - " orgId = urllib.parse.quote(str(orgId), safe='')\n" - " resource = f'/organizations/{orgId}/networks'\n" - ' body_params = ["name", "productTypes", "tags"]\n' - " payload = {k: v for k, v in kwargs.items() if k in body_params}\n" - " return self._session.post(metadata, resource, payload)\n" - ) - drifts = check_body_wiring(content, "organizations") - wiring = [d for d in drifts if d["type"] == "BODY_WIRING"] - assert len(wiring) == 1 - assert "name" in wiring[0]["detail"] or "productTypes" in wiring[0]["detail"] - - def test_passes_with_kwargs_update_locals(self): - content = ( - " def createNetwork(self, orgId: str, name: str, productTypes: list, **kwargs):\n" - " kwargs.update(locals())\n" - " metadata = {}\n" - ' body_params = ["name", "productTypes", "tags"]\n' - " payload = {k: v for k, v in kwargs.items() if k in body_params}\n" - " return self._session.post(metadata, resource, payload)\n" - ) - drifts = check_body_wiring(content, "organizations") - assert len(drifts) == 0 - - def test_passes_with_kwargs_equals_locals(self): - content = ( - " def createNetwork(self, orgId: str, name: str, productTypes: list):\n" - " kwargs = locals()\n" - " metadata = {}\n" - ' body_params = ["name", "productTypes"]\n' - " payload = {k: v for k, v in kwargs.items() if k in body_params}\n" - " return self._session.post(metadata, resource, payload)\n" - ) - drifts = check_body_wiring(content, "organizations") - assert len(drifts) == 0 - - def test_ignores_path_only_params(self): - content = ( - " def getNetwork(self, networkId: str):\n" - " metadata = {}\n" - " networkId = urllib.parse.quote(str(networkId), safe='')\n" - " return self._session.get(metadata, resource)\n" - ) - drifts = check_body_wiring(content, "networks") - assert len(drifts) == 0 - - def test_detects_query_wiring_issue(self): - content = ( - " def listClients(self, networkId: str, mac: str, **kwargs):\n" - " metadata = {}\n" - ' query_params = ["mac", "ip"]\n' - " params = {k: v for k, v in kwargs.items() if k in query_params}\n" - " return self._session.get(metadata, resource, params)\n" - ) - drifts = check_body_wiring(content, "networks") - query = [d for d in drifts if d["type"] == "QUERY_WIRING"] - assert len(query) == 1 - assert "mac" in query[0]["detail"] diff --git a/tests/integration/test_client_crud_lifecycle_async.py b/tests/integration/test_client_crud_lifecycle_async.py index 7bcad546..f471453f 100644 --- a/tests/integration/test_client_crud_lifecycle_async.py +++ b/tests/integration/test_client_crud_lifecycle_async.py @@ -201,6 +201,7 @@ async def test_delete_policy_objects(dashboard, org_id, version_salt): async def test_delete_network(dashboard, org_id, network): import asyncio from meraki.api.batch.networks import ActionBatchNetworks + from meraki.exceptions import AsyncAPIError action = ActionBatchNetworks().deleteNetwork(network["id"]) max_attempts = 5 @@ -209,12 +210,21 @@ async def test_delete_network(dashboard, org_id, network): delay = 2**attempt await asyncio.sleep(delay) - batch = await dashboard.organizations.createOrganizationActionBatch( - organizationId=org_id, - actions=[action], - confirmed=False, - synchronous=False, - ) + try: + batch = await dashboard.organizations.createOrganizationActionBatch( + organizationId=org_id, + actions=[action], + confirmed=False, + synchronous=False, + ) + except AsyncAPIError as e: + # Action batches are asynchronous: a prior attempt's batch can + # finish deleting the network after we stopped observing it. The + # network being gone is the desired end state, so a "not found" + # here means the delete already succeeded. + if e.status == 400 and "not found" in str(e.message).lower(): + return + raise assert batch is not None assert batch["id"] diff --git a/tests/integration/test_client_crud_lifecycle_sync.py b/tests/integration/test_client_crud_lifecycle_sync.py index 20e8bf75..9f003b67 100644 --- a/tests/integration/test_client_crud_lifecycle_sync.py +++ b/tests/integration/test_client_crud_lifecycle_sync.py @@ -208,6 +208,7 @@ def test_delete_policy_objects(dashboard, org_id, version_salt): def test_delete_network(dashboard, org_id, network): import time from meraki.api.batch.networks import ActionBatchNetworks + from meraki.exceptions import APIError action = ActionBatchNetworks().deleteNetwork(network["id"]) max_attempts = 5 @@ -216,12 +217,21 @@ def test_delete_network(dashboard, org_id, network): delay = 2**attempt time.sleep(delay) - batch = dashboard.organizations.createOrganizationActionBatch( - organizationId=org_id, - actions=[action], - confirmed=False, - synchronous=False, - ) + try: + batch = dashboard.organizations.createOrganizationActionBatch( + organizationId=org_id, + actions=[action], + confirmed=False, + synchronous=False, + ) + except APIError as e: + # Action batches are asynchronous: a prior attempt's batch can + # finish deleting the network after we stopped observing it. The + # network being gone is the desired end state, so a "not found" + # here means the delete already succeeded. + if e.status == 400 and "not found" in str(e.message).lower(): + return + raise assert batch is not None assert batch["id"] diff --git a/tests/unit/test_aio_rest_session.py b/tests/unit/test_aio_rest_session.py index e14f9131..d198b50f 100644 --- a/tests/unit/test_aio_rest_session.py +++ b/tests/unit/test_aio_rest_session.py @@ -363,6 +363,59 @@ async def test_retry_on_500(self, async_session): result = await async_session._request(_metadata(), "GET", "/organizations") assert result.status == 200 + @pytest.mark.asyncio + async def test_request_id_logged_in_warning(self, async_session_with_logger): + session = async_session_with_logger + resp_500 = _mock_aio_response( + 500, + reason="Internal Server Error", + headers={"X-Request-Id": "abc123def456"}, + ) + resp_200 = _mock_aio_response(200) + session._req_session.request = AsyncMock(side_effect=[resp_500, resp_200]) + + with patch(SLEEP_PATCH, side_effect=_noop_sleep): + result = await session._request(_metadata(), "GET", "/organizations") + + assert result.status == 200 + warning_messages = [c.args[0] for c in session._logger.warning.call_args_list] + assert any("X-Request-Id: abc123def456" in m for m in warning_messages) + + @pytest.mark.asyncio + async def test_request_id_logged_as_error_after_exhausting_retries(self, async_session_with_logger): + session = async_session_with_logger + session._maximum_retries = 2 + resp_500 = _mock_aio_response( + 500, + reason="Internal Server Error", + headers={"X-Request-Id": "deadbeef00112233"}, + ) + session._req_session.request = AsyncMock(return_value=resp_500) + + with patch(SLEEP_PATCH, side_effect=_noop_sleep): + with pytest.raises(AsyncAPIError): + await session._request(_metadata(), "GET", "/organizations") + + error_messages = [c.args[0] for c in session._logger.error.call_args_list] + assert any("deadbeef00112233" in m for m in error_messages) + assert any("Provide this X-Request-Id to Meraki" in m for m in error_messages) + + @pytest.mark.asyncio + async def test_no_request_id_logs_none(self, async_session_with_logger): + session = async_session_with_logger + session._maximum_retries = 2 + resp_500 = _mock_aio_response(500, reason="Internal Server Error", headers={}) + session._req_session.request = AsyncMock(return_value=resp_500) + + with patch(SLEEP_PATCH, side_effect=_noop_sleep): + with pytest.raises(AsyncAPIError): + await session._request(_metadata(), "GET", "/organizations") + + warning_messages = [c.args[0] for c in session._logger.warning.call_args_list] + assert any("X-Request-Id: none" in m for m in warning_messages) + error_messages = [c.args[0] for c in session._logger.error.call_args_list] + assert any("log lookup: none" in m for m in error_messages) + @pytest.mark.asyncio async def test_5xx_raises_after_max_retries(self, async_session): async_session._maximum_retries = 2 diff --git a/tests/unit/test_rest_session.py b/tests/unit/test_rest_session.py index 1fb4b58a..e6fe4035 100644 --- a/tests/unit/test_rest_session.py +++ b/tests/unit/test_rest_session.py @@ -32,6 +32,31 @@ def session(): return s +@pytest.fixture +def session_with_logger(): + with patch("meraki.rest_session.check_python_version"): + s = RestSession( + logger=MagicMock(), + api_key="fake_api_key_1234567890123456789012345678901234567890", + base_url="https://api.meraki.com/api/v1", + single_request_timeout=60, + certificate_path="", + requests_proxy="", + wait_on_rate_limit=True, + nginx_429_retry_wait_time=2, + action_batch_retry_wait_time=2, + network_delete_retry_wait_time=2, + retry_4xx_error=False, + retry_4xx_error_wait_time=1, + maximum_retries=2, + simulate=False, + be_geo_id="", + caller="TestApp TestVendor", + use_iterator_for_get_pages=False, + ) + return s + + def _metadata(operation="getOrganizations", tags=None): return {"tags": tags or ["organizations"], "operation": operation} @@ -172,6 +197,61 @@ def test_5xx_raises_after_max_retries(self, mock_sleep, session): with pytest.raises(APIError): session.request(_metadata(), "GET", "/organizations") + +# --- X-Request-Id logging on 5xx --- + + +class TestRequestIdLoggingOn5xx: + @patch("time.sleep", return_value=None) + def test_request_id_logged_in_warning(self, mock_sleep, session_with_logger): + session = session_with_logger + resp_500 = _mock_response( + 500, + reason="Internal Server Error", + headers={"X-Request-Id": "abc123def456"}, + ) + resp_200 = _mock_response(200) + session._req_session.request = MagicMock(side_effect=[resp_500, resp_200]) + + result = session.request(_metadata(), "GET", "/organizations") + + assert result.status_code == 200 + warning_messages = [c.args[0] for c in session._logger.warning.call_args_list] + assert any("X-Request-Id: abc123def456" in m for m in warning_messages) + + @patch("time.sleep", return_value=None) + def test_request_id_logged_as_error_after_exhausting_retries(self, mock_sleep, session_with_logger): + session = session_with_logger + session._maximum_retries = 2 + resp_500 = _mock_response( + 500, + reason="Internal Server Error", + headers={"X-Request-Id": "deadbeef00112233"}, + ) + session._req_session.request = MagicMock(return_value=resp_500) + + with pytest.raises(APIError): + session.request(_metadata(), "GET", "/organizations") + + error_messages = [c.args[0] for c in session._logger.error.call_args_list] + assert any("deadbeef00112233" in m for m in error_messages) + assert any("Provide this X-Request-Id to Meraki" in m for m in error_messages) + + @patch("time.sleep", return_value=None) + def test_no_request_id_logs_none(self, mock_sleep, session_with_logger): + session = session_with_logger + session._maximum_retries = 2 + resp_500 = _mock_response(500, reason="Internal Server Error", headers={}) + session._req_session.request = MagicMock(return_value=resp_500) + + with pytest.raises(APIError): + session.request(_metadata(), "GET", "/organizations") + + warning_messages = [c.args[0] for c in session._logger.warning.call_args_list] + assert any("X-Request-Id: none" in m for m in warning_messages) + error_messages = [c.args[0] for c in session._logger.error.call_args_list] + assert any("log lookup: none" in m for m in error_messages) + @patch("time.sleep", return_value=None) def test_retry_on_connection_error(self, mock_sleep, session): exc = requests.exceptions.ConnectionError("Connection refused") diff --git a/uv.lock b/uv.lock index e67da646..92d1d8f2 100644 --- a/uv.lock +++ b/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.14.0" +version = "3.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -25,108 +25,108 @@ dependencies = [ { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ee/ab/93ce242f899b68c51b0578c027aafa791ab3614cb9345fa5d37b5f5c8e3e/aiohttp-3.14.0.tar.gz", hash = "sha256:2882de819734c715fd1b9c11c97e09fa020d14438203d1d354d8ed1702791c9b", size = 7940674, upload-time = "2026-06-01T19:41:02.763Z" } +sdist = { url = "https://files.pythonhosted.org/packages/82/78/8ea7308cac6934de8c74a14f3d5f65d1c89287426688be79538d0e5c013d/aiohttp-3.14.1.tar.gz", hash = "sha256:307f2cff90a764d329e77040603fa032db89c5c24fdad50c4c15334cba744035", size = 7955794, upload-time = "2026-06-07T21:09:35.529Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/67/47/7727bfe8db93f8835a001bd4359d8480cc68d1259b8bce334668f8be97bd/aiohttp-3.14.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:54bf3522d6f7351e55f89a62d5c2bf138ad557b031670266c5df604ae88e0b5a", size = 759147, upload-time = "2026-06-01T19:37:12.918Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f2/cd3fedff6fade73d71df9ec908c210cec518ef90fd00289250684b90aecf/aiohttp-3.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0746d9fb0ac4fdef643a84494efe3f06d50335dd8c7a530228b86448aae0a803", size = 513705, upload-time = "2026-06-01T19:37:14.633Z" }, - { url = "https://files.pythonhosted.org/packages/5a/fe/49746b6b610144a06323bebd8e1211a390310d8c69b98dd6d52df341bc3e/aiohttp-3.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9f3a96b6d39a4872222beee72e1df41d2ff886ae96152cf3e757ef8c5673ef0e", size = 509627, upload-time = "2026-06-01T19:37:16.385Z" }, - { url = "https://files.pythonhosted.org/packages/4c/3f/28f2f6cf3d5c0e7b01b27140d0e7873fd11fb341169ad3ce78ad04aba628/aiohttp-3.14.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d336820adbb914debbc90a1d8c1bfc4bea55996aecf64866a989d35d1f9fd903", size = 1769293, upload-time = "2026-06-01T19:37:18.067Z" }, - { url = "https://files.pythonhosted.org/packages/97/6f/2e5f1b525d5474b12b3c60abf733a755845f3bceff21542081ada515f837/aiohttp-3.14.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:71b2604c9bfc1b115547d63a094d5244b3f02799833513a99a68aaa7b167c4cb", size = 1732363, upload-time = "2026-06-01T19:37:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/a8/ce/596120faa85ca7b19cd061e3f2f3be23aa8f11a0aedf9191db9e0da1bd76/aiohttp-3.14.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:610d68800435903e303ca0542b9d3e4eb72a12ff33a6d471a070c1d81eebd3c2", size = 1840375, upload-time = "2026-06-01T19:37:22.104Z" }, - { url = "https://files.pythonhosted.org/packages/72/3c/a7ffe05a757a4a7867643da69357ec41f506879fbd1b231d2ed90af246b2/aiohttp-3.14.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:514db9a79337068981ee2137310283a07b4b885c584991097a91a4da419bcb81", size = 1921484, upload-time = "2026-06-01T19:37:24.068Z" }, - { url = "https://files.pythonhosted.org/packages/93/fa/2c861170bbd4a491de93a69e081db1d971092569e0d593a98ef62c384dc1/aiohttp-3.14.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c452d17eeb95d563fc8b936f3050301dbd1d268126c4632d8b70ede9696202ee", size = 1774153, upload-time = "2026-06-01T19:37:26.256Z" }, - { url = "https://files.pythonhosted.org/packages/9d/da/1d2f5a165f47ec9b1f69d37b8b977fdc4d501aa72ffb7930db27bb9e49ea/aiohttp-3.14.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed94a81506e3d1bdbad5108f497a58f2a2354aedb4ca314d5326f07d1fd1ac2d", size = 1632569, upload-time = "2026-06-01T19:37:28.192Z" }, - { url = "https://files.pythonhosted.org/packages/46/1d/7a6e295c4257252f70f69e90864fdad74b6a1293054fb3f9e65a15de6d63/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1394dce36e0f0d260ac0b555a654de19cb989f3c1b8bdd24f505314dfea18a00", size = 1740325, upload-time = "2026-06-01T19:37:30.08Z" }, - { url = "https://files.pythonhosted.org/packages/f1/7e/e1899b1ca3ec62f1eab2a5cbde14039b97493f7f53eb88d9b668562ffa8d/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d1467d1e7b48a73ca7237e0ee4335f3d02b923dbc27b82fd254bc301c97d4026", size = 1748691, upload-time = "2026-06-01T19:37:32.211Z" }, - { url = "https://files.pythonhosted.org/packages/ec/54/4e6b61c1fe7d3433f82bcc6bd7e4d7c683a742a10c9b12a025fd3695c047/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6a5f3532125233c261cf61f32df4059cfcf482eb793c7d3db8452e3142028b86", size = 1814477, upload-time = "2026-06-01T19:37:34.173Z" }, - { url = "https://files.pythonhosted.org/packages/9c/38/86fd51be2e08d8e45c83d879d255f10391903cd9fe2a16512f7591a15873/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3ea81eb518a2ecb319d8ec6d1424a37c773f6634bd87d6985eb606b2faac419f", size = 1623393, upload-time = "2026-06-01T19:37:36.281Z" }, - { url = "https://files.pythonhosted.org/packages/78/49/466e947a42a88ee23c486d036e7e5d1b097f1bafd8084ad9c9a0a92f0f43/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:32e735c3182de7b64f6941a4ede48b38c7f47d9437bd615dd30b5bda8fa1bc93", size = 1824097, upload-time = "2026-06-01T19:37:38.421Z" }, - { url = "https://files.pythonhosted.org/packages/f3/89/35f3410bc284682338a1be6b6ea0c5abfa05f063942cfaa9256608440434/aiohttp-3.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c21ca9a1c63d4509158f478aeb9d02914dcc52adc68d1bc9dee2452284ee5996", size = 1764790, upload-time = "2026-06-01T19:37:40.755Z" }, - { url = "https://files.pythonhosted.org/packages/42/80/2d4291bd5724d3d17e5951aff5a3e02281483fb47295f0788276ee66cd73/aiohttp-3.14.0-cp311-cp311-win32.whl", hash = "sha256:19ca5fc84130675ba11c6ca5c7da5cb65f7bf8a32cdd2b616bf49cd334688aae", size = 454176, upload-time = "2026-06-01T19:37:42.837Z" }, - { url = "https://files.pythonhosted.org/packages/59/ed/41d0ad4f6ececffc32bdf1f7b494e5498f7ca5c849ea2e3cc9bbd1668251/aiohttp-3.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:d488e6e9d3bb8ba5ae7066d5be885ae9670eba021b8c6ccb9a3a568e6b19d6e5", size = 479334, upload-time = "2026-06-01T19:37:44.776Z" }, - { url = "https://files.pythonhosted.org/packages/d1/86/c0b5e305c770053f8c3d069bb52b8196917ba91949d1962d52eb307fb0d2/aiohttp-3.14.0-cp311-cp311-win_arm64.whl", hash = "sha256:8b93618102caf12801638a01a2b478a55410ddd71bd41cfaf6f707953a49ac43", size = 450262, upload-time = "2026-06-01T19:37:46.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/97/2b6889bfb6b6847520d50d95eb8c4307a45e28aaca39faf4a9454b3d1b2f/aiohttp-3.14.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:b29518c9c2ec7e373e68259206a137c7f4f5439c58baaec4b5ab3ab799850a4e", size = 750194, upload-time = "2026-06-01T19:37:48.164Z" }, - { url = "https://files.pythonhosted.org/packages/21/e2/62634b7fff918ed98c3c6b2f0e70d520f7f28846cb412d451b04354c6459/aiohttp-3.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:dbec68ce61b64cb73cab4d33df9433427b1713c8bcccb181dce695c1b6f8e87c", size = 506966, upload-time = "2026-06-01T19:37:50.014Z" }, - { url = "https://files.pythonhosted.org/packages/dd/fb/5ce075150828c797a5106f1c2fb26034e709d4289b9d2bf8b07f1e59fac6/aiohttp-3.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3cdf534aa455593e589302990c5097aa5c92c06c4262a20da22934f9186a5fff", size = 507527, upload-time = "2026-06-01T19:37:51.96Z" }, - { url = "https://files.pythonhosted.org/packages/01/d5/405a0ae4e6b081754a3609c1c97c63a950e000a2def16046f1e736933a0e/aiohttp-3.14.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb6c657104393b5fbff01a5f59b2023db74058a8077d94475d6c25d03882a108", size = 1762420, upload-time = "2026-06-01T19:37:53.839Z" }, - { url = "https://files.pythonhosted.org/packages/ae/1d/e05a7c896b15a6bc6fb8fc5319eb437861c2c49c34559ef928add6590315/aiohttp-3.14.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:46fbbec4e4fab7428d4396a3823f9320e4560aa3113b89eeebce712c27c9ed5a", size = 1733672, upload-time = "2026-06-01T19:37:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/cc/22/a72f7c459e195fa41bf4f7abd1f925b91fe91f8097e51c654229ba144a33/aiohttp-3.14.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2c2c7e05dd5335b298085abf45ddf98673934c3ee1c083d0b9ea13d4186ad500", size = 1805064, upload-time = "2026-06-01T19:37:57.931Z" }, - { url = "https://files.pythonhosted.org/packages/80/50/e85bdaba0be59ca4838005ebfef4048fcdd5f35a02b07057a9a123394440/aiohttp-3.14.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3c7139100fbaae76515b73051d8f0aa3a3ff02e415eec8a8eee8e2223d9ba955", size = 1902125, upload-time = "2026-06-01T19:38:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/19/d8/51de5c6b971c27bb1ef620293b8d1ca611ec78736b34b3f6ccf68e4c8785/aiohttp-3.14.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78d6f9286a629ce52728430afe18f8ed2b6c39a1fddb3802d7244b9983910ad2", size = 1783112, upload-time = "2026-06-01T19:38:02.641Z" }, - { url = "https://files.pythonhosted.org/packages/73/ae/b4402bfde77e43dfb1b6ccff83c7b7ab63ed06b50c4754f0c5423fb374fe/aiohttp-3.14.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cc3c3e12cdaeb92d7dcf13db00e9f6b1956b910e47256e696df1cfa946d02159", size = 1586356, upload-time = "2026-06-01T19:38:04.637Z" }, - { url = "https://files.pythonhosted.org/packages/bc/05/750a3265ca4dc54a460bd0cb1121a8f2ce9171fce4a135fb47ea7fd594d2/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4d6a998191f5ebe3b8c28463ff72bc030250008b3193c402464efadd08b5ca02", size = 1723119, upload-time = "2026-06-01T19:38:06.713Z" }, - { url = "https://files.pythonhosted.org/packages/37/01/8c0812c50b3b1b1c37b323bf170d6be8847a8f234060485b7d1e71953f60/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0fc2b75ae8d169d853be2862d960be8550da6c5c65711d5476407eb3fdb006bd", size = 1757216, upload-time = "2026-06-01T19:38:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/2a/50fb98028a26887cbe48dcc1df92a90825615bc73b5584301304090cded8/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:16eee56bcc72d04600bc56c1759982c2385ec0b41d3fd3521f836bf64a0957ef", size = 1770500, upload-time = "2026-06-01T19:38:11.111Z" }, - { url = "https://files.pythonhosted.org/packages/bd/32/0ffd598a2fa2b9a423daf242e700cfdabda35d6e602394ad9ae58972c1c7/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:5a2e7ca615c3ddc15b82687e05a624e5f5cba3f1d6c20cb81172d70ea498451e", size = 1576224, upload-time = "2026-06-01T19:38:13.391Z" }, - { url = "https://files.pythonhosted.org/packages/0b/f9/b9fc381dd9b66afb33f2634c40e229d106467be0afcabe79648631ab6712/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f0b7b8bbbec3ce9467ee0ebe334622fd90624f593edd3136c567811453fc4fae", size = 1794252, upload-time = "2026-06-01T19:38:15.498Z" }, - { url = "https://files.pythonhosted.org/packages/a8/fb/05d9214c975f23225a8cd5c439325e338c7c377b315480ef3871db51f54e/aiohttp-3.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ba10966d4f03dd96a14365be4b8e37c327c76f11c3ca867116966cdd9f98066", size = 1760193, upload-time = "2026-06-01T19:38:17.624Z" }, - { url = "https://files.pythonhosted.org/packages/d9/4b/02992fc4fb9e1b6673ee3f888a8e587a6447afda1f6f4aca776c148c2876/aiohttp-3.14.0-cp312-cp312-win32.whl", hash = "sha256:101df7779c80c0636014a6b2c6642acd3efb5b355d48347c9d7dfb720aee9430", size = 448650, upload-time = "2026-06-01T19:38:19.545Z" }, - { url = "https://files.pythonhosted.org/packages/39/e9/246532214c3abda518477cbaaf16d420295ad8effa5233844cbb38f299ab/aiohttp-3.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:b0a5747586d4467efd1f932710b269131c9717a872dce082cd92a00c1c13123a", size = 476145, upload-time = "2026-06-01T19:38:21.505Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c3/63f8c20090048915711598b0adf475b149216d736157961de06480a45b15/aiohttp-3.14.0-cp312-cp312-win_arm64.whl", hash = "sha256:5f1c5be60add78fabb4aacd13c5a348ae79d2fcbfc7fa78da8f1eb192273b370", size = 444250, upload-time = "2026-06-01T19:38:24.027Z" }, - { url = "https://files.pythonhosted.org/packages/21/61/d11f7d9a3144bffe825247d6367cd93053666da50b94707c9129c78868d5/aiohttp-3.14.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:25400d710641a8040bf022a8a99f579e581ffa1c5bd42c33255d7d6f3957c127", size = 502399, upload-time = "2026-06-01T19:38:25.955Z" }, - { url = "https://files.pythonhosted.org/packages/4f/9b/a7e317625d36356844f8bb022cabd305b541f968856cc3c2e0b58e53ee6e/aiohttp-3.14.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:c5492b9929826e07cc3fcb9739ae87aab05dff6b5e67a9b73fd1700c6d008981", size = 510068, upload-time = "2026-06-01T19:38:27.828Z" }, - { url = "https://files.pythonhosted.org/packages/11/41/cc2d2cfbfbdc3126ba258f3cd27d1ac8a33492ae3c35a4583ee21f0ba7f1/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3366751d68d237c621264233a32f3078bbc21b7904ab90a77e03d21390c742c6", size = 481670, upload-time = "2026-06-01T19:38:29.836Z" }, - { url = "https://files.pythonhosted.org/packages/3c/07/381f4023c3b08cb616e520f566d8c58957abad54e56441d41fe67cfb0195/aiohttp-3.14.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:57ea07d28695a7a40304d42251892a8df765e5588c10ee32afeddcd5df33c0a2", size = 487591, upload-time = "2026-06-01T19:38:31.704Z" }, - { url = "https://files.pythonhosted.org/packages/fb/4d/4506fdb7a022bdf70011a3bbb4ca00c5c570026ef6a3c5bd7bc70c39089c/aiohttp-3.14.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:076cb014191ae2e65d949e1ad01f1dcfe33e32789b5172510f3e79c79fc04d50", size = 496503, upload-time = "2026-06-01T19:38:33.6Z" }, - { url = "https://files.pythonhosted.org/packages/ef/7d/c814111e04894a45d9e2defc94443879a6f118d9633d5fedfe6e2e8af5f0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2f3fc37054564dee64a855b5b092d87ec35dcddfaabf7dacb1c8a2b1f83dc0a9", size = 745870, upload-time = "2026-06-01T19:38:36.013Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ee/80eee0efddfe187e7cd05027086b7ce1c0e492e82a4eda58f5c5543a44a0/aiohttp-3.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8fcaef74d2ab0f607d7ff85a0d15e21bb5a258c4a58df1908396eb50d7f4ed3c", size = 505588, upload-time = "2026-06-01T19:38:38.282Z" }, - { url = "https://files.pythonhosted.org/packages/d6/f8/0f28f04eef75d52fc9c715dde7ce9c0abb810fd20cfeb0fea7afd2ab1e98/aiohttp-3.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e4c01b0bfc6209590960e68eac083cd22d5d87c21f974dd6208cafa5d3542bc8", size = 504492, upload-time = "2026-06-01T19:38:40.611Z" }, - { url = "https://files.pythonhosted.org/packages/ff/db/44c755232085545065c94378dfce38641b1aee647f4939fcd32f5b32e719/aiohttp-3.14.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f12eb7896e81caf403a2b18c9406426f1207361e7239c057ab29c076d4257e83", size = 1752111, upload-time = "2026-06-01T19:38:42.682Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6a/42e030a46743841414402a3b00cd3d78419055e86c66fb5822c14b5abfc6/aiohttp-3.14.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c79a044cacf360ec46738d863d2f41c9300d2a06ef4a7402ea0df306a350e61", size = 1729674, upload-time = "2026-06-01T19:38:44.79Z" }, - { url = "https://files.pythonhosted.org/packages/34/26/3199beb415202e3108e7b83ecebe10914d806d33fb9860c3e4aa60a19be3/aiohttp-3.14.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:85e0675f47be4eff0636bf88c02140ea89168ae0df3ff1f3f464e9de9610d277", size = 1798808, upload-time = "2026-06-01T19:38:47.01Z" }, - { url = "https://files.pythonhosted.org/packages/bd/94/b9b6fcf0ee17c21d0d19fb8c22bf83ad18f82e702a9c3bd901a868f5e446/aiohttp-3.14.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b33e751cab03fdc960095b1e326cb5a03f5ee577d6ded59f3d1c100f8668882", size = 1891921, upload-time = "2026-06-01T19:38:49.233Z" }, - { url = "https://files.pythonhosted.org/packages/c5/a3/3800dbd095cb2bb165a7ea5d94d790914677e27f45638c7d80e3f34c8945/aiohttp-3.14.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26d9224c6dd7f5c749aba4f61315a894601448b28d94d12f4dea0903e26d2096", size = 1777241, upload-time = "2026-06-01T19:38:52.04Z" }, - { url = "https://files.pythonhosted.org/packages/21/2a/45be91ad1b860508557448d4cc2e165a2ee68dd865657b73bf66cc5a00fb/aiohttp-3.14.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6281aecdf2732940f4fe06bd6adec5ae4d59b78b080b8e3a6b81467301010988", size = 1579554, upload-time = "2026-06-01T19:38:54.508Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/dc94df99ed1511fdf28314f722643ed334112643cab00223577085e788c4/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:23e8314e7aed8576fbe33314d218bd81447a3adbc91dc36f1163bf583cd3084c", size = 1714864, upload-time = "2026-06-01T19:38:56.788Z" }, - { url = "https://files.pythonhosted.org/packages/ae/e4/1f1c8acbb3acd5c8f795473b92c9c3d44eb60a5692c6104256c8a1c83a0c/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:3b54fbff46127aeafdd764cecd0d99fa2f24a0e37ea5c18a7c3a4ac450df1db3", size = 1749803, upload-time = "2026-06-01T19:38:59.367Z" }, - { url = "https://files.pythonhosted.org/packages/0b/c8/c45ea6e7ed84cebba939b9c334498a045ba19d79c61b0110df5f21580de3/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b27d89af91a555f58e08e4902dbcbc48862fd40095720ca705990476bd93b7ac", size = 1765023, upload-time = "2026-06-01T19:39:01.651Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a1/a932941784432962fe390e1066823aaef64b4e5ac9fa595df57b5fe472a9/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:25d2326a4967bf705a9f9913a13005e93b6020ad8a9f6bd6bd78850d5171332e", size = 1571671, upload-time = "2026-06-01T19:39:04.044Z" }, - { url = "https://files.pythonhosted.org/packages/b0/01/e1280feac522597a4d46eb67a0cdfa053cfae263033030b761ab146f29fb/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a1d209375c503472b3c0a340cdf3c55fcd82e84b46dda7caeaced59faba373ec", size = 1789904, upload-time = "2026-06-01T19:39:06.294Z" }, - { url = "https://files.pythonhosted.org/packages/fa/10/ab28818262f4d26bdb47ed5f1fc7999b69e2fc6e0370b02d0f49011f45ea/aiohttp-3.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:666c7c5036df57b693026398b69b41874a1931ac5b3485fd910e57bfac253869", size = 1754516, upload-time = "2026-06-01T19:39:08.788Z" }, - { url = "https://files.pythonhosted.org/packages/af/cc/c122eabd7a1b7e0c9bbdd6be60e4715905b858399145d9df872bb94f1427/aiohttp-3.14.0-cp313-cp313-win32.whl", hash = "sha256:23f094a1ef64823fd35854ddf5c7a80a078162f37f9d2f7c6142b51a6affa456", size = 448656, upload-time = "2026-06-01T19:39:11.171Z" }, - { url = "https://files.pythonhosted.org/packages/41/a5/bab07d79848a00eedd8ed979ccb302aaea3ac6eb9fa16bd0ed87135869b4/aiohttp-3.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:e03abdaa17d553f17e1d1d06bb266b3970106c78051d06795723e748d8e49d11", size = 475803, upload-time = "2026-06-01T19:39:13.439Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a0/f03ade8566c153666a3871afccbedf6d99911da006325e1fc6cf72a2de99/aiohttp-3.14.0-cp313-cp313-win_arm64.whl", hash = "sha256:acdb400538cf4769543548bb5d1eb23d39bed4f96554a6078cb728c7cb2c268b", size = 443889, upload-time = "2026-06-01T19:39:15.945Z" }, - { url = "https://files.pythonhosted.org/packages/28/03/5f36ab196a88ba5e9648ae5643e6531e67a3a8c0e96f9c6510ff41540fec/aiohttp-3.14.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:363ef9e91014e7891679bfb2ac0a7c6ea93435dbbfd10ecf41b9f06fcf506c5f", size = 503330, upload-time = "2026-06-01T19:39:18.195Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ce/8b49ec2f30f68e02f314f4832186cd45e583360a5a386058be36855d23b6/aiohttp-3.14.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:884a4edbdad77be9d0ef36142c8b504351b170df0bf62b51e784fadabf311c42", size = 509822, upload-time = "2026-06-01T19:39:20.396Z" }, - { url = "https://files.pythonhosted.org/packages/1a/fe/6edbf5d39bf29322b6816365b17ed8ede4dace164a3aea1abcd30110eb78/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:70ea956f6cc4a37620966b56c2e205d88ca3e6d85ec063277e414b1035cddad3", size = 483329, upload-time = "2026-06-01T19:39:22.607Z" }, - { url = "https://files.pythonhosted.org/packages/1b/5a/fae531bdbc6456fb6241f46b7b81e4d8a0dd3fc09118a0055dc7141ac1ec/aiohttp-3.14.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:ea3b9806c89f61da22fddf1f12dd524fb368e5e28f1261fbdafe5c3cd8ce893b", size = 489502, upload-time = "2026-06-01T19:39:24.881Z" }, - { url = "https://files.pythonhosted.org/packages/36/f4/48a7b0414db7fed77a03d5dde34508c026afd83510ab6bca08c313855776/aiohttp-3.14.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a071be341c2bd9b0188e62d173509f024e0a35b1c342c53c50f8daaeda8c3bd8", size = 497357, upload-time = "2026-06-01T19:39:27.197Z" }, - { url = "https://files.pythonhosted.org/packages/75/75/e85a13a370acc007fca5feb1fd1b88ac2d8426e6dadd625479b7cadd55a3/aiohttp-3.14.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:198cfe61bf253b19da1fb3e0fa122249dc4f14c12709493fed8054aa0411cc76", size = 750898, upload-time = "2026-06-01T19:39:29.563Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e4/3d637f800c724eff0e2bed64df72557444482366fd0a35b0cec0e6968f6c/aiohttp-3.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc203d6ce6b9106d54e2a93f41dfdfebfbca2d99962ba503bfd3e5921a6549e", size = 506986, upload-time = "2026-06-01T19:39:31.872Z" }, - { url = "https://files.pythonhosted.org/packages/1d/df/35161f3598bf7501d2b2a805b41ab4f45a2e34150c421bcb4ef8c0d281a7/aiohttp-3.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9e19d17ab02bf16832a2c8c0d55a486792c5b1645665652ee9531aebcc30cb72", size = 508033, upload-time = "2026-06-01T19:39:34.137Z" }, - { url = "https://files.pythonhosted.org/packages/e5/39/b36e5d3d31e850fb4691dd3e941684ac490a2559249f6fa634b6b0fdf020/aiohttp-3.14.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d925fba0c14d5b498a8028b0107beebdfd16c5d48d702ff54f879cb017aaaca3", size = 1746213, upload-time = "2026-06-01T19:39:36.654Z" }, - { url = "https://files.pythonhosted.org/packages/b1/28/24e1409e605a9aa5d84abe0e2acb365354b70ae56d40948101cabe3341ab/aiohttp-3.14.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d33e61021222ce7f9792bcac870d6f58d8adfceda33ab857b01264f4560f2c5f", size = 1705862, upload-time = "2026-06-01T19:39:38.968Z" }, - { url = "https://files.pythonhosted.org/packages/8c/d0/e5eb3ff1daeaf644c7e36a957517672494122628e067c38b263fa04eda77/aiohttp-3.14.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:44eca38755d0105bb32f47d085f5dd449846a449e1245fc105889e3279dcf8e3", size = 1798909, upload-time = "2026-06-01T19:39:41.334Z" }, - { url = "https://files.pythonhosted.org/packages/d3/ba/8943f906f0570342886ababb9a722a44e360f786a028c5e0b0e29e3f735b/aiohttp-3.14.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f13087e06f68fea4941c21a0c541c00553aa16e4f8fd7bbe2b198df761e964d6", size = 1868892, upload-time = "2026-06-01T19:39:43.807Z" }, - { url = "https://files.pythonhosted.org/packages/3a/05/27df32c844b2156e1675a8d8ec22d963e3c8ba469ed7ceb1863320c7b521/aiohttp-3.14.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ff82be7f1ef73634cb77890a770743239bc3d487b848669be1c599889336dc0a", size = 1751659, upload-time = "2026-06-01T19:39:46.398Z" }, - { url = "https://files.pythonhosted.org/packages/7f/62/da182e5910ab912b2e88aa919b61a16046a37a95714a5795b02eb57b2d18/aiohttp-3.14.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a150c0875ac8fd87f1c398650841308a30d65facf7416b12dbdb9cfdcbe5a48c", size = 1578775, upload-time = "2026-06-01T19:39:48.902Z" }, - { url = "https://files.pythonhosted.org/packages/66/e3/53c67097e8a5ce98625e91e3fa7f43c9c6940de680345d03b3509a72a078/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:edc01ea4e1ec5a1649a28866262bf24195889ff7b27bdd947029a6086741de9b", size = 1710090, upload-time = "2026-06-01T19:39:51.392Z" }, - { url = "https://files.pythonhosted.org/packages/dd/55/0e2732ca598c7a4dfe8a775662376d0ca2977cb1030e48386d4da5d9a456/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:540632bf882ff8fc88f2e1697be0761578e89e0d79fb4a8a6d65dc5da7e729d4", size = 1715016, upload-time = "2026-06-01T19:39:53.807Z" }, - { url = "https://files.pythonhosted.org/packages/5a/96/f0b73730798c9ca525afc30b39f1f81bbe24e245d9654c54d3b39d63212d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:860a86bc2c80237f5dff52edcf427e10a8d8352271fd84845429a3e60199e02c", size = 1763810, upload-time = "2026-06-01T19:39:56.31Z" }, - { url = "https://files.pythonhosted.org/packages/71/cc/11acb6c4518f448323405a7312b6f255d0f974a34373ad1db7633c4aadc8/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5cbd50e6a50d6b99283a826b18cbdebf65b0797689a7535cb0e9dd37be0f63c3", size = 1573064, upload-time = "2026-06-01T19:39:58.718Z" }, - { url = "https://files.pythonhosted.org/packages/de/2d/28c31dde0a7dc98c0ee7d0da2ddcec3f7688c4fc131e5989e278d0c03c0a/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20144819e99db593e22bbd2f3f2691a5e149f879142d6b8670254708853ff4fb", size = 1775765, upload-time = "2026-06-01T19:40:01.195Z" }, - { url = "https://files.pythonhosted.org/packages/b8/69/155c4ef3aec96417d47024800472b33b16c5d8a665371dcd044c2afdf25d/aiohttp-3.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:26b6d79aa54cb4ed50cc7d41ed14e99e0f1fc8e7c2d42f2e05b37aea897b2b52", size = 1733716, upload-time = "2026-06-01T19:40:03.631Z" }, - { url = "https://files.pythonhosted.org/packages/5f/44/6126116fd8a316b712bb615660b855c78466bb67ba1bb1742427eafcf7ac/aiohttp-3.14.0-cp314-cp314-win32.whl", hash = "sha256:106ed074a856f3e21d186b8579e2c8afb6da598e267cdaab01059e13db2fc44d", size = 453684, upload-time = "2026-06-01T19:40:06.277Z" }, - { url = "https://files.pythonhosted.org/packages/a2/d7/eff4c58a88c5cac5e38b55f44fb8a6d3929c3cbd77356e383e094d3220bd/aiohttp-3.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:4f770846edae8f00ecc57af825bce811f787f87a7dcf0e90d191790efe5b31f7", size = 481758, upload-time = "2026-06-01T19:40:08.653Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ed/17b5bd9fbcb46e688f02e572f517754a9a75831e7b54702f027761dc4fa5/aiohttp-3.14.0-cp314-cp314-win_arm64.whl", hash = "sha256:acf1581c4f21ed4b80a2dded504d87b055a071a84d5737ea966435f768275ac6", size = 450557, upload-time = "2026-06-01T19:40:11.03Z" }, - { url = "https://files.pythonhosted.org/packages/12/34/6180103ce9aabc8ebff3f7bb55a1228ffe60f61042823031d9692cb7b101/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6aa1a40f9cbb3da9f80714c5966b8946c21e6a2530d809b9498b33161e3c8733", size = 787878, upload-time = "2026-06-01T19:40:13.401Z" }, - { url = "https://files.pythonhosted.org/packages/92/e9/08954a40e8b7baa3d8beadd2b074b186e9b1e9c8ddabc288678a6265de50/aiohttp-3.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b62af5a8cc96a194eaa01a9ed7b34a3ffa58d3d8daaa1a0d7a749353ad12d228", size = 524400, upload-time = "2026-06-01T19:40:15.972Z" }, - { url = "https://files.pythonhosted.org/packages/08/6a/b5965a634ac4d5ba99a463314cf4ab214ca073fcdc38a15e0294273701fc/aiohttp-3.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6eb63b1417efaf7d1002a6ad034a40d44376afcc16508a57f8e74b49ad26a095", size = 527904, upload-time = "2026-06-01T19:40:18.28Z" }, - { url = "https://files.pythonhosted.org/packages/06/b4/932bcdd850c354d9bcca30f360e475d7852e30413fbbd44b182782ed5432/aiohttp-3.14.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c20b9ad156a79eb97be5cf9e069eec01d2f0dc8472ffbd75299a8b2d4c2cbbde", size = 1912162, upload-time = "2026-06-01T19:40:20.825Z" }, - { url = "https://files.pythonhosted.org/packages/c6/85/ce79bab0310d2e3fd2d7bc7e44412abeff7c8338f8a21dd0f2f1714989e5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:40ae7b0642c25632c7eabc4a04754012691864d2a1b93becf7cddb76027b838a", size = 1778813, upload-time = "2026-06-01T19:40:23.726Z" }, - { url = "https://files.pythonhosted.org/packages/05/54/ba62ac2d1bc87e010aad23751e383b8794e45d931df67677313a2da78823/aiohttp-3.14.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:95f5217e76a046b9f228a101717ef8d42b1eb3d9d196d15202db5bf41df88936", size = 1899969, upload-time = "2026-06-01T19:40:26.406Z" }, - { url = "https://files.pythonhosted.org/packages/dc/82/7cc7907725d83a19f31551334061e1ab8e108b1d7ac52632a2a844a4acb5/aiohttp-3.14.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1a4a9f17e85b80878c176695c1998c790e83731d8271881e5d356488652a1f9e", size = 1991771, upload-time = "2026-06-01T19:40:29.061Z" }, - { url = "https://files.pythonhosted.org/packages/d0/1c/a57de71a4508c93a830b77c28af3d08cd97f606dedfc6b94275347744508/aiohttp-3.14.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:145262119b07d7f95abc1839add35ba2bfc84551d4b4660ca11542c0b215455b", size = 1868606, upload-time = "2026-06-01T19:40:31.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/ae/3839726cd49150a53ed340cc24ce5ba09d4c2117020ef9d45542bec5eb2f/aiohttp-3.14.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:49a33ded29b0b2fa7a367a02cf0fb89af602bb87542a16177ec8ce1c9c51d12a", size = 1665437, upload-time = "2026-06-01T19:40:35.01Z" }, - { url = "https://files.pythonhosted.org/packages/35/1e/c237923232c7da7f0392ea25d89fc5e60c0e93f685f4ebca8e7bcdd5271c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2cc736a9c9fc2bc4dd71fd404815741b6573df27c3f985948ec4076989ac57de", size = 1834090, upload-time = "2026-06-01T19:40:37.733Z" }, - { url = "https://files.pythonhosted.org/packages/98/02/a5a7a2524f92d3911761b405a7c067c751891942144adc13e2ad79611e39/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:b4141a3e5342ee3053a9cab54d25b64ed28289c1041e4c54b3d99839314d90ce", size = 1816907, upload-time = "2026-06-01T19:40:40.46Z" }, - { url = "https://files.pythonhosted.org/packages/fa/76/a8b9f0d09234d516af9f2d7dd715557f33b5da3b0b56ead41d1170e86e3c/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:e30871b2d58996cb81aac52d2b1d15ac05257131ef0f90f18c2115a380fbfe7c", size = 1840382, upload-time = "2026-06-01T19:40:43.48Z" }, - { url = "https://files.pythonhosted.org/packages/c9/8e/140e715a0a4bbc211979ea30ec8396ad2ed5bf90ab87d8058fc4668b1923/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:667b881d083ccae3900ea5a241e17e5007ca78844c53ed389bb63d48f729d9c7", size = 1659497, upload-time = "2026-06-01T19:40:46.265Z" }, - { url = "https://files.pythonhosted.org/packages/10/c7/7ba5de8af9650b9767b063c675427b8685f43fa7ce563673a7bc3af60f08/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:b584dfe615d151e9b8f0a8ecb3aee6147f2927ec5b95ba25fe621f5377510928", size = 1870829, upload-time = "2026-06-01T19:40:49.583Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bc/2aaab2f85cadb26ea59c091fa2b8e370d625154b5c14b478f1b489d07551/aiohttp-3.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6199707cc40e0e9cd39c36fbc97bec416c704e1d0ddce03412bb3b3e6a90ccd0", size = 1832281, upload-time = "2026-06-01T19:40:52.303Z" }, - { url = "https://files.pythonhosted.org/packages/39/98/31b9ad9fbc01f0075ee7221002df5fd2d10b647f451ca5f30edc802d9dd6/aiohttp-3.14.0-cp314-cp314t-win32.whl", hash = "sha256:a8d93334d4961c9d566b1f046c81dee475b7c21eb730728d38237bfa70d1c8e6", size = 490597, upload-time = "2026-06-01T19:40:54.937Z" }, - { url = "https://files.pythonhosted.org/packages/59/1f/299b21441c8de42ff70fddc7cfe65e92f810abcf740739a09b56f7835364/aiohttp-3.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d2ffe9b614f50f069068b3b52e73414e4107fc10b7efc939a76acff9251fdd2", size = 525789, upload-time = "2026-06-01T19:40:57.306Z" }, - { url = "https://files.pythonhosted.org/packages/70/11/7f83fcba9ee05d4c54d61b3f8104da0d43a59adac44dd28effc0c9a10422/aiohttp-3.14.0-cp314-cp314t-win_arm64.whl", hash = "sha256:7a3fc4358e65826c515350f199c210de747cf669998211b1ee6c2e46de364b24", size = 467399, upload-time = "2026-06-01T19:40:59.993Z" }, + { url = "https://files.pythonhosted.org/packages/26/dd/bf526e6f0a1120dd6f2df2e97bacfe4d358f13d17a0ff5847301a1375a51/aiohttp-3.14.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa00140699487bd435fde4342d85c94cb256b7cd3a5b9c3396c67f19922afda2", size = 765225, upload-time = "2026-06-07T21:06:07.957Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e1/a2872aa55495a70f61310d411541c6ee23812d9a884e000c716e1bc3edbf/aiohttp-3.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c1af67559445498b502030c35c59db59966f47041ca9de5b4e707f86bd10b5f", size = 518743, upload-time = "2026-06-07T21:06:09.749Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e7/c60c7b209e509cc787de3cea0550a518538cfc08003e1c1e14c1c63fff71/aiohttp-3.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d44ec478e713ee7f29b439f7eb8dc2b9d4079e11ae114d2c2ac3d5daf30516c8", size = 514139, upload-time = "2026-06-07T21:06:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/5b/8d/614ace2f579702c9840ab1e1447fd8509e35b0b904f7196418fa2f57b25d/aiohttp-3.14.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3b1a184a9a8f548a6b73f1e26b96b052193e4b3175ed7342aaf1151a1f00a04", size = 1784088, upload-time = "2026-06-07T21:06:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/726e90f99542bf292f81a96a12cc4847deb86f3ccf62c6f4014a201f4d33/aiohttp-3.14.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5f2504bc0322437c9a1ff6d3333ca56c7477b727c995f036b976ae17b98372c8", size = 1737835, upload-time = "2026-06-07T21:06:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/0b/4b/d176d5c4db9d33dacf0543102ea59503bc1d528af4cfd0b719949ca49389/aiohttp-3.14.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:73f05ea02013e02512c3bf42714f1208c57168c779cc6fe23516e4543089d0a6", size = 1842801, upload-time = "2026-06-07T21:06:16.228Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/5a99b563690ea0cbed912ae94a2ce33993a5709a651a3a4fe761e7dd973a/aiohttp-3.14.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:797457503c2d426bee06eef808d07b31ede30b65e054444e7de64cad0061b7af", size = 1929992, upload-time = "2026-06-07T21:06:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/76/7f/a987b14a3859094b3cea3f4825219c3e5536242564af6e3f9c2f6c994eb2/aiohttp-3.14.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b821a1f7dedf7e37450654e620038ac3b2e81e8fa6ea269337e97101978ec730", size = 1786989, upload-time = "2026-06-07T21:06:19.677Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1a/420e5c85a3e73349372ed22ce0b6af86bfa6ce16a4b20a64a2e94608c781/aiohttp-3.14.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4cd96b5ba05d67ed0cf00b5b405c8cd99586d8e3481e8ee0a831057591af7621", size = 1640129, upload-time = "2026-06-07T21:06:22.558Z" }, + { url = "https://files.pythonhosted.org/packages/a7/80/18a592ed3be0a402cc03670bd72ee1f8563ddbe1d8d5542dbf868f274136/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d459b98a932296c6f0e94f87511a0b1b90a8a02c30a50e60a297619cd5a58ee", size = 1756576, upload-time = "2026-06-07T21:06:24.8Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0b/8b3d5713373858ff71a617daf6e3b0e81ad63e79d09a3cf2f6b6b983939c/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:764457a7be60825fb770a644852ff717bcbb5042f189f2bd16df61a81b3f6573", size = 1754668, upload-time = "2026-06-07T21:06:26.528Z" }, + { url = "https://files.pythonhosted.org/packages/9f/49/fd564575cf225821d7ba5a117cb8bc27213d8a7e1811162afb43ae077039/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f7a16ef45b081454ef844502d87a848876c490c4cb5c650c230f6ec79ed2c1e7", size = 1817019, upload-time = "2026-06-07T21:06:28.297Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/e850c9ae6fc91356552ae668bb6c51e93fa29c8aef13398a10b56678557f/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:2fbc3ed048b3475b9f0cbcb9978e9d2d3511acd91ead203af26ed9f0056004cf", size = 1631638, upload-time = "2026-06-07T21:06:30.242Z" }, + { url = "https://files.pythonhosted.org/packages/eb/94/3c337ba72451a89806ace6f75bddc92bafc5b8d53d90115a512858024b63/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:bedb0cd073cc2dc035e30aeb99444389d3cd2113afe4ef9fcd23d439f5bade85", size = 1835660, upload-time = "2026-06-07T21:06:31.943Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9c/9c18cf367a0498212d9ba7daf990b504a5e8ae064cda4b504e2647c89c03/aiohttp-3.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6feea921016eb3d4e04d65fc4e9ca402d1a3801f562aef94989f54694917af3", size = 1775698, upload-time = "2026-06-07T21:06:33.72Z" }, + { url = "https://files.pythonhosted.org/packages/b5/63/a251a9d2a6cb45065b2ddc0bde2b3dd10108740a9a42f632c66405a761a2/aiohttp-3.14.1-cp311-cp311-win32.whl", hash = "sha256:313701e488100074ce99850404ee36e741abf6330179fec908a1944ecf570126", size = 458386, upload-time = "2026-06-07T21:06:35.279Z" }, + { url = "https://files.pythonhosted.org/packages/17/ca/69274c51dcd6e8947d77b2806cf47a4a15f2c846e2cbeb1882547d3da283/aiohttp-3.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:03ab4530fdcb3a543a122ba4b65ac9919da9fe9f78a03d328a6e38ff962f7aa5", size = 483406, upload-time = "2026-06-07T21:06:36.824Z" }, + { url = "https://files.pythonhosted.org/packages/2c/8a/c25904f77690c3688ec140f87591ef11a0cfe36bf3d5c0f1f38056fb62b3/aiohttp-3.14.1-cp311-cp311-win_arm64.whl", hash = "sha256:486f7d16ed54c39c2cbd7ca71fd8ba2b8bb7860df65bd7b6ed640bab96a38a8b", size = 452987, upload-time = "2026-06-07T21:06:38.371Z" }, + { url = "https://files.pythonhosted.org/packages/1d/21/151624b51cd92553d95424daf4bf19f19ce9be9002d19253e7e7ce67197b/aiohttp-3.14.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d35143e27778b4bb0fb189562d7f275bff79c62ab8e98459717c0ea617ff2480", size = 757402, upload-time = "2026-06-07T21:06:40.311Z" }, + { url = "https://files.pythonhosted.org/packages/c2/82/280619e0bd7bf2454987e19282616e84762255dd9c8468f62382e8c191f1/aiohttp-3.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bcfb80a2cc36fba2534e5e5b5264dc7ae6fcd9bf15256da3e53d2f499e6fa29d", size = 512310, upload-time = "2026-06-07T21:06:42.207Z" }, + { url = "https://files.pythonhosted.org/packages/55/b2/2aac325583aaa1353045f96dffa586d8a34e8322e14a7ba49cffeb103ab4/aiohttp-3.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:27fd7c91e51729b4f7e1577865fa6d34c9adccbc39aabe9000285b48af9f0ec2", size = 512448, upload-time = "2026-06-07T21:06:43.813Z" }, + { url = "https://files.pythonhosted.org/packages/8a/72/a60607cb849faa8af8a356c9329ea2eb6f395d49e82cc82ccba1fd8deb8f/aiohttp-3.14.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64c567bf9eaf664280116a8688f63016e6b32db2505908e2bdaca1b6438142f2", size = 1766854, upload-time = "2026-06-07T21:06:45.391Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d3/d9fe1c9ec7557ab4d0d82bebaa728c6418f0b93295ec2f4ab015f7710cc7/aiohttp-3.14.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f5e6ff2bdbb8f4cd3fbe41f99e25bbcd58e3bf9f13d3dd31a11e7917251cc77a", size = 1740884, upload-time = "2026-06-07T21:06:47.413Z" }, + { url = "https://files.pythonhosted.org/packages/c1/dc/f2cecfaf9337ba3e63f181500814ff502aa3d00d9c7ec93a9d23d10a27b2/aiohttp-3.14.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2f73e01dc37122325caf079982621262f96d74823c179038a82fddfc50359264", size = 1810034, upload-time = "2026-06-07T21:06:50.165Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/2ff65c5e65c0d7476daf7e15c032e0805e36811185b9623e3238ad6c763e/aiohttp-3.14.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bb2c0c80d431c0d03f2c7dbf125150fedd4f0de17366a7ca33f7ccb822391842", size = 1904054, upload-time = "2026-06-07T21:06:52.035Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/d445818389df371f56d141d881153ba23183c4735a03f7356ffb43f7757d/aiohttp-3.14.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3e6fc1a85fa7194a1a7d19f44e8609180f4a8eb5fa4c7ed8b4355f080fad235c", size = 1790278, upload-time = "2026-06-07T21:06:54.049Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/bf04cb4d865fc6101c2229a294ad744973b72e513fdc5a6b791e6983d72a/aiohttp-3.14.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:686b6c0d3911ec387b444ddf5dc62fb7f7c0a7d5186a7861626496a5ab4aff95", size = 1591795, upload-time = "2026-06-07T21:06:55.911Z" }, + { url = "https://files.pythonhosted.org/packages/dc/b4/4dac0038960427ba832f6609dfb4ea5437d7fd80c72001b9e48f834f428b/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c6fa4dc7ad6f8109c70bb1499e589f76b0b792baf39f9b017eb92c8a81d0a199", size = 1728397, upload-time = "2026-06-07T21:06:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f9/7cd4e8ad7aa3b75f17d56bb5498dd604a93d4e6eece822ba0568c413fff0/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:87a5eea1b2a5e21e1ebdbb33ad4165359189327e63fc4e4894693e7f821ac817", size = 1766504, upload-time = "2026-06-07T21:07:00.009Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/fc01d9fcad0f73fed3f3d361f1f94f975947b50dff82919f6dc2bf4316cc/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c1421eb01d4fd608d88cc8290211d177a58532b55ad94076fb349c5bf467f0a", size = 1777806, upload-time = "2026-06-07T21:07:02.064Z" }, + { url = "https://files.pythonhosted.org/packages/41/09/47e2d090bddcc8fb4ccb4c314aadc32d7c5d9bb55f50f6ad1c92fc15d501/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:34b257ec41345c1e8f2df68fa908a7952f5de932723871eb633ecbbff396c9a4", size = 1580707, upload-time = "2026-06-07T21:07:03.942Z" }, + { url = "https://files.pythonhosted.org/packages/3d/36/f1a4ce904ae0b6930cfe9afc96d0896f7ec1a620c400405d63783bb95a9c/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:de538791a80e5d862addbc183f70f0158ac9b9bb872bb147f1fd2a683691e087", size = 1798121, upload-time = "2026-06-07T21:07:05.987Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/e0075ce9ca0279ee1d4f0c0b85f54fea02ebc83c3007651a72bece658fec/aiohttp-3.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6f71173be42d3241d428f760122febb748de0623f44308a6f120d0dd9ec572e3", size = 1767580, upload-time = "2026-06-07T21:07:07.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/a0c0a8f327a9c52095cdd8e312391b00d3ed64ab6c72bb5c33d8ec251cf7/aiohttp-3.14.1-cp312-cp312-win32.whl", hash = "sha256:ec8dc383ee57ea3e883477dcca3f11b65d58199f1080acaf4cd6ad9a99698be4", size = 452771, upload-time = "2026-06-07T21:07:09.669Z" }, + { url = "https://files.pythonhosted.org/packages/df/d9/ea367c75f16ac9c6cdc8febb25e8318fa21a2b1bc8d6514d4b2d890bface/aiohttp-3.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:2aa92c87868cd13674989f9ee83e5f9f7ea4237589b728048e1f0c8f6caa3271", size = 479873, upload-time = "2026-06-07T21:07:11.538Z" }, + { url = "https://files.pythonhosted.org/packages/03/64/8d96784a7851156db8a4c6c3f6f91042fdf39fb15a4cc38c8b3c14833c45/aiohttp-3.14.1-cp312-cp312-win_arm64.whl", hash = "sha256:2c840c90759922cb5e6dda94596e079a30fb5a5ba548e7e0dc00574703940847", size = 448073, upload-time = "2026-06-07T21:07:13.637Z" }, + { url = "https://files.pythonhosted.org/packages/bc/97/bd137012dd97e1649162b099135a80e1fd59aaa807b2430fc448d1029aff/aiohttp-3.14.1-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:b3a03285a7f9c7b016324574a6d92a1c895da6b978cb8f1deee3ac72bc6da178", size = 506882, upload-time = "2026-06-07T21:07:15.501Z" }, + { url = "https://files.pythonhosted.org/packages/ef/79/e5cc690e9d922a66887ceeaca53a8ffd5a7b0be3816142b7abc433742d89/aiohttp-3.14.1-cp313-cp313-android_21_x86_64.whl", hash = "sha256:2a73f487ab8ef5abbb24b7aa9b73e98eaba9e9e031804ff2416f02eca315ccaf", size = 515270, upload-time = "2026-06-07T21:07:17.53Z" }, + { url = "https://files.pythonhosted.org/packages/fe/22/a73ccbf9dbd6e26dda0b24d5fd5db7da92ee3383a79f47677ffb834c5c5b/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:915fbb7b41b115192259f8c9ae58f3ddc444d2b5579917270211858e606a4afd", size = 485841, upload-time = "2026-06-07T21:07:19.555Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b9/57ed8eaf596321c2ad747bd480fb1700dbd7177c60dfc9e4c187f629662e/aiohttp-3.14.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:7fb4bdf95b0561a79f259f9d28fbc109728c5ee7f27aff6391f0ca703a329abe", size = 492088, upload-time = "2026-06-07T21:07:21.581Z" }, + { url = "https://files.pythonhosted.org/packages/78/c0/5ebe5270a7c140d7c6f79dcb018640225f14d406c149e4eec04a7d82fe71/aiohttp-3.14.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:1b9748363260121d2927704f5d4fc498150669ca3ae93625986ee89c8f80dcd4", size = 501564, upload-time = "2026-06-07T21:07:23.388Z" }, + { url = "https://files.pythonhosted.org/packages/75/7f/8cdaa24fc7983865e0915153b96a9ac5bcdd3548d64c5a27d17cecccad2d/aiohttp-3.14.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:86a6dab78b0e43e2897a3bbe15745aa60dc5423ca437b7b0b164c069bf91b876", size = 751998, upload-time = "2026-06-07T21:07:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f4/c4227aacfacc5cb0cc2d119b65301d177912a6842cd64e120c47af76064f/aiohttp-3.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4dfd6e47d3c44c2279907607f73a4240b88c69eb8b90da7e2441a8045dfd21da", size = 510918, upload-time = "2026-06-07T21:07:27.28Z" }, + { url = "https://files.pythonhosted.org/packages/ab/01/a2d5f96cd4e74424864d30bc0a7e44d0a12dacdcfa91b5b2d1bd3dca6bf3/aiohttp-3.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:317acd9f8602858dc7d59679812c376c7f0b97bcbbf16e0d6237f54141d8a8a6", size = 508657, upload-time = "2026-06-07T21:07:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ed/3c0fb5c500fdd8e7ebc10d1889c04384fffa1a9163eac1356088ca9da1b1/aiohttp-3.14.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd869c427324e5cb15195793de951295710db28be7d818247f3097b4ab5d4b96", size = 1757907, upload-time = "2026-06-07T21:07:31.03Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ab/d4c924d9bd5be3050c226612413ce68cb54c70d2c31b661bfc8d9a5b6a70/aiohttp-3.14.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:93b032b5ec3255473c143627d21a69ac74ae12f7f33974cb587c564d11b1066f", size = 1737565, upload-time = "2026-06-07T21:07:33.031Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/37326821ff779084020cdc33224d20b19f42f4183a500ff92022a739eda7/aiohttp-3.14.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f234b4deb12f3ad59127e037bc57c40c21e45b45282df7d3a55a0f409f595296", size = 1799018, upload-time = "2026-06-07T21:07:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/6e947ba73e4ce09070761c05ed3a8ceb7c21f5e46798671d8b2aac0e4626/aiohttp-3.14.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9af6779bfb46abf124068327abcdf9ce95c9ef8287a3e8da76ccf2d0f16c28fa", size = 1894416, upload-time = "2026-06-07T21:07:36.956Z" }, + { url = "https://files.pythonhosted.org/packages/9d/6e/dbf1d0625dc711fb2851f4f3c3055c39ed58bae92082d8c627dbe6013736/aiohttp-3.14.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:faccab372e66bc76d5731525e7f1143c922271725b9d38c9f97edcc66266b451", size = 1783881, upload-time = "2026-06-07T21:07:39.063Z" }, + { url = "https://files.pythonhosted.org/packages/44/c2/5e25098a67268ed369483ae7d1a58bd0a13d03aab860d2a0e4a6eb25b046/aiohttp-3.14.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f380468b09d2a81633ee863b0ec5648d364bd17bb8ecfb8c2f387f7ac1faf42c", size = 1587572, upload-time = "2026-06-07T21:07:41.058Z" }, + { url = "https://files.pythonhosted.org/packages/2a/bd/cf9cee17e140f942a3de73e658a543aa8fbf35a5fc67a9d2538d52d77f0b/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:97e704dcd26271f5bda3fa07c3ce0fb76d6d3f8659f4baa1a24442cc9ba177ca", size = 1722137, upload-time = "2026-06-07T21:07:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5684f8c59045c96f81a18cefbc1fbbd79d25b88f1c622f2a5c5c08fcb632/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:269b76ac5394092b95bc4a098f4fc6c191c083c3bd12775d1e30e663132f6a09", size = 1755953, upload-time = "2026-06-07T21:07:45.933Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/35caf3170f8359760740a7d9aa0fff2e344bef98e1d1186f5a0f6dec17e6/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0b3e614340c889d575451696374c9d17affd54cd607ca0babed8f8c37b9397", size = 1766479, upload-time = "2026-06-07T21:07:48.047Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a1/b0c61e7a137f0d81de49a82023a6df73c3c16d6fefb0f8e4a93d21639002/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:5663ee9257cfa1add7253a7da3035a02f31b6600ec48261585e1800a81533080", size = 1580077, upload-time = "2026-06-07T21:07:50.069Z" }, + { url = "https://files.pythonhosted.org/packages/0b/41/194ea4623693009fcefebef7aef63c141754f153e9cd0d39d3b9e36c175c/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:603a2c834142172ffddc054067f5ec0ca65d57a0aa98a71bc81952573208e345", size = 1791688, upload-time = "2026-06-07T21:07:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/ba/45/4de841f005cfe1fd63e2a2fe011262c515e2a62aa6994b15947e7d717ac9/aiohttp-3.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb21957bb8aca671c1765e32f58164cf0c50e6bf41c0bbbd16da20732ecaf588", size = 1761094, upload-time = "2026-06-07T21:07:54.113Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ae/dbce10533d3896d544d5053939ed75b7dc31a1b0973d959b1b5ae21028d6/aiohttp-3.14.1-cp313-cp313-win32.whl", hash = "sha256:e509a55f681e6158c20f70f102f9cf61fb20fbc382272bc6d94b7343f2582780", size = 452662, upload-time = "2026-06-07T21:07:56.06Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d9/0bf1a19362c32f06229da5e7ddfcec91f93474d6307f7a2d3135e9c674dc/aiohttp-3.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:1ac8531b638959718e18c2207fbfe297819875da46a740b29dfa29beba64355a", size = 479748, upload-time = "2026-06-07T21:07:58.319Z" }, + { url = "https://files.pythonhosted.org/packages/22/0a/62e7232dc9484fbec112ceb32efb6a624cc7994ec6e2b019286f17c4e8f2/aiohttp-3.14.1-cp313-cp313-win_arm64.whl", hash = "sha256:250d14af67f6b6a1a4a811049b1afa69d61d617fca6bf33149b3ab1a6dbcf7b8", size = 447723, upload-time = "2026-06-07T21:08:00.154Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a1/5fafa04e1ca91ddb47608699d60649c1c6db3cf41c99e78fc4056f9513db/aiohttp-3.14.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:7c106c26852ca1c2047c6b80384f17100b4e439af276f21ef3d4e2f450ae7e15", size = 508531, upload-time = "2026-06-07T21:08:02.093Z" }, + { url = "https://files.pythonhosted.org/packages/fa/2e/bfa02f699d87ffc86d5959270b28f1cb410add3ccaced8ed2e0b8a5238fc/aiohttp-3.14.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:20205f7f5ade7aaec9f4b500549bbc071b046453aed72f9c06dcab87896a83e8", size = 514718, upload-time = "2026-06-07T21:08:04.476Z" }, + { url = "https://files.pythonhosted.org/packages/85/a5/9594ad6289eebbc97d167c44213d557807f90e59115caad24de21ad2c3b1/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:62a759436b29e677181a9e76bab8b8f689a29cb9c535f45f7c48c9c830d3f8c3", size = 487918, upload-time = "2026-06-07T21:08:06.377Z" }, + { url = "https://files.pythonhosted.org/packages/b4/61/16a32c36c3c49edec122a3dc811f2057df2f94d3b14aa107c8017d981618/aiohttp-3.14.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2964cbf553df4d7a57348da44d961d871895fc1ee4e8c322b2a95612c7b17fba", size = 494014, upload-time = "2026-06-07T21:08:08.263Z" }, + { url = "https://files.pythonhosted.org/packages/9b/89/3ebcf96ed99c05bec9c434aaac6963fd3cbab4a786ae739908a144d9ce44/aiohttp-3.14.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:237651caadc3a59badd39319c54642b5299e9cc98a3a194310e55d5bb9f5e397", size = 502398, upload-time = "2026-06-07T21:08:10.244Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3d/b74870a0c2d40c355928cd5b96c7a11fa821b8a40fc41365e64479b151fb/aiohttp-3.14.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:896e12dfdbbab9d8f7e16d2b28c6769a60126fa92095d1ebf9473d02593a2448", size = 758018, upload-time = "2026-06-07T21:08:12.447Z" }, + { url = "https://files.pythonhosted.org/packages/d3/66/f42f5c984d99e49c6cff5f26f590750f2e2f7ef1fcfb99966ab5be1b632e/aiohttp-3.14.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d03f281ed22579314ba00821ce20115a7c0ac430660b4cc05704a3f818b3e004", size = 512462, upload-time = "2026-06-07T21:08:14.624Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/248e1aebe0c7810b0271e021a0f2a5eb6e78a051885b3c9df49f42a5802d/aiohttp-3.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07eabb979d236335fed927e137a928c9adfb7df3b9ec7aa31726f133a62be983", size = 512824, upload-time = "2026-06-07T21:08:16.572Z" }, + { url = "https://files.pythonhosted.org/packages/26/97/2aa0e5ba0727dc3bd5aaebb7ccbc510f7dfb7fb961ec87497cd496635ab1/aiohttp-3.14.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4fe1f1087cbadb280b5e1bb054a4f00d1423c74d6626c5e48400d871d34ecefe", size = 1749898, upload-time = "2026-06-07T21:08:18.635Z" }, + { url = "https://files.pythonhosted.org/packages/00/8d/e97f6c96c891d457c8479d92a514ba194d0412f981d72c70341ee18488ed/aiohttp-3.14.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:367a9314fdc79dab0fac96e216cb41dd73c85bdca85306ce8999118ba7e0f333", size = 1710114, upload-time = "2026-06-07T21:08:20.892Z" }, + { url = "https://files.pythonhosted.org/packages/6f/e6/aa8d7e863048c8fceb5cd6ce74017311cec3ead07847387e12265fb4444e/aiohttp-3.14.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a24f677ebe83749039e7bdf862ff0bbb16818ae4193d4ef96505e269375bcce0", size = 1802541, upload-time = "2026-06-07T21:08:23.044Z" }, + { url = "https://files.pythonhosted.org/packages/83/a8/72193137de57fda4ebfae4563182d082c8856e3b6e9871d0b46f028fb369/aiohttp-3.14.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c83afe0ba876be7e943d2e0ba645809ad441575d2840c895c21ee5de93b9377a", size = 1875776, upload-time = "2026-06-07T21:08:25.288Z" }, + { url = "https://files.pythonhosted.org/packages/a0/18/938441025db6769a3464596b2410af3afde0b21eb2f204c6f766f68af4bd/aiohttp-3.14.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:634e385930fb6d2d479cf3aa66515955863b77a5e3c2b5894ca259a25b308602", size = 1760329, upload-time = "2026-06-07T21:08:27.363Z" }, + { url = "https://files.pythonhosted.org/packages/60/29/bf2496b4065e76e09fe48015aaffe5ce161d8f089b06ac6982070f653076/aiohttp-3.14.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeea07c4397bbc57719c4eed8f9c284874d4f175f9b6d57f7a1546b976d455ca", size = 1587293, upload-time = "2026-06-07T21:08:29.805Z" }, + { url = "https://files.pythonhosted.org/packages/49/a2/2136674d52123b1354bd05dd5753c318db47dc0c927cc70b27bab3755456/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:335c0cc3e3545ce98dcb9cfcb836f40c3411f43fa03dab757597d80c89af8a35", size = 1714756, upload-time = "2026-06-07T21:08:32.094Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/e5fd2e6f915503081c0f9b1e8540947037929c70c191da2e4d54b31a21a1/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ae6be797afdef264e8a84864a85b196ca06045586481b3df8a967322fd2fa844", size = 1721052, upload-time = "2026-06-07T21:08:34.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/5a/2833e324a2263e104e31e2e91bc5bbee81bc499afd32203faee048a883f0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:8560b4d712474335d08907db7973f71912d3a9a8f1dee992ec06b5d2fe359496", size = 1766888, upload-time = "2026-06-07T21:08:36.95Z" }, + { url = "https://files.pythonhosted.org/packages/57/fa/dea6511870913162f3b2e8c42a7614eb203a4540b8c2da43e0bfb0548f3c/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7edd08e0a5deb1e8564a2fcd8f4561014a3f05252334671bbf55ddd47db0e5", size = 1581679, upload-time = "2026-06-07T21:08:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/14/bd/3cf0d55e71784b33534e9710a67d382d900598b4787fbce6cc7317f8c42a/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:b6ff7fcee63287ae57b5df3e4f5957ce032122802509246dec1a5bcc55904c95", size = 1782021, upload-time = "2026-06-07T21:08:41.407Z" }, + { url = "https://files.pythonhosted.org/packages/c1/af/14bb5843eccbe234f4dfb78ab73e549d99727247e62ae5d62cbd22eaf5b0/aiohttp-3.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ffbb2f4ec1ceaff7e07d43922954da26b223d188bf30658e561b98e23089444", size = 1742574, upload-time = "2026-06-07T21:08:43.795Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1e/fbeb7af9210a67ac0f9c9bec0f8f4568497924e33137a3d5b48e1cf85f3f/aiohttp-3.14.1-cp314-cp314-win32.whl", hash = "sha256:a9875b46d910cff3ea2f5962f9d266b465459fe634e22556ab9bd6fc1192eea0", size = 457773, upload-time = "2026-06-07T21:08:46.168Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/13e8d741a9ec5db7d900c060554cf8352ab85e44e2a4469ebb9d377bda17/aiohttp-3.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:af8b4b81a960eeaf1234971ac3cd0ba5901f3cd42eae42a46b4d089a8b492719", size = 485001, upload-time = "2026-06-07T21:08:48.401Z" }, + { url = "https://files.pythonhosted.org/packages/df/30/491acfa2c4d6c3ff59c49a14fc1b50be3241e25bbb0c84c09e2da4d11395/aiohttp-3.14.1-cp314-cp314-win_arm64.whl", hash = "sha256:cf4491381b1b57425c315a56a439251b1bdac07b2275f19a8c44bc57744532ec", size = 453809, upload-time = "2026-06-07T21:08:50.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/19dbe1a1f4cc6230eb9e314de7fe68053b0992f9302b27d12141a0b5db53/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:819c054312f1af92947e6a55883d1b66feefab11531a7fc45e0fb9b63880b5c2", size = 793320, upload-time = "2026-06-07T21:08:52.775Z" }, + { url = "https://files.pythonhosted.org/packages/7f/20/1b7182219ba1b108430d6e4dc53d25ae02dcfcf5a045b33af4e8c5167527/aiohttp-3.14.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10ee9c1753a8f706345b22496c79fbddb5be0599e0823f3738b1534058e25340", size = 529077, upload-time = "2026-06-07T21:08:55Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c8/14ce60ec31a2e5f5274bb17d383a6f7a3aabca31ac04eee05585bbadab16/aiohttp-3.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1601cc37baf5750ccacae618ec2daf020769581695550e3b654a911f859c563d", size = 532476, upload-time = "2026-06-07T21:08:57.176Z" }, + { url = "https://files.pythonhosted.org/packages/7e/02/9ac85e081e53da2e061b02fa7758fe0a12d17b8ce2d1f5e6c7cb76730328/aiohttp-3.14.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d6e0ac9da31c9c04c84e1c0182ad8d6df35965a85cae29cd71d089621b3ae94", size = 1922347, upload-time = "2026-06-07T21:08:59.563Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3e/d3ba07a0ab38b5389e10bec4362d21e10a4f667cba2d79ba30837b3a5059/aiohttp-3.14.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9e8f2d660c350b3d0e259c7a7e3d9b7fc8b41210cbcc3d4a7076ff0a5e5c2fdc", size = 1786465, upload-time = "2026-06-07T21:09:01.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/e2ee978a00cfb2df829704a69528b18154eba5939f45bc1efa8f33aee4c5/aiohttp-3.14.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4691802dda97be727f79d86818acaad7eb8e9252626a1d6b519fedbb92d5e251", size = 1909423, upload-time = "2026-06-07T21:09:04.357Z" }, + { url = "https://files.pythonhosted.org/packages/73/5d/1430334858b1022b58ae50399a918f0bd6fe8fa7fa183598d657ff61e040/aiohttp-3.14.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c389c482a7e9b9dc3ee2701ac46c4125297a3818875b9c305ddb603c04828fd1", size = 2001906, upload-time = "2026-06-07T21:09:06.722Z" }, + { url = "https://files.pythonhosted.org/packages/66/4e/560c7472d3d198a23aa5c8b19a5115bf6a9b77b7d3e4bb363da320430ad2/aiohttp-3.14.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fc0cacab7ba4e56f0f81c82a98c09bed2f39c940107b03a34b168bdf7597edd3", size = 1877095, upload-time = "2026-06-07T21:09:09.011Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f1/4745806578d447db4a784a8591e2dae3afdfc2bcb96f8f81271b13df6543/aiohttp-3.14.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:979ed4717f59b8bb12e3963378fa285d93d367e15bcd66c721311826d3c44a6c", size = 1676222, upload-time = "2026-06-07T21:09:11.461Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/48255813cca749a229ef0ab476004ec623728ad79a9c0840616f6c076325/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38e1e7daaea81df51c952e18483f323d878499a1e2bfe564790e0f9701d6f203", size = 1842922, upload-time = "2026-06-07T21:09:14.118Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c0/bbd054e2bee909f529523a5af3891052606af5143c09f5f183ec3b234676/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:4132e72c608fe9fecb8f409113567605915b83e9bdd3ea56538d2f9cd35002f1", size = 1825035, upload-time = "2026-06-07T21:09:16.447Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ae/90395d4376deceb74e09ec26b6adf7d2015a6f8802d6d84446af860fef04/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:eefd9cc9b6d4a2db5f00a26bc3e4f9acf71926a6ec557cd56c9c6f27c290b665", size = 1849512, upload-time = "2026-06-07T21:09:18.742Z" }, + { url = "https://files.pythonhosted.org/packages/93/bd/fb25f3049957553d4ce0ba6ae480aa2f592a6985497fca590837d16c1be0/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b165790117eea512d7f3fb22f1f6dad3d55a7189571993eb015591c1401276d1", size = 1668571, upload-time = "2026-06-07T21:09:21.458Z" }, + { url = "https://files.pythonhosted.org/packages/3f/22/7f73303d64dd567ff3addca90b556690ed1233a47b8f55d242fb90af3681/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ed09c7eb1c391271c2ed0314a51903e72a3acb653d5ccfc264cdf3ef11f8269d", size = 1881159, upload-time = "2026-06-07T21:09:23.813Z" }, + { url = "https://files.pythonhosted.org/packages/44/be/0474c5a8b5640e1e4aa1923430a91f4151be82e511373fe764189b89aef5/aiohttp-3.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:99abd37084b82f5830c635fddd0b4993b9742a66eb746dacf433c8590e8f9e3c", size = 1841409, upload-time = "2026-06-07T21:09:26.207Z" }, + { url = "https://files.pythonhosted.org/packages/7b/3c/bb4a7cba26956cb3da4553cc2056cf67be5b5ff6e6d8fa4fbdff73bfb7ae/aiohttp-3.14.1-cp314-cp314t-win32.whl", hash = "sha256:47ddf841cdecc810749921d25606dee45857d12d2ad5ddb7b5bd7eab12e4b365", size = 494166, upload-time = "2026-06-07T21:09:28.505Z" }, + { url = "https://files.pythonhosted.org/packages/8a/84/ec80c2c1f66a952555a9f86df6b33af65108a6febfa0471b69013a12f807/aiohttp-3.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e78b522b7a6e27e0b25d19b247b75039ac4c94f99823e3c9e53ae1603a9f7e9", size = 530255, upload-time = "2026-06-07T21:09:30.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/71/6e22be134a4061ada85a92951b842f2657f17d926b727f3f94c56ae963d6/aiohttp-3.14.1-cp314-cp314t-win_arm64.whl", hash = "sha256:90d53f1609c29ccc2193945ef732428382a28f78d0456ae4d3daf0d48b74f0f6", size = 469640, upload-time = "2026-06-07T21:09:33.028Z" }, ] [[package]] @@ -217,6 +217,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "click" +version = "8.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -537,7 +549,7 @@ wheels = [ [[package]] name = "meraki" -version = "3.2.0" +version = "3.3.0" source = { editable = "." } dependencies = [ { name = "aiohttp" }, @@ -553,6 +565,7 @@ dev = [ { name = "pytest-cov" }, { name = "responses" }, { name = "ruff" }, + { name = "towncrier" }, ] generator = [ { name = "jinja2" }, @@ -573,6 +586,7 @@ dev = [ { name = "pytest-cov", specifier = ">=7.1.0,<8" }, { name = "responses", specifier = ">=0.25,<1" }, { name = "ruff", specifier = ">=0.15.12" }, + { name = "towncrier", specifier = ">=24.8.0" }, ] generator = [{ name = "jinja2", specifier = "==3.1.6" }] @@ -807,7 +821,7 @@ wheels = [ [[package]] name = "pytest" -version = "9.0.3" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -816,9 +830,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -947,27 +961,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.15" +version = "0.15.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/84/6f/a76f7d96e5c962f5b69cee865e49c15c1116897c01990faa8a57edb62e7f/ruff-0.15.15.tar.gz", hash = "sha256:b8dff018130b46d8e5bf0f926ef6b60cf871d6d5ae45fc9334e09632daa741d6", size = 4706985, upload-time = "2026-05-28T14:16:57.784Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/dc/35b341fc554ba02f217fc10da57d1a75168cfbcf75b0ef2202176d4c4f2d/ruff-0.15.20.tar.gz", hash = "sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566", size = 4755489, upload-time = "2026-06-25T17:20:37.578Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fa/9d/3a45c05b8ab04b4705989de70a79008e27c8003296a0feaee9edc18dd7e9/ruff-0.15.15-py3-none-linux_armv6l.whl", hash = "sha256:cf93e5388f412e1b108b1f8b34a6e036b70fe8aff89393befad96fe48670311b", size = 10710652, upload-time = "2026-05-28T14:16:06.701Z" }, - { url = "https://files.pythonhosted.org/packages/05/66/da974431624bf3b49f6ee1f9543c02d929ff1cba78b0d5a79c38cf21f744/ruff-0.15.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ac5a646d1f6a7dadd5d50842dae2c1f9862ac887ef5d1b1375e02def791fde6e", size = 11096615, upload-time = "2026-05-28T14:16:23.313Z" }, - { url = "https://files.pythonhosted.org/packages/8c/09/7443452e5d290230a712103f2fdceeef7184f3ec99a2bd01c8be78aaceb5/ruff-0.15.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:77d955a431430c66f72dd94e379ad38a16daea3d25094872ac4edf9e797be530", size = 10436683, upload-time = "2026-05-28T14:16:40.974Z" }, - { url = "https://files.pythonhosted.org/packages/53/01/d330c26a57fa4f3943a14424904027428315b700fe4d14a84bb123a649e5/ruff-0.15.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7614ee79c69788cf6cedd568069ade9cecc22a1ad20494efe8d0c9ebb4b622d4", size = 10769064, upload-time = "2026-05-28T14:16:28.905Z" }, - { url = "https://files.pythonhosted.org/packages/1d/85/cc8770f8bdff541b1da8392d1634141fe4a0e3f4ee596605959b7906c27f/ruff-0.15.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3cdb1679e06a1f6b47bc384714ae96f6e2fb65ca441eb78c43d2ca554176ce1f", size = 10511987, upload-time = "2026-05-28T14:16:43.732Z" }, - { url = "https://files.pythonhosted.org/packages/7c/29/8c190c1472b63013583ba391f3342036e02010544c1270455ed8e519bdf3/ruff-0.15.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2728b93d7b23a603ea2c0ac6eb73d760bd38ec9de35f35fb41e18f7a3fee7622", size = 11275100, upload-time = "2026-05-28T14:16:55.244Z" }, - { url = "https://files.pythonhosted.org/packages/9f/6b/7e145ce2cc8e63d6834eca03d83a0e18d121def5c69f91b4cf4011ed4879/ruff-0.15.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:be582fcc0db438902c7792b08d6ddf6c9b9e21addaa10092c2c741cfb09e5a45", size = 12176903, upload-time = "2026-05-28T14:16:14.368Z" }, - { url = "https://files.pythonhosted.org/packages/80/a3/d5974637f68e451f7fadf015cf3101d1cd7d8ba5027cffe0b9e3826ebe6b/ruff-0.15.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7aa77465b8ecaf1a27bea098d696f7fed5e1eccbd10b321b682d6de586ae5627", size = 11404550, upload-time = "2026-05-28T14:16:20.138Z" }, - { url = "https://files.pythonhosted.org/packages/fe/1c/e6e5e568f22be4fb05d6244234aba384c06b451252453b821e1a529263cf/ruff-0.15.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48decfa11d740de4889de623be1463308346312f2409a56e24aa280c86162dc4", size = 11382027, upload-time = "2026-05-28T14:16:46.615Z" }, - { url = "https://files.pythonhosted.org/packages/1d/01/170921b49fcd2e8858825593f91cf7146c3e40a5c3e6df763e4bb0484dde/ruff-0.15.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a5015088452ca0081387063649ec67f06d3d1d6b8b936a1f836b5e9657ecd48c", size = 11366041, upload-time = "2026-05-28T14:16:26.247Z" }, - { url = "https://files.pythonhosted.org/packages/87/54/a7bad711d7de93254e15e06a4c375b89a03d18de45d3e5dcc86a4472fb1a/ruff-0.15.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5294aab6356c81600fcdea3a62bb1b924dfd5e91767c12318d3f68f86af57cd", size = 10741795, upload-time = "2026-05-28T14:16:17.11Z" }, - { url = "https://files.pythonhosted.org/packages/c9/31/38c075963668f8b41c6914ee0f6f318727fbe30ab9145cb29e6df464c5fa/ruff-0.15.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:db5bd4d802415cca656dc1616070b725952d6ae95eb5d4831e49fbd94a38f75f", size = 10511117, upload-time = "2026-05-28T14:16:31.767Z" }, - { url = "https://files.pythonhosted.org/packages/9d/96/6ff689e1f7e375d1d97075eca022f74c2bab59554a432fe4d2e6f091986a/ruff-0.15.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:587a6278ed42059191c1a466e490bd7930fb50bd2e255398bc29616c895a61cb", size = 10994867, upload-time = "2026-05-28T14:16:35.149Z" }, - { url = "https://files.pythonhosted.org/packages/c3/c2/5dce0ab9f92a8d534fa62b9bf9caca3eddb8c1a81b616f5e195ada4f0d6e/ruff-0.15.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:df0c1c084f5f4be9812f61518a45c440d3c30d69ce4bf6c5270e66d38338f02a", size = 11482101, upload-time = "2026-05-28T14:16:49.598Z" }, - { url = "https://files.pythonhosted.org/packages/b1/c0/1003b60edd697c649faf61f1a34094b1abb38fb3d1181e3f895781250a08/ruff-0.15.15-py3-none-win32.whl", hash = "sha256:29428ea79694afbe756d45fd59b36f22b6b020dc0443cf7de0173046236964b9", size = 10716774, upload-time = "2026-05-28T14:16:52.337Z" }, - { url = "https://files.pythonhosted.org/packages/02/a8/1269eddd6945a06c23f055ef7848886e37cf9d6a8bebb386a3115f01470c/ruff-0.15.15-py3-none-win_amd64.whl", hash = "sha256:8df0323902e15e24bc4bf246da830573d3cf3352bd0b9a164eab335d111ff4a4", size = 11868463, upload-time = "2026-05-28T14:16:11.333Z" }, - { url = "https://files.pythonhosted.org/packages/4e/b2/920464c907b191e37469d477a1aa8bc048b8f36c4c1610dfa4ab87b39e18/ruff-0.15.15-py3-none-win_arm64.whl", hash = "sha256:3c8ceca6792f38196b8f589bc92eccd03eef286602da92e5dc05cc42ef6441b7", size = 11138498, upload-time = "2026-05-28T14:16:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/94/d9/2d5014f0253ba541d2061d9fa7193f48e941c8b21bb88a7ff9bbe0bd0596/ruff-0.15.20-py3-none-linux_armv6l.whl", hash = "sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078", size = 10839665, upload-time = "2026-06-25T17:19:44.702Z" }, + { url = "https://files.pythonhosted.org/packages/c6/d3/ac1798ba64f670698867fcfc591d50e7e421bef137db564858f619a30fcf/ruff-0.15.20-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b", size = 11208649, upload-time = "2026-06-25T17:19:48.787Z" }, + { url = "https://files.pythonhosted.org/packages/47/47/d3ac899991202095dfcf3d5176be4272642be3cf981a2f1a30f72a2afb95/ruff-0.15.20-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632", size = 10622638, upload-time = "2026-06-25T17:19:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/33/13/4e043fe30aa94d4ff5213a9881fc296d12960f5971b234a5263fdc225312/ruff-0.15.20-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd", size = 10984227, upload-time = "2026-06-25T17:19:54.044Z" }, + { url = "https://files.pythonhosted.org/packages/76/e6/92e7bf40388bc5800073b96564f56264f7e48bfd1a498f5ced6ae6d5a769/ruff-0.15.20-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b", size = 10622882, upload-time = "2026-06-25T17:19:57.037Z" }, + { url = "https://files.pythonhosted.org/packages/13/7a/43460be3f24495a3aa46d4b16873e2c4941b3b5f0b00cf88c03b7b94b339/ruff-0.15.20-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267", size = 11474808, upload-time = "2026-06-25T17:20:00.357Z" }, + { url = "https://files.pythonhosted.org/packages/27/a0/f37077884873221c6b33b4ab49eb18f9f88e54a16a25a5bca59bef46dd66/ruff-0.15.20-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c", size = 12293094, upload-time = "2026-06-25T17:20:03.446Z" }, + { url = "https://files.pythonhosted.org/packages/a6/74/165545b60256a9704c21ac0ec4a0d07933b320812f9584836c9f4aca4292/ruff-0.15.20-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae", size = 11526176, upload-time = "2026-06-25T17:20:06.301Z" }, + { url = "https://files.pythonhosted.org/packages/86/b1/a976a136d40ade83ce743578399865f57001003a409acadc0ecbb3051082/ruff-0.15.20-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b", size = 11520767, upload-time = "2026-06-25T17:20:09.191Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f032696cb01c9b54c0263fa393474d7758f1cdc021a01b04e3cbc2500999/ruff-0.15.20-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487", size = 11500132, upload-time = "2026-06-25T17:20:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f4/51b1a14bc69e8c224b15dab9cce8e99b425e0455d462caa2b3c9be2b6a8e/ruff-0.15.20-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3", size = 10943828, upload-time = "2026-06-25T17:20:16.635Z" }, + { url = "https://files.pythonhosted.org/packages/71/4b/fe267640783cd02bf6c5cc290b1df1051be2ec294c678b5c15fe19e52343/ruff-0.15.20-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053", size = 10645418, upload-time = "2026-06-25T17:20:19.4Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c0/a65aa4ec2f5e87a1df32dc3ec1fede434fe3dfd5cbcf3b503cafc676ab54/ruff-0.15.20-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4", size = 11211770, upload-time = "2026-06-25T17:20:22.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a4/0caa331d954ae2723d729d351c989cb4ca8b6077d5c6c2cb6de75e98c041/ruff-0.15.20-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460", size = 11618698, upload-time = "2026-06-25T17:20:25.259Z" }, + { url = "https://files.pythonhosted.org/packages/10/9b/5f14927848d2fd4aa891fd88d883788c5a7baba561c7874732364045708c/ruff-0.15.20-py3-none-win32.whl", hash = "sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21", size = 10857322, upload-time = "2026-06-25T17:20:28.612Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f0/fe47c501f9dea92a26d788ff98bb5d92ed4cb4c88792c5c88af6b697dc8e/ruff-0.15.20-py3-none-win_amd64.whl", hash = "sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415", size = 11993274, upload-time = "2026-06-25T17:20:31.871Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2b/9555445e1201d92b3195f45cdb153a0b68f24e0a4273f6e3d5ab46e212bb/ruff-0.15.20-py3-none-win_arm64.whl", hash = "sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca", size = 11343498, upload-time = "2026-06-25T17:20:35.03Z" }, ] [[package]] @@ -1024,6 +1038,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, ] +[[package]] +name = "towncrier" +version = "25.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "jinja2" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/eb/5bf25a34123698d3bbab39c5bc5375f8f8bcbcc5a136964ade66935b8b9d/towncrier-25.8.0.tar.gz", hash = "sha256:eef16d29f831ad57abb3ae32a0565739866219f1ebfbdd297d32894eb9940eb1", size = 76322, upload-time = "2025-08-30T11:41:55.393Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/06/8ba22ec32c74ac1be3baa26116e3c28bc0e76a5387476921d20b6fdade11/towncrier-25.8.0-py3-none-any.whl", hash = "sha256:b953d133d98f9aeae9084b56a3563fd2519dfc6ec33f61c9cd2c61ff243fb513", size = 65101, upload-time = "2025-08-30T11:41:53.644Z" }, +] + [[package]] name = "typing-extensions" version = "4.13.2"