feat: add python utils and integrate into workflow (#2176)
Some checks failed
Glean probe-scraper / glean-probe-scraper (push) Has been cancelled
Main Workflow - Lint, Build, Test / python-env (push) Has been cancelled
Main Workflow - Lint, Build, Test / rust-env (push) Has been cancelled
Main Workflow - Lint, Build, Test / python-checks (push) Has been cancelled
Main Workflow - Lint, Build, Test / rust-checks (push) Has been cancelled
Main Workflow - Lint, Build, Test / clippy (mysql) (push) Has been cancelled
Main Workflow - Lint, Build, Test / clippy (postgres) (push) Has been cancelled
Main Workflow - Lint, Build, Test / clippy (spanner) (push) Has been cancelled
Main Workflow - Lint, Build, Test / build-and-unit-test-postgres (push) Has been cancelled
Main Workflow - Lint, Build, Test / build-postgres-image (push) Has been cancelled
Main Workflow - Lint, Build, Test / postgres-e2e-tests (push) Has been cancelled
Main Workflow - Lint, Build, Test / build-and-unit-test-mysql (push) Has been cancelled
Main Workflow - Lint, Build, Test / build-mysql-image (push) Has been cancelled
Main Workflow - Lint, Build, Test / mysql-e2e-tests (push) Has been cancelled
Main Workflow - Lint, Build, Test / build-and-unit-test-spanner (push) Has been cancelled
Main Workflow - Lint, Build, Test / build-spanner-image (push) Has been cancelled
Main Workflow - Lint, Build, Test / spanner-e2e-tests (push) Has been cancelled
Build, Tag and Push Container Images to GAR / check (push) Has been cancelled
Build, Tag and Push Container Images to GAR / build-and-push-syncstorage-rs (push) Has been cancelled
Build, Tag and Push Container Images to GAR / build-and-push-syncserver-postgres (push) Has been cancelled
Build, Tag and Push Container Images to GAR / build-and-push-syncstorage-rs-spanner-python-utils (push) Has been cancelled
Build, Tag and Push Container Images to GAR / build-and-push-syncserver-postgres-python-utils (push) Has been cancelled
Build, Tag and Push Container Images to GAR / build-and-push-syncserver-mysql (push) Has been cancelled
Publish Sync docs to pages / build-mdbook (push) Has been cancelled
Publish Sync docs to pages / build-openapi (push) Has been cancelled
Publish Sync docs to pages / combine-and-prepare (push) Has been cancelled
Publish Sync docs to pages / deploy (push) Has been cancelled

feat: add python utils and integrate into workflow
This commit is contained in:
Taddes 2026-04-01 16:02:34 -04:00 committed by GitHub
parent 5513da89cd
commit af1c5fb68a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 2447 additions and 2044 deletions

View File

@ -121,6 +121,15 @@ jobs:
- name: Python Lint Check
run: poetry run ruff check tools
- name: Python Docstring Check
run: poetry run pydocstyle -es --count --config=pyproject.toml tools
- name: Python Security Check
run: poetry run bandit --quiet -r -c pyproject.toml tools
- name: Python Type Check
run: poetry run mypy --config-file=pyproject.toml tools
# Rust lint and format checks ======
rust-checks:
needs: rust-env

View File

@ -284,6 +284,18 @@ py-deps-latest: $(INSTALL_STAMP) ## Checks latest versions in PyPI
py-deps-outdated: $(INSTALL_STAMP) ## Checks for outdated Python packages
$(POETRY) show --outdated $(TOOLS_DIR)
.PHONY: bandit
bandit: $(INSTALL_STAMP) ## Run bandit
$(POETRY) run bandit --quiet -r -c $(ROOT_PYPROJECT_TOML) $(TOOLS_DIR)
.PHONY: mypy
mypy: $(INSTALL_STAMP) ## Run mypy
$(POETRY) run mypy --config-file=$(ROOT_PYPROJECT_TOML) $(TOOLS_DIR)
.PHONY: pydocstyle
pydocstyle: $(INSTALL_STAMP) ## Run pydocstyle
$(POETRY) run pydocstyle -es --count --config=$(ROOT_PYPROJECT_TOML) $(TOOLS_DIR)
# Documentation utilities
.PHONY: doc-install-deps
doc-install-deps: ## Install the dependencies for doc generation

View File

@ -43,6 +43,44 @@ black = "^26.3.1"
bandit = "^1.9.4"
isort = "^8.0.1"
[tool.pydocstyle]
match = ".*\\.py"
convention = "pep257"
# Error Code Ref: https://www.pydocstyle.org/en/stable/error_codes.html
# D212 Multi-line docstring summary should start at the first line
add-select = ["D212"]
# D105 Docstrings for magic methods
# D107 Docstrings for __init__
# D203 as it conflicts with D211 https://github.com/PyCQA/pydocstyle/issues/141
# D205 1 blank line required between summary line and description, awkward spacing
# D400 First line should end with a period, doesn't work when sentence spans 2 lines
add-ignore = ["D105","D107","D203", "D205", "D400"]
[tool.bandit]
# B101: https://bandit.readthedocs.io/en/latest/plugins/b101_assert_used.html
# B104: https://bandit.readthedocs.io/en/latest/plugins/b104_hardcoded_bind_all_interfaces.html
# B105: https://bandit.readthedocs.io/en/latest/plugins/b105_hardcoded_password_string.html — test fixture secrets only
# B106: https://bandit.readthedocs.io/en/latest/plugins/b106_hardcoded_password_funcarg.html — test fixture secrets only
# B311: https://bandit.readthedocs.io/en/latest/blacklists/blacklist_calls.html#b311-random
# B404: https://bandit.readthedocs.io/en/latest/blacklists/blacklist_imports.html#b404-import-subprocess
# B603: https://bandit.readthedocs.io/en/latest/plugins/b603_subprocess_without_shell_equals_true.html — subprocess on trusted internal binary path
skips = ["B101", "B104", "B105", "B106", "B311", "B404", "B603"]
[tool.mypy]
python_version = "3.12"
disable_error_code = "attr-defined"
disallow_untyped_calls = false
follow_imports = "normal"
ignore_missing_imports = true
pretty = true
show_error_codes = true
strict_optional = true
warn_no_return = true
warn_redundant_casts = true
warn_return_any = true
warn_unused_ignores = true
warn_unreachable = true
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"

View File

@ -0,0 +1 @@
"""Tools package for syncstorage-rs utilities and scripts."""

View File

@ -0,0 +1 @@
"""Hawk authentication utilities package."""

46
tools/hawk/poetry.lock generated
View File

@ -450,14 +450,14 @@ toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""]
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
@ -644,30 +644,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.15.6"
version = "0.15.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"},
{file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"},
{file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"},
{file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"},
{file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"},
{file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"},
{file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"},
{file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"},
{file = "ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7"},
{file = "ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570"},
{file = "ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1"},
{file = "ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49"},
{file = "ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34"},
{file = "ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89"},
{file = "ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2"},
{file = "ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e"},
]
[[package]]

View File

@ -1,3 +1,5 @@
"""Tests for the make_hawk_token utility script."""
import argparse
import time
from typing import Any

View File

@ -0,0 +1 @@
"""Integration tests package for syncstorage-rs."""

View File

@ -1,10 +1,12 @@
"""Pytest configuration and fixtures for integration tests."""
import os
import psutil
import signal
import subprocess
import time
import pytest
import requests
import requests # type: ignore[import-untyped]
import logging
DEBUG_BUILD = "target/debug/syncserver"
@ -19,9 +21,7 @@ logger = logging.getLogger("tokenserver.scripts.conftest")
def _terminate_process(process):
"""
Gracefully terminate the process and its children.
"""
"""Gracefully terminate the process and its children."""
proc = psutil.Process(pid=process.pid)
child_proc = proc.children(recursive=True)
for p in [proc] + child_proc:
@ -30,10 +30,10 @@ def _terminate_process(process):
def _wait_for_server_startup(max_attempts=SYNC_SERVER_STARTUP_MAX_ATTEMPTS):
"""
Waits for the __heartbeat__ endpoint to return a 200, pausing for 1 second
between attempts. Raises a RuntimeError if the server does not start after
the specific number of attempts.
"""Wait for the __heartbeat__ endpoint to return a 200.
Pause for 1 second between attempts. Raise a RuntimeError if the server
does not start after the specific number of attempts.
"""
itter = 0
while True:
@ -50,10 +50,7 @@ def _wait_for_server_startup(max_attempts=SYNC_SERVER_STARTUP_MAX_ATTEMPTS):
def _start_server():
"""
Starts the syncserver process, waits for it to be running,
and return the process handle.
"""
"""Start the syncserver process, wait for it to be running, and return the handle."""
target_binary = None
if os.path.exists(DEBUG_BUILD):
target_binary = DEBUG_BUILD
@ -63,8 +60,7 @@ def _start_server():
raise RuntimeError("Neither {DEBUG_BUILD} nor {RELEASE_BUILD} were found.")
server_proc = subprocess.Popen(
target_binary,
shell=True,
[target_binary],
text=True,
env=os.environ,
)
@ -75,9 +71,7 @@ def _start_server():
def _server_manager():
"""
Context manager to gracefully start and stop the server.
"""
"""Gracefully start and stop the server as a context manager."""
server_process = _start_server()
try:
yield server_process
@ -86,8 +80,8 @@ def _server_manager():
def _set_local_test_env_vars():
"""
Set environment variables for local testing.
"""Set environment variables for local testing.
This function sets the necessary environment variables for the syncserver.
"""
os.environ.setdefault("SYNC_MASTER_SECRET", "secret0")
@ -104,8 +98,8 @@ def _set_local_test_env_vars():
@pytest.fixture(scope="session")
def setup_server_local_testing():
"""
Fixture to set up the server for local testing.
"""Set up the server for local testing.
This fixture sets the necessary environment variables and
starts the server.
"""
@ -115,8 +109,8 @@ def setup_server_local_testing():
@pytest.fixture(scope="session")
def setup_server_end_to_end_testing():
"""
Fixture to set up the server for end-to-end testing.
"""Set up the server for end-to-end testing.
This fixture sets the necessary environment variables and
starts the server.
"""

File diff suppressed because it is too large Load Diff

View File

@ -1,8 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Functional tests for the SyncStorage server protocol.
"""Functional tests for the SyncStorage server protocol.
This file runs tests to ensure the correct operation of the server
as specified in:
@ -11,7 +10,6 @@ as specified in:
If there's an aspect of that spec that's not covered by a test in this file,
consider it a bug.
"""
import pytest
@ -26,7 +24,7 @@ import webtest
import contextlib
# import math
import simplejson
import simplejson # type: ignore[import-untyped]
from pyramid.interfaces import IAuthenticationPolicy
from webtest.app import AppError
@ -37,10 +35,14 @@ import tokenlib
class ConflictError(Exception):
"""Raised when a conflict (409) response is returned."""
pass
class BackendError(Exception):
"""Raised when a backend error (503) response is returned."""
pass
@ -70,6 +72,7 @@ _ASCII = string.ascii_letters + string.digits
def randtext(size=10):
"""Return a random ASCII string of the given size."""
return "".join([random.choice(_ASCII) for i in range(size)])
@ -92,7 +95,8 @@ class TestStorage(StorageFunctionalTestCase):
@contextlib.contextmanager
def _switch_user(self):
"""Allows for temporary switch url to another user id.
"""Allow temporary switch of url to another user id.
Context manager yields for duration of test and then
returns to original user, regardless of test result.
If unsuccessful, root url is retained.
@ -111,15 +115,15 @@ class TestStorage(StorageFunctionalTestCase):
self.root = orig_root
def retry_post_json(self, *args, **kwargs):
"""Helper wrapper for any POST operation."""
"""Send a POST request with retry on transient errors."""
return self._retry_send(self.app.post_json, *args, **kwargs)
def retry_put_json(self, *args, **kwargs):
"""Helper wrapper for any PUT operation."""
"""Send a PUT request with retry on transient errors."""
return self._retry_send(self.app.put_json, *args, **kwargs)
def retry_delete(self, *args, **kwargs):
"""Helper wrapper for any DELETE operation."""
"""Send a DELETE request with retry on transient errors."""
return self._retry_send(self.app.delete, *args, **kwargs)
def _retry_send(self, func, *args, **kwargs):
@ -137,6 +141,7 @@ class TestStorage(StorageFunctionalTestCase):
return func(*args, **kwargs)
def test_get_info_collections(self):
"""Test get info collections."""
# xxx_col1 gets 3 items, xxx_col2 gets 5 items.
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(3)]
resp = self.retry_post_json(self.root + "/storage/xxx_col1", bsos)
@ -164,6 +169,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res["xxx_col2"], ts2)
def test_get_collection_count(self):
"""Test get collection count."""
# xxx_col1 gets 3 items, xxx_col2 gets 5 items.
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(3)]
self.retry_post_json(self.root + "/storage/xxx_col1", bsos)
@ -177,6 +183,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res["xxx_col2"], 5)
def test_bad_cache(self):
"""Test bad cache."""
# fixes #637332
# the collection name <-> id mapper is temporarely cached to
# save a few requests.
@ -195,6 +202,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(resp.json), numcols + 1)
def test_get_collection_only(self):
"""Test get collection only."""
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(5)]
self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
@ -403,6 +411,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res, ["01", "02", "00"])
def test_alternative_formats(self):
"""Test alternative formats."""
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(5)]
self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
@ -443,6 +452,7 @@ class TestStorage(StorageFunctionalTestCase):
)
def test_set_collection_with_if_modified_since(self):
"""Test set collection with if modified since."""
# Create five items with different timestamps.
for i in range(5):
bsos = [{"id": str(i).zfill(2), "payload": "xxx"}]
@ -465,6 +475,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertTrue("X-Last-Modified" in res.headers)
def test_get_item(self):
"""Test get item."""
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(5)]
self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
# grabbing object 1 from xxx_col2
@ -496,6 +507,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res.json["id"], "01")
def test_set_item(self):
"""Test set item."""
# let's create an object
bso = {"payload": _PLD}
self.retry_put_json(self.root + "/storage/xxx_col2/12345", bso)
@ -511,6 +523,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res["payload"], "YYY")
def test_set_collection(self):
"""Test set collection."""
# sending two bsos
bso1 = {"id": "12", "payload": _PLD}
bso2 = {"id": "13", "payload": _PLD}
@ -547,6 +560,7 @@ class TestStorage(StorageFunctionalTestCase):
self.app.get(self.root + "/storage/xxx_col2/two", status=404)
def test_set_collection_input_formats(self):
"""Test set collection input formats."""
# If we send with application/newlines it should work.
bso1 = {"id": "12", "payload": _PLD}
bso2 = {"id": "13", "payload": _PLD}
@ -572,6 +586,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(items), 0)
def test_set_item_input_formats(self):
"""Test set item input formats."""
# If we send with application/json it should work.
body = json_dumps({"payload": _PLD})
self.app.put(
@ -600,6 +615,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(item["payload"], _PLD)
def test_app_newlines_when_payloads_contain_newlines(self):
"""Test app newlines when payloads contain newlines."""
# Send some application/newlines with embedded newline chars.
bsos = [
{"id": "01", "payload": "hello\nworld"},
@ -634,6 +650,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(items[1]["payload"], bsos[1]["payload"])
def test_collection_usage(self):
"""Test collection usage."""
self.retry_delete(self.root + "/storage")
bso1 = {"id": "13", "payload": "XyX"}
@ -648,6 +665,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(round(xxx_col2_size, 2), round(wanted, 2))
def test_delete_collection_items(self):
"""Test delete collection items."""
# creating a collection of three
bso1 = {"id": "12", "payload": _PLD}
bso2 = {"id": "13", "payload": _PLD}
@ -675,6 +693,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res.json), 0)
def test_delete_item(self):
"""Test delete item."""
# creating a collection of three
bso1 = {"id": "12", "payload": _PLD}
bso2 = {"id": "13", "payload": _PLD}
@ -698,6 +717,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertTrue(ts < float(res.headers["X-Last-Modified"]))
def test_delete_storage(self):
"""Test delete storage."""
# creating a collection of three
bso1 = {"id": "12", "payload": _PLD}
bso2 = {"id": "13", "payload": _PLD}
@ -715,6 +735,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(items), 0)
def test_x_timestamp_header(self):
"""Test x timestamp header."""
if self.distant:
pytest.skip("Test cannot be run against a live server.")
@ -744,6 +765,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertTrue(now <= float(res.headers["X-Weave-Timestamp"]))
def test_ifunmodifiedsince(self):
"""Test ifunmodifiedsince."""
bso = {"id": "12345", "payload": _PLD}
res = self.retry_put_json(self.root + "/storage/xxx_col2/12345", bso)
# Using an X-If-Unmodified-Since in the past should cause 412s.
@ -842,6 +864,7 @@ class TestStorage(StorageFunctionalTestCase):
)
def test_quota(self):
"""Test quota."""
res = self.app.get(self.root + "/info/quota")
old_used = res.json[0]
bso = {"payload": _PLD}
@ -851,6 +874,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(used - old_used, len(_PLD) / 1024.0)
def test_get_collection_ttl(self):
"""Test get collection ttl."""
bso = {"payload": _PLD, "ttl": 0}
res = self.retry_put_json(self.root + "/storage/xxx_col2/12345", bso)
time.sleep(1.1)
@ -874,6 +898,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res.json), 0)
def test_multi_item_post_limits(self):
"""Test multi item post limits."""
res = self.app.get(self.root + "/info/configuration")
try:
max_bytes = res.json["max_post_bytes"]
@ -923,6 +948,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res["failed"]), 1)
def test_weird_args(self):
"""Test weird args."""
# pushing some data in xxx_col2
bsos = [{"id": str(i).zfill(2), "payload": _PLD} for i in range(10)]
res = self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
@ -948,6 +974,7 @@ class TestStorage(StorageFunctionalTestCase):
self.app.get(self.root + "/storage/xxx_col2?blabla=1", status=200)
def test_guid_deletion(self):
"""Test guid deletion."""
# pushing some data in xxx_col2
bsos = [
{
@ -971,6 +998,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res.json), 3)
def test_specifying_ids_with_percent_encoded_query_string(self):
"""Test specifying ids with percent encoded query string."""
# create some items
bsos = [{"id": "test-%d" % i, "payload": _PLD} for i in range(5)]
res = self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
@ -987,6 +1015,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res.json), 3)
def test_timestamp_numbers_are_decimals(self):
"""Test timestamp numbers are decimals."""
# Create five items with different timestamps.
for i in range(5):
bsos = [{"id": str(i).zfill(2), "payload": "xxx"}]
@ -1024,6 +1053,7 @@ class TestStorage(StorageFunctionalTestCase):
raise AssertionError(msg)
def test_strict_newer(self):
"""Test strict newer."""
# send two bsos in the 'meh' collection
bso1 = {"id": "01", "payload": _PLD}
bso2 = {"id": "02", "payload": _PLD}
@ -1044,6 +1074,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(sorted(res), ["03", "04"])
def test_strict_older(self):
"""Test strict older."""
# send two bsos in the 'xxx_meh' collection
bso1 = {"id": "01", "payload": _PLD}
bso2 = {"id": "02", "payload": _PLD}
@ -1064,6 +1095,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(sorted(res), ["01", "02"])
def test_handling_of_invalid_json_in_bso_uploads(self):
"""Test handling of invalid json in bso uploads."""
# Single upload with JSON that's not a BSO.
bso = "notabso"
res = self.retry_put_json(
@ -1106,6 +1138,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res["failed"]), 1)
def test_handling_of_invalid_bso_fields(self):
"""Test handling of invalid bso fields."""
coll_url = self.root + "/storage/xxx_col2"
# Invalid ID - unacceptable characters.
# The newline cases are especially nuanced because \n
@ -1179,6 +1212,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res.json, WEAVE_INVALID_WBO)
def test_that_batch_gets_are_limited_to_max_number_of_ids(self):
"""Test that batch gets are limited to max number of ids."""
bso = {"id": "01", "payload": "testing"}
self.retry_put_json(self.root + "/storage/xxx_col2/01", bso)
@ -1197,6 +1231,7 @@ class TestStorage(StorageFunctionalTestCase):
self.app.get(self.root + "/storage/xxx_col2?ids=" + ids, status=400)
def test_that_batch_deletes_are_limited_to_max_number_of_ids(self):
"""Test that batch deletes are limited to max number of ids."""
bso = {"id": "01", "payload": "testing"}
# Deleting with less than the limit works OK.
@ -1215,6 +1250,7 @@ class TestStorage(StorageFunctionalTestCase):
self.retry_delete(self.root + "/storage/xxx_col2?ids=" + ids, status=400)
def test_that_expired_items_can_be_overwritten_via_PUT(self):
"""Test that expired items can be overwritten via PUT."""
# Upload something with a small ttl.
bso = {"payload": "XYZ", "ttl": 0}
self.retry_put_json(self.root + "/storage/xxx_col2/TEST", bso)
@ -1226,6 +1262,7 @@ class TestStorage(StorageFunctionalTestCase):
self.retry_put_json(self.root + "/storage/xxx_col2/TEST", bso)
def test_if_modified_since_on_info_views(self):
"""Test if modified since on info views."""
# Store something, so the views have a modified time > 0.
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(3)]
self.retry_post_json(self.root + "/storage/xxx_col1", bsos)
@ -1274,6 +1311,7 @@ class TestStorage(StorageFunctionalTestCase):
# self.app.get(self.root + view, headers=headers, status=304)
def test_that_x_last_modified_is_sent_for_all_get_requests(self):
"""Test that x last modified is sent for all get requests."""
bsos = [{"id": str(i).zfill(2), "payload": "xxx"} for i in range(5)]
self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
r = self.app.get(self.root + "/info/collections")
@ -1286,6 +1324,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertTrue("X-Last-Modified" in r.headers)
def test_update_of_ttl_without_sending_data(self):
"""Test update of ttl without sending data."""
bso = {"payload": "x", "ttl": 1}
self.retry_put_json(self.root + "/storage/xxx_col2/TEST1", bso)
self.retry_put_json(self.root + "/storage/xxx_col2/TEST2", bso)
@ -1312,6 +1351,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertTrue(ts2 < ts3)
def test_bulk_update_of_ttls_without_sending_data(self):
"""Test bulk update of ttls without sending data."""
# Create 5 BSOs with a ttl of 1 second.
bsos = [{"id": str(i).zfill(2), "payload": "x", "ttl": 1} for i in range(5)]
r = self.retry_post_json(self.root + "/storage/xxx_col2", bsos)
@ -1346,6 +1386,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(items["06"]["modified"], ts2)
def test_that_negative_integer_fields_are_not_accepted(self):
"""Test that negative integer fields are not accepted."""
# ttls cannot be negative
self.retry_put_json(
self.root + "/storage/xxx_col2/TEST",
@ -1388,6 +1429,7 @@ class TestStorage(StorageFunctionalTestCase):
)
def test_meta_global_sanity(self):
"""Test meta global sanity."""
# Memcache backend is configured to store 'meta' in write-through
# cache, so we want to check it explicitly. We might as well put it
# in the base tests because there's nothing memcached-specific here.
@ -1416,11 +1458,13 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res.json["modified"], ts)
def test_that_404_responses_have_a_json_body(self):
"""Test that 404 responses have a json body."""
res = self.app.get(self.root + "/nonexistent/url", status=404)
self.assertEqual(res.content_type, "application/json")
self.assertEqual(res.json, 0)
def test_that_internal_server_fields_are_not_echoed(self):
"""Test that internal server fields are not echoed."""
self.retry_post_json(
self.root + "/storage/xxx_col1", [{"id": "one", "payload": "blob"}]
)
@ -1440,6 +1484,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertFalse("ttl" in res.json)
def test_accessing_info_collections_with_an_expired_token(self):
"""Test accessing info collections with an expired token."""
# This can't be run against a live server because we
# have to forge an auth token to test things properly.
if self.distant:
@ -1482,6 +1527,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(resp.json["xxx_col1"], ts)
def test_pagination_with_newer_and_sort_by_oldest(self):
"""Test pagination with newer and sort by oldest."""
# Twelve bsos with three different modification times.
NUM_ITEMS = 12
bsos = []
@ -1525,6 +1571,7 @@ class TestStorage(StorageFunctionalTestCase):
)
def test_pagination_with_older_and_sort_by_newest(self):
"""Test pagination with older and sort by newest."""
# Twelve bsos with three different modification times.
NUM_ITEMS = 12
bsos = []
@ -1568,6 +1615,7 @@ class TestStorage(StorageFunctionalTestCase):
)
def assertCloseEnough(self, val1, val2, delta=0.05):
"""Test assertCloseEnough."""
if abs(val1 - val2) < delta:
return True
raise AssertionError(
@ -1575,6 +1623,7 @@ class TestStorage(StorageFunctionalTestCase):
)
def test_batches(self):
"""Test batches."""
endpoint = self.root + "/storage/xxx_col2"
bso1 = {"id": "12", "payload": "elegance"}
@ -1653,6 +1702,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(committed, resp3.json["modified"])
def test_aaa_batch_commit_collision(self):
"""Test aaa batch commit collision."""
# It's possible that a batch contain a BSO inside a batch as well
# as inside the final "commit" message. This is a bit of a problem
# for spanner because of conflicting ways that the data is written
@ -1678,6 +1728,7 @@ class TestStorage(StorageFunctionalTestCase):
assert resp.json[0].get("payload") == repl, "wrong payload returned"
def test_we_dont_need_no_stinkin_batches(self):
"""Test we dont need no stinkin batches."""
endpoint = self.root + "/storage/xxx_col2"
# invalid batch ID
@ -1688,6 +1739,7 @@ class TestStorage(StorageFunctionalTestCase):
self.retry_post_json(endpoint + "?commit=true", [], status=400)
def test_batch_size_limits(self):
"""Test batch size limits."""
limits = self.app.get(self.root + "/info/configuration").json
self.assertTrue("max_post_records" in limits)
self.assertTrue("max_post_bytes" in limits)
@ -1842,6 +1894,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(resp.status_code, 400)
def test_batch_partial_update(self):
"""Test batch partial update."""
collection = self.root + "/storage/xxx_col2"
bsos = [
{"id": "a", "payload": "aai"},
@ -1890,6 +1943,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res[1]["sortindex"], 17)
def test_batch_ttl_update(self):
"""Test batch ttl update."""
collection = self.root + "/storage/xxx_col2"
bsos = [
{"id": "a", "payload": "ayy"},
@ -1927,6 +1981,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res[0]["payload"], "see")
def test_batch_ttl_is_based_on_commit_timestamp(self):
"""Test batch ttl is based on commit timestamp."""
collection = self.root + "/storage/xxx_col2"
resp = self.retry_post_json(collection + "?batch=true", [], status=202)
@ -1954,6 +2009,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(len(res), 0)
def test_batch_with_immediate_commit(self):
"""Test batch with immediate commit."""
collection = self.root + "/storage/xxx_col2"
bsos = [
{"id": "a", "payload": "aih"},
@ -1982,6 +2038,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res[2]["payload"], "cee")
def test_batch_uploads_properly_update_info_collections(self):
"""Test batch uploads properly update info collections."""
collection1 = self.root + "/storage/xxx_col1"
collection2 = self.root + "/storage/xxx_col2"
bsos = [
@ -2026,6 +2083,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(resp.json["xxx_col2"], ts2)
def test_batch_with_failing_bsos(self):
"""Test batch with failing bsos."""
collection = self.root + "/storage/xxx_col2"
bsos = [
{"id": "a", "payload": "aai"},
@ -2057,6 +2115,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res[1]["payload"], "sea")
def test_batch_id_is_correctly_scoped_to_a_collection(self):
"""Test batch id is correctly scoped to a collection."""
collection1 = self.root + "/storage/xxx_col1"
bsos = [
{"id": "a", "payload": "aih"},
@ -2090,6 +2149,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res[3]["payload"], "dii")
def test_users_with_the_same_batch_id_get_separate_data(self):
"""Test users with the same batch id get separate data."""
# Try to generate two users with the same batch-id.
# It might take a couple of attempts...
for _ in range(100):
@ -2122,6 +2182,7 @@ class TestStorage(StorageFunctionalTestCase):
pytest.skip("failed to generate conflicting batchid")
def test_that_we_dont_resurrect_committed_batches(self):
"""Test that we dont resurrect committed batches."""
# This retry loop tries to trigger a situation where we:
# * create a batch with a single item
# * successfully commit that batch
@ -2151,6 +2212,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(resp.json, ["j"])
def test_batch_id_is_correctly_scoped_to_a_user(self):
"""Test batch id is correctly scoped to a user."""
collection = self.root + "/storage/xxx_col1"
bsos = [
{"id": "a", "payload": "aih"},
@ -2186,6 +2248,7 @@ class TestStorage(StorageFunctionalTestCase):
# bug 1332552 make sure ttl:null use the default ttl
def test_create_bso_with_null_ttl(self):
"""Test create bso with null ttl."""
bso = {"payload": "x", "ttl": None}
self.retry_put_json(self.root + "/storage/xxx_col2/TEST1", bso)
time.sleep(0.1)
@ -2193,6 +2256,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res.json["payload"], "x")
def test_rejection_of_known_bad_payloads(self):
"""Test rejection of known bad payloads."""
bso = {
"id": "keys",
"payload": json_dumps(
@ -2212,6 +2276,8 @@ class TestStorage(StorageFunctionalTestCase):
# bug 1397357
def test_batch_empty_commit(self):
"""Test batch empty commit."""
def testEmptyCommit(contentType, body, status=200):
bsos = [{"id": str(i).zfill(2), "payload": "X"} for i in range(5)]
res = self.retry_post_json(self.root + "/storage/xxx_col?batch=true", bsos)
@ -2235,6 +2301,7 @@ class TestStorage(StorageFunctionalTestCase):
testEmptyCommit("application/newlines", "[]", status=400)
def test_cors_settings_are_set(self):
"""Test cors settings are set."""
res = self.app.options(
self.root + "/__heartbeat__",
headers={
@ -2248,6 +2315,7 @@ class TestStorage(StorageFunctionalTestCase):
self.assertEqual(res.headers["access-control-allow-origin"], "localhost")
def test_cors_allows_any_origin(self):
"""Test cors allows any origin."""
self.app.options(
self.root + "/__heartbeat__",
headers={
@ -2260,6 +2328,7 @@ class TestStorage(StorageFunctionalTestCase):
# PATCH is not a default allowed method, so request should return 405
def test_patch_is_not_allowed(self):
"""Test patch is not allowed."""
collection = self.root + "/storage/xxx_col1"
with self.assertRaises(AppError) as error:
self.app.patch_json(collection)

View File

@ -49,9 +49,11 @@ class Secrets(object):
self.load(filename)
def keys(self):
"""Return all node keys stored in secrets."""
return self._secrets.keys()
def load(self, filename):
"""Load secrets from the given filename or list of filenames."""
if not isinstance(filename, (list, tuple)):
filename = [filename]
@ -74,6 +76,7 @@ class Secrets(object):
self._secrets[node] = secrets
def save(self, filename):
"""Save secrets to the given filename in CSV format."""
with open(filename, "wb") as f:
writer = csv.writer(f, delimiter=",")
for node, secrets in self._secrets.items():
@ -84,9 +87,11 @@ class Secrets(object):
writer.writerow(secrets)
def get(self, node):
"""Return list of secrets for the given node."""
return [secret for timestamp, secret in self._secrets[node]]
def add(self, node, size=256):
"""Add a new randomly generated secret for the given node."""
timestamp = str(int(time.time()))
secret = binascii.b2a_hex(os.urandom(size))[:size]
# The new secret *must* sort at the end of the list.
@ -114,9 +119,11 @@ class FixedSecrets(object):
self._secrets = secrets
def get(self, node):
"""Return the fixed list of secrets for any node."""
return list(self._secrets)
def keys(self):
"""Return an empty list since all nodes use the same fixed secrets."""
return []
@ -204,7 +211,7 @@ def get_configurator(global_config, **settings):
def restore_env(*keys):
"""Decorator that ensures os.environ gets restored after a test.
"""Decorate a test to ensure os.environ gets restored after the call.
Given a list of environment variable keys, this decorator will save the
current values of those environment variables at the start of the call
@ -233,10 +240,12 @@ class TestCase(unittest.TestCase):
"""TestCase with some generic helper methods."""
def setUp(self):
"""Set up test fixtures."""
super(TestCase, self).setUp()
self.config = self.get_configurator()
def tearDown(self):
"""Tear down test fixtures."""
self.config.end()
super(TestCase, self).tearDown()
@ -267,6 +276,7 @@ class StorageTestCase(TestCase):
@restore_env("MOZSVC_TEST_INI_FILE")
def setUp(self):
"""Set up test fixtures with fresh environment variables."""
# Put a fresh UUID into the environment.
# This can be used in e.g. config files to create unique paths.
os.environ["MOZSVC_UUID"] = str(uuid.uuid4())
@ -288,6 +298,7 @@ class StorageTestCase(TestCase):
super(StorageTestCase, self).setUp()
def tearDown(self):
"""Tear down test fixtures and clean up databases."""
self._cleanup_test_databases()
# clear the pyramid threadlocals
self.config.end()
@ -295,6 +306,7 @@ class StorageTestCase(TestCase):
del os.environ["MOZSVC_UUID"]
def get_configurator(self):
"""Return the test configurator with storage settings applied."""
config = super(StorageTestCase, self).get_configurator()
# config.include("syncstorage")
return config
@ -337,6 +349,7 @@ class FunctionalTestCase(TestCase):
"""
def setUp(self):
"""Set up the functional test app and host URL."""
super(FunctionalTestCase, self).setUp()
# now that we're testing against a rust server, we're always distant.
@ -366,6 +379,7 @@ class StorageFunctionalTestCase(FunctionalTestCase, StorageTestCase):
"""Abstract base class for functional testing of a storage API."""
def setUp(self):
"""Set up storage functional test with authentication credentials."""
super(StorageFunctionalTestCase, self).setUp()
# Generate userid and auth token crednentials.
@ -382,6 +396,7 @@ class StorageFunctionalTestCase(FunctionalTestCase, StorageTestCase):
self.app.do_request = new_do_request
def basic_testing_authenticate(self):
"""Authenticate using a random uid for basic testing."""
# For basic testing, use a random uid and sign our own tokens.
# Subclasses might like to override this and use a live tokenserver.
pass

View File

@ -0,0 +1 @@
"""Tokenserver integration tests package."""

View File

@ -1,3 +1,5 @@
"""Mock FxA OAuth server for integration testing."""
from wsgiref.simple_server import make_server as _make_server
from pyramid.config import Configurator
from pyramid.response import Response
@ -24,6 +26,7 @@ def _mock_oauth_jwk(request):
def make_server(host, port):
"""Create and return a mock FxA OAuth WSGI server bound to host and port."""
with Configurator() as config:
config.add_route("mock_oauth_verify", "/v1/verify")
config.add_view(

View File

@ -1,6 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Authorization integration tests for the tokenserver."""
import pytest
import unittest
from integration_tests.tokenserver.test_support import TestCase
@ -8,13 +10,18 @@ from integration_tests.tokenserver.test_support import TestCase
@pytest.mark.usefixtures("setup_server_local_testing")
class TestAuthorization(TestCase, unittest.TestCase):
"""Authorization integration tests for the tokenserver."""
def setUp(self):
"""Set up test fixtures."""
super(TestAuthorization, self).setUp()
def tearDown(self):
"""Tear down test fixtures."""
super(TestAuthorization, self).tearDown()
def test_unauthorized_error_status(self):
"""Test unauthorized error status."""
# Totally busted auth -> generic error.
headers = {"Authorization": "Unsupported-Auth-Scheme IHACKYOU"}
res = self.app.get("/1.0/sync/1.5", headers=headers, status=401)
@ -26,6 +33,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_no_auth(self):
"""Test no auth."""
res = self.app.get("/1.0/sync/1.5", status=401)
expected_error_response = {
@ -35,6 +43,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_invalid_client_state_in_key_id(self):
"""Test invalid client state in key id."""
additional_headers = {"X-KeyID": "1234-state!"}
headers = self._build_auth_headers(
keys_changed_at=1234, client_state="aaaa", **additional_headers
@ -48,6 +57,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_invalid_client_state_in_x_client_state(self):
"""Test invalid client state in x client state."""
additional_headers = {"X-Client-State": "state!"}
headers = self._build_auth_headers(
generation=1234,
@ -71,6 +81,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_keys_changed_at_less_than_equal_to_generation(self):
"""Test keys changed at less than equal to generation."""
self._add_user(generation=1232, keys_changed_at=1234)
# If keys_changed_at changes, that change must be less than or equal
# to the new generation
@ -103,6 +114,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.app.get("/1.0/sync/1.5", headers=headers)
def test_disallow_reusing_old_client_state(self):
"""Test disallow reusing old client state."""
# Add a user record that has already been replaced
self._add_user(client_state="aaaa", replaced_at=1200)
# Add the most up-to-date user record
@ -138,6 +150,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertNotEqual(res1.json["uid"], res2.json["uid"])
def test_generation_change_must_accompany_client_state_change(self):
"""Test generation change must accompany client state change."""
self._add_user(generation=1234, client_state="aaaa")
# A request with a new client state must also contain a new generation
headers = self._build_auth_headers(
@ -188,6 +201,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.app.get("/1.0/sync/1.5", headers=headers)
def test_keys_changed_at_change_must_accompany_client_state_change(self):
"""Test keys changed at change must accompany client state change."""
self._add_user(generation=1234, keys_changed_at=1234, client_state="aaaa")
# A request with a new client state must also contain a new
# keys_changed_at
@ -214,6 +228,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.app.get("/1.0/sync/1.5", headers=headers)
def test_generation_must_not_be_less_than_last_seen_value(self):
"""Test generation must not be less than last seen value."""
uid = self._add_user(generation=1234)
# The generation in the request cannot be less than the generation
# currently stored on the user record
@ -253,6 +268,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(res.json["uid"], uid)
def test_set_generation_unchanged_without_keys_changed_at_update(self):
"""Test set generation unchanged without keys changed at update."""
# Add a user who has never sent us a generation
uid = self._add_user(generation=0, keys_changed_at=1234, client_state="aaaa")
# Send a request without a generation that doesn't update
@ -274,6 +290,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(user["generation"], 1235)
def test_set_generation_with_keys_changed_at_initialization(self):
"""Test set generation with keys changed at initialization."""
# Add a user who has never sent us a generation or a keys_changed_at
uid = self._add_user(generation=0, keys_changed_at=None, client_state="aaaa")
@ -287,6 +304,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(user["generation"], 1234)
def test_fxa_kid_change(self):
"""Test fxa kid change."""
self._add_user(generation=1234, keys_changed_at=None, client_state="aaaa")
# An OAuth client shows up, setting keys_changed_at.
# (The value matches generation number above, beause in this scenario
@ -335,6 +353,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(token["node"], token0["node"])
def test_client_specified_duration(self):
"""Test client specified duration."""
self._add_user(generation=1234, keys_changed_at=1234, client_state="aaaa")
headers = self._build_auth_headers(
generation=1234, keys_changed_at=1234, client_state="aaaa"
@ -355,6 +374,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
# case to be handled. See this PR for more information:
# https://github.com/mozilla-services/tokenserver/pull/176
def test_kid_change_during_gradual_tokenserver_rollout(self):
"""Test kid change during gradual tokenserver rollout."""
# Let's start with a user already in the db, with no keys_changed_at.
uid = self._add_user(generation=1234, client_state="aaaa", keys_changed_at=None)
user1 = self._get_user(uid)
@ -394,6 +414,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(user2["nodeid"], user1["nodeid"])
def test_update_client_state(self):
"""Test update client state."""
uid = self._add_user(generation=0, keys_changed_at=None, client_state="")
user1 = self._get_user(uid)
# The user starts out with no client_state
@ -463,6 +484,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(expected_error_response, res.json)
def test_set_generation_from_no_generation(self):
"""Test set generation from no generation."""
# Add a user that has no generation set
uid = self._add_user(generation=0, keys_changed_at=None, client_state="aaaa")
headers = self._build_auth_headers(
@ -475,6 +497,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(user["generation"], 1234)
def test_set_keys_changed_at_from_no_keys_changed_at(self):
"""Test set keys changed at from no keys changed at."""
# Add a user that has no keys_changed_at set
uid = self._add_user(generation=1234, keys_changed_at=None, client_state="aaaa")
headers = self._build_auth_headers(
@ -487,6 +510,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(user["keys_changed_at"], 1234)
def test_x_client_state_must_have_same_client_state_as_key_id(self):
"""Test x client state must have same client state as key id."""
self._add_user(client_state="aaaa")
additional_headers = {"X-Client-State": "bbbb"}
headers = self._build_auth_headers(
@ -507,6 +531,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
res = self.app.get("/1.0/sync/1.5", headers=headers)
def test_zero_generation_treated_as_null(self):
"""Test zero generation treated as null."""
# Add a user that has a generation set
uid = self._add_user(generation=1234, keys_changed_at=1234, client_state="aaaa")
headers = self._build_auth_headers(
@ -520,6 +545,7 @@ class TestAuthorization(TestCase, unittest.TestCase):
self.assertEqual(user["generation"], 1234)
def test_zero_keys_changed_at_treated_as_null(self):
"""Test zero keys changed at treated as null."""
# Add a user that has no keys_changed_at set
uid = self._add_user(generation=1234, keys_changed_at=None, client_state="aaaa")
headers = self._build_auth_headers(

View File

@ -1,6 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""End-to-end integration tests for the tokenserver."""
from base64 import urlsafe_b64decode
import hmac
import json
@ -36,14 +38,19 @@ SCOPE = "https://identity.mozilla.com/apps/oldsync"
@pytest.mark.usefixtures("setup_server_end_to_end_testing")
class TestE2e(TestCase, unittest.TestCase):
"""End-to-end integration tests using real FxA accounts."""
def setUp(self):
"""Set up test fixtures."""
super(TestE2e, self).setUp()
def tearDown(self):
"""Tear down test fixtures."""
super(TestE2e, self).tearDown()
@classmethod
def setUpClass(cls):
"""Set up class-level test fixtures."""
# Create an ephemeral email account to use to create an FxA account
cls.acct = TestEmailAccount()
cls.client = Client(FXA_ACCOUNT_STAGE_HOST)
@ -67,6 +74,7 @@ class TestE2e(TestCase, unittest.TestCase):
@classmethod
def tearDownClass(cls):
"""Tear down class-level test fixtures."""
cls.acct.clear()
# A teardown of some of the tests can produce a 401 error because
# of a race condition, where the record had already been removed.
@ -131,6 +139,7 @@ class TestE2e(TestCase, unittest.TestCase):
return hasher.hexdigest()
def test_unauthorized_oauth_error_status(self):
"""Test unauthorized oauth error status."""
# Totally busted auth -> generic error.
headers = {
"Authorization": "Unsupported-Auth-Scheme IHACKYOU",
@ -158,6 +167,7 @@ class TestE2e(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_valid_oauth_request(self):
"""Test valid oauth request."""
oauth_token = self.oauth_token
headers = {"Authorization": f"Bearer {oauth_token}", "X-KeyID": "1234-qqo"}
# Send a valid request, allocating a new user

View File

@ -1,6 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Miscellaneous integration tests for the tokenserver."""
import pytest
import unittest
@ -11,13 +13,18 @@ MAX_GENERATION = 9223372036854775807
@pytest.mark.usefixtures("setup_server_local_testing")
class TestMisc(TestCase, unittest.TestCase):
"""Miscellaneous tokenserver integration tests."""
def setUp(self):
"""Set up test fixtures."""
super(TestMisc, self).setUp()
def tearDown(self):
"""Tear down test fixtures."""
super(TestMisc, self).tearDown()
def test_unknown_app(self):
"""Test unknown app."""
headers = self._build_auth_headers(
generation=1234, keys_changed_at=1234, client_state="aaaa"
)
@ -35,6 +42,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_unknown_version(self):
"""Test unknown version."""
headers = self._build_auth_headers(
generation=1234, keys_changed_at=1234, client_state="aaaa"
)
@ -52,6 +60,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(res.json, expected_error_response)
def test_valid_app(self):
"""Test valid app."""
self._add_user()
headers = self._build_auth_headers(
generation=1234, keys_changed_at=1234, client_state="aaaa"
@ -62,6 +71,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(res.json["duration"], 3600)
def test_current_user_is_the_most_up_to_date(self):
"""Test current user is the most up to date."""
# Add some users
self._add_user(generation=1234, created_at=1234)
self._add_user(generation=1235, created_at=1234)
@ -76,6 +86,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(res.json["uid"], uid)
def test_user_creation_when_most_current_user_is_replaced(self):
"""Test user creation when most current user is replaced."""
# Add some users
uid1 = self._add_user(generation=1234, created_at=1234)
uid2 = self._add_user(generation=1235, created_at=1235)
@ -90,6 +101,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertNotIn(res.json["uid"], seen_uids)
def test_old_users_marked_as_replaced_in_race_recovery(self):
"""Test old users marked as replaced in race recovery."""
# Add some users
uid1 = self._add_user(generation=1234, created_at=1234)
uid2 = self._add_user(generation=1235, created_at=1235)
@ -109,6 +121,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(user2["replaced_at"], 1240)
def test_user_updates_with_new_client_state(self):
"""Test user updates with new client state."""
# Start with a single user in the database
uid = self._add_user(generation=1234, keys_changed_at=1234, client_state="aaaa")
# Send a request, updating the generation, keys_changed_at, and
@ -144,6 +157,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(replaced_user["client_state"], "aaaa")
def test_user_updates_with_same_client_state(self):
"""Test user updates with same client state."""
# Start with a single user in the database
uid = self._add_user(generation=1234, keys_changed_at=1234)
# Send a request, updating the generation and keys_changed_at but not
@ -161,6 +175,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(user["keys_changed_at"], 1235)
def test_retired_users_can_make_requests(self):
"""Test retired users can make requests."""
# Add a retired user to the database
self._add_user(generation=MAX_GENERATION)
headers = self._build_auth_headers(
@ -182,6 +197,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.app.get("/1.0/sync/1.5", headers=headers)
def test_replaced_users_can_make_requests(self):
"""Test replaced users can make requests."""
# Add a replaced user to the database
self._add_user(generation=1234, created_at=1234, replaced_at=1234)
headers = self._build_auth_headers(
@ -191,6 +207,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.app.get("/1.0/sync/1.5", headers=headers)
def test_retired_users_with_no_node_cannot_make_requests(self):
"""Test retired users with no node cannot make requests."""
# Add a retired user to the database
invalid_node_id = self.NODE_ID + 1
self._add_user(generation=MAX_GENERATION, nodeid=invalid_node_id)
@ -201,6 +218,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.app.get("/1.0/sync/1.5", headers=headers, status=500)
def test_replaced_users_with_no_node_can_make_requests(self):
"""Test replaced users with no node can make requests."""
# Add a replaced user to the database
invalid_node_id = self.NODE_ID + 1
self._add_user(created_at=1234, replaced_at=1234, nodeid=invalid_node_id)
@ -214,6 +232,7 @@ class TestMisc(TestCase, unittest.TestCase):
self.assertEqual(user["nodeid"], self.NODE_ID)
def test_x_content_type_options(self):
"""Test x content type options."""
self._add_user(generation=1234, keys_changed_at=1234, client_state="aaaa")
headers = self._build_auth_headers(
generation=1234, keys_changed_at=1234, client_state="aaaa"

View File

@ -1,6 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Node assignment integration tests for the tokenserver."""
import pytest
import unittest
@ -10,13 +12,18 @@ from sqlalchemy.sql import text as sqltext
@pytest.mark.usefixtures("setup_server_local_testing")
class TestNodeAssignment(TestCase, unittest.TestCase):
"""Node assignment integration tests for the tokenserver."""
def setUp(self):
"""Set up test fixtures."""
super(TestNodeAssignment, self).setUp()
def tearDown(self):
"""Tear down test fixtures."""
super(TestNodeAssignment, self).tearDown()
def test_user_creation(self):
"""Test user creation."""
# Add a few more nodes
self._add_node(available=0, node="https://node1")
self._add_node(available=1, node="https://node2")
@ -46,6 +53,7 @@ class TestNodeAssignment(TestCase, unittest.TestCase):
self.assertEqual(self._count_users(), 1)
def test_new_user_allocation(self):
"""Test new user allocation."""
# Start with a clean database
cursor = self._execute_sql(sqltext("DELETE FROM nodes"), {})
cursor.close()
@ -77,6 +85,7 @@ class TestNodeAssignment(TestCase, unittest.TestCase):
self.assertEqual(node["available"], 98)
def test_successfully_releasing_node_capacity(self):
"""Test successfully releasing node capacity."""
# Start with a clean database
cursor = self._execute_sql(sqltext("DELETE FROM nodes"), {})
cursor.close()
@ -125,6 +134,7 @@ class TestNodeAssignment(TestCase, unittest.TestCase):
self.assertEqual(node5["available"], 0)
def test_unsuccessfully_releasing_node_capacity(self):
"""Test unsuccessfully releasing node capacity."""
# Start with a clean database
cursor = self._execute_sql(sqltext("DELETE FROM nodes"), {})
cursor.close()

View File

@ -1,6 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Test support utilities for tokenserver integration tests."""
from base64 import urlsafe_b64encode as b64encode
import binascii
import json
@ -18,6 +20,8 @@ DEFAULT_OAUTH_SCOPE = "https://identity.mozilla.com/apps/oldsync"
class TestCase:
"""Base test case for tokenserver integration tests."""
FXA_EMAIL_DOMAIN = "api-accounts.stage.mozaws.net"
FXA_METRICS_HASH_SECRET = os.environ.get("SYNC_MASTER_SECRET", "secret0")
NODE_ID = 800
@ -27,9 +31,11 @@ class TestCase:
@classmethod
def setUpClass(cls):
"""Set up class-level fixtures for the tokenserver test case."""
cls._build_auth_headers = cls._build_oauth_headers
def setUp(self):
"""Set up test fixtures including database connection and test node."""
db_url = os.environ["SYNC_TOKENSERVER__DATABASE_URL"]
# SQLAlchemy 1.4+ wants postgresql
if db_url.startswith("postgres://"):
@ -70,6 +76,7 @@ class TestCase:
self._add_node(capacity=100, node=self.NODE_URL, id=self.NODE_ID)
def tearDown(self):
"""Tear down test fixtures and clean up the database."""
# And clean up at the end, for good measure.
self._clear_db()
self.database.close()
@ -362,11 +369,14 @@ class TestCase:
return count
def _execute_sql(self, *args, **kwds):
"""Execute SQL statement. *args is the query and **kwds are the keyword
argument parameters."""
"""Execute SQL statement.
*args is the query and **kwds are the keyword argument parameters.
"""
cursor = self.database.execute(*args, **kwds)
return cursor
def unsafelyParseToken(self, token):
"""Parse a token without verifying its HMAC signature."""
# For testing purposes, don't check HMAC or anything...
return json.loads(decode_token_bytes(token)[:-32].decode("utf8"))

View File

@ -78,14 +78,14 @@ uvloop = ["uvloop (>=0.15.2) ; sys_platform != \"win32\"", "winloop (>=0.5.0) ;
[[package]]
name = "click"
version = "8.1.8"
version = "8.3.1"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
{file = "click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6"},
{file = "click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a"},
]
[package.dependencies]
@ -106,60 +106,66 @@ files = [
[[package]]
name = "greenlet"
version = "3.2.5"
version = "3.3.2"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["main"]
markers = "platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\""
files = [
{file = "greenlet-3.2.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:34cc7cf8ab6f4b85298b01e13e881265ee7b3c1daf6bc10a2944abc15d4f87c3"},
{file = "greenlet-3.2.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c11fe0cfb0ce33132f0b5d27eeadd1954976a82e5e9b60909ec2c4b884a55382"},
{file = "greenlet-3.2.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a145f4b1c4ed7a2c94561b7f18b4beec3d3fb6f0580db22f7ed1d544e0620b34"},
{file = "greenlet-3.2.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:edbf4ab9a7057ee430a678fe2ef37ea5d69125d6bdc7feb42ed8d871c737e63b"},
{file = "greenlet-3.2.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc1d01bdd67db3e5711e6246e451d7a0f75fae7bbf40adde129296a7f9aa7cc9"},
{file = "greenlet-3.2.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd593db7ee1fa8a513a48a404f8cc4126998a48025e3f5cbbc68d51be0a6bf66"},
{file = "greenlet-3.2.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ac8db07bced2c39b987bba13a3195f8157b0cfbce54488f86919321444a1cc3c"},
{file = "greenlet-3.2.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4544ab2cfd5912e42458b13516429e029f87d8bbcdc8d5506db772941ae12493"},
{file = "greenlet-3.2.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:acabf468466d18017e2ae5fbf1a5a88b86b48983e550e1ae1437b69a83d9f4ac"},
{file = "greenlet-3.2.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:472841de62d60f2cafd60edd4fd4dd7253eb70e6eaf14b8990dcaf177f4af957"},
{file = "greenlet-3.2.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d951e7d628a6e8b68af469f0fe4f100ef64c4054abeb9cdafbfaa30a920c950"},
{file = "greenlet-3.2.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:87b791dd0e031a574249af717ac36f7031b18c35329561c1e0368201c18caf1f"},
{file = "greenlet-3.2.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8317d732e2ae0935d9ed2af2ea876fa714cf6f3b887a31ca150b54329b0a6e9"},
{file = "greenlet-3.2.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce8aed6fdd5e07d3cbb988cbdc188266a4eb9e1a52db9ef5c6526e59962d3933"},
{file = "greenlet-3.2.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:60c06b502d56d5451f60ca665691da29f79ed95e247bcf8ce5024d7bbe64acb9"},
{file = "greenlet-3.2.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d2a78e6f1bf3f1672df91e212a2f8314e1e7c922f065d14cbad4bc815059467"},
{file = "greenlet-3.2.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2acb30e77042f747ca81f0a10cc153296567e92e666c5e1b117f4595afd43352"},
{file = "greenlet-3.2.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:393c03c26c865f17f31d8db2f09603fadbe0581ad85a5d5908b131549fc38217"},
{file = "greenlet-3.2.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:04e6a202cde56043fd355fefd1552c4caa5c087528121871d950eb4f1b51fa99"},
{file = "greenlet-3.2.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d5583b2ffa677578a384337ee13125bdf9a427485d689014b39d638a4f3d8dbe"},
{file = "greenlet-3.2.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:45fcea7b697b91290b36eafc12fff479aca6ba6500d98ef6f34d5634c7119cbe"},
{file = "greenlet-3.2.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96e2bb8a56b7e1aed1dbfbbe0050cb2ecca99c7c91892fd1771e3afab63b3e3"},
{file = "greenlet-3.2.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d7456e67b0be653dfe643bb37d9566cd30939c80f858e2ce6d2d54951f75b14a"},
{file = "greenlet-3.2.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5ceb29d1f74c7280befbbfa27b9bf91ba4a07a1a00b2179a5d953fc219b16c42"},
{file = "greenlet-3.2.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:f2cc88b50b9006b324c1b9f5f3552f9d4564c78af57cdfb4c7baf4f0aa089146"},
{file = "greenlet-3.2.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e66872daffa360b2537170b73ad530f14fa31785b1bc78080125d92edf0a6def"},
{file = "greenlet-3.2.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c5445ddb7b586d870dad32ca9fc47c287d6022a528d194efdb8912093c5303ad"},
{file = "greenlet-3.2.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:fd904626b8779810062cb455514594776e3cba3b8c0ba4939894df9f7b384971"},
{file = "greenlet-3.2.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:752c896a8c976548faafe8a306d446c6a4c68d4fd24699b84d4393bd9ac69a8e"},
{file = "greenlet-3.2.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499b809e7738c8af0ff9ac9d5dd821cb93f4293065a9237543217f0b252f950a"},
{file = "greenlet-3.2.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2c7429f6e9cea7cbf2637d86d3db12806ba970f7f972fcab39d6b54b4457cbaf"},
{file = "greenlet-3.2.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a5e4b25e855800fba17713020c5c33e0a4b7a1829027719344f0c7c8870092a2"},
{file = "greenlet-3.2.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:7123b29e6bad2f3f89681be4ef316480fca798ebe8d22fbaced9cc3775007a4f"},
{file = "greenlet-3.2.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e8fe0c72603201a86b2e038daf9b6c8570715f8779566419cff543b6ace88de"},
{file = "greenlet-3.2.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:050703a60603db0e817364d69e048c70af299040c13a7e67792b9e62d4571196"},
{file = "greenlet-3.2.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:04633da773ae432649a3f092a8e4add390732cc9e1ab52c8ff2c91b8dc86f202"},
{file = "greenlet-3.2.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6712bfd520530eb67331813f7112d3ee18e206f48b3d026d8a96cd2d2ad20251"},
{file = "greenlet-3.2.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bc06a78fa3ffbe2a75f1ebc7e040eacf6fa1050a9432953ab111fbbbf0d03c1"},
{file = "greenlet-3.2.5-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:dbe0e81e24982bb45907ca20152b31c2e3300ca352fdc4acbd4956e4a2cbc195"},
{file = "greenlet-3.2.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:15871afc0d78ec87d15d8412b337f287fc69f8f669346e391585824970931c48"},
{file = "greenlet-3.2.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5bf0d7d62e356ef2e87e55e46a4e930ac165f9372760fb983b5631bb479e9d3a"},
{file = "greenlet-3.2.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e3f03ddd7142c758ab41c18089a1407b9959bd276b4e6dfbd8fd06403832c87a"},
{file = "greenlet-3.2.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6dff6433742073e5b6ad40953a78a0e8cddcb3f6869e5ea635d29a810ca5e7d0"},
{file = "greenlet-3.2.5-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bdd67619cefe1cc9fcab57c8853d2bb36eca9f166c0058cc0d428d471f7c785c"},
{file = "greenlet-3.2.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3828b309dfb1f117fe54867512a8265d8d4f00f8de6908eef9b885f4d8789062"},
{file = "greenlet-3.2.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:67725ae9fea62c95cf1aa230f1b8d4dc38f7cd14f6103d1df8a5a95657eb8e54"},
{file = "greenlet-3.2.5.tar.gz", hash = "sha256:c816554eb33e7ecf9ba4defcb1fd8c994e59be6b4110da15480b3e7447ea4286"},
{file = "greenlet-3.3.2-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:9bc885b89709d901859cf95179ec9f6bb67a3d2bb1f0e88456461bd4b7f8fd0d"},
{file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b568183cf65b94919be4438dc28416b234b678c608cafac8874dfeeb2a9bbe13"},
{file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:527fec58dc9f90efd594b9b700662ed3fb2493c2122067ac9c740d98080a620e"},
{file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508c7f01f1791fbc8e011bd508f6794cb95397fdb198a46cb6635eb5b78d85a7"},
{file = "greenlet-3.3.2-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad0c8917dd42a819fe77e6bdfcb84e3379c0de956469301d9fd36427a1ca501f"},
{file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:97245cc10e5515dbc8c3104b2928f7f02b6813002770cfaffaf9a6e0fc2b94ef"},
{file = "greenlet-3.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8c1fdd7d1b309ff0da81d60a9688a8bd044ac4e18b250320a96fc68d31c209ca"},
{file = "greenlet-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:5d0e35379f93a6d0222de929a25ab47b5eb35b5ef4721c2b9cbcc4036129ff1f"},
{file = "greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86"},
{file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f"},
{file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55"},
{file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4375a58e49522698d3e70cc0b801c19433021b5c37686f7ce9c65b0d5c8677d2"},
{file = "greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358"},
{file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99"},
{file = "greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be"},
{file = "greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5"},
{file = "greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd"},
{file = "greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd"},
{file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd"},
{file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac"},
{file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae9e21c84035c490506c17002f5c8ab25f980205c3e61ddb3a2a2a2e6c411fcb"},
{file = "greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070"},
{file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79"},
{file = "greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395"},
{file = "greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f"},
{file = "greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643"},
{file = "greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4"},
{file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986"},
{file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92"},
{file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ccd21bb86944ca9be6d967cf7691e658e43417782bce90b5d2faeda0ff78a7dd"},
{file = "greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab"},
{file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a"},
{file = "greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b"},
{file = "greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124"},
{file = "greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327"},
{file = "greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab"},
{file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082"},
{file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9"},
{file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e3cb43ce200f59483eb82949bf1835a99cf43d7571e900d7c8d5c62cdf25d2f9"},
{file = "greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506"},
{file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce"},
{file = "greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5"},
{file = "greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492"},
{file = "greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71"},
{file = "greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54"},
{file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4"},
{file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff"},
{file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:59b3e2c40f6706b05a9cd299c836c6aa2378cabe25d021acd80f13abf81181cf"},
{file = "greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4"},
{file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727"},
{file = "greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e"},
{file = "greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a"},
{file = "greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2"},
]
[package.extras]
@ -168,14 +174,14 @@ test = ["objgraph", "psutil", "setuptools"]
[[package]]
name = "iniconfig"
version = "2.1.0"
version = "2.3.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
]
[[package]]
@ -296,14 +302,14 @@ files = [
[[package]]
name = "markdown-it-py"
version = "3.0.0"
version = "4.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
{file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"},
{file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"},
]
[package.dependencies]
@ -311,13 +317,12 @@ mdurl = ">=0.1,<1.0"
[package.extras]
benchmarking = ["psutil", "pytest", "pytest-benchmark"]
code-style = ["pre-commit (>=3.0,<4.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"]
linkify = ["linkify-it-py (>=1,<3)"]
plugins = ["mdit-py-plugins"]
plugins = ["mdit-py-plugins (>=0.5.0)"]
profiling = ["gprof2dot"]
rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"]
testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
[[package]]
name = "mdurl"
@ -436,21 +441,16 @@ tests = ["pytest (>=9)", "typing-extensions (>=4.15)"]
[[package]]
name = "platformdirs"
version = "4.4.0"
version = "4.9.4"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"},
{file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"},
{file = "platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868"},
{file = "platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934"},
]
[package.extras]
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.14.1)"]
[[package]]
name = "pluggy"
version = "1.6.0"
@ -564,14 +564,14 @@ toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""]
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
@ -758,30 +758,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.15.6"
version = "0.15.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"},
{file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"},
{file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"},
{file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"},
{file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"},
{file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"},
{file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"},
{file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"},
{file = "ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7"},
{file = "ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570"},
{file = "ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1"},
{file = "ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49"},
{file = "ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34"},
{file = "ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89"},
{file = "ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2"},
{file = "ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e"},
]
[[package]]
@ -888,14 +888,14 @@ files = [
[[package]]
name = "stevedore"
version = "5.5.0"
version = "5.7.0"
description = "Manage dynamic plugins for Python applications"
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["dev"]
files = [
{file = "stevedore-5.5.0-py3-none-any.whl", hash = "sha256:18363d4d268181e8e8452e71a38cd77630f345b2ef6b4a8d5614dac5ee0d18cf"},
{file = "stevedore-5.5.0.tar.gz", hash = "sha256:d31496a4f4df9825e1a1e4f1f74d19abb0154aff311c3b376fcc89dae8fccd73"},
{file = "stevedore-5.7.0-py3-none-any.whl", hash = "sha256:fd25efbb32f1abb4c9e502f385f0018632baac11f9ee5d1b70f88cc5e22ad4ed"},
{file = "stevedore-5.7.0.tar.gz", hash = "sha256:31dd6fe6b3cbe921e21dcefabc9a5f1cf848cf538a1f27543721b8ca09948aa3"},
]
[[package]]

View File

@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Utility script to purge expired TTL rows from the PostgreSQL database."""
import argparse
import logging
@ -93,7 +94,7 @@ def purge_records(args: argparse.Namespace) -> None:
if args.mode in ["batches", "both"]:
(batch_query, params) = add_conditions(
args,
f"DELETE FROM batches WHERE {expiry_condition}",
f"DELETE FROM batches WHERE {expiry_condition}", # nosec B608
)
exec_delete(
engine,
@ -106,7 +107,7 @@ def purge_records(args: argparse.Namespace) -> None:
if args.mode in ["bsos", "both"]:
(bso_query, params) = add_conditions(
args,
f"DELETE FROM bsos WHERE {expiry_condition}",
f"DELETE FROM bsos WHERE {expiry_condition}", # nosec B608
)
exec_delete(
engine,

View File

@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Tests for the PostgreSQL TTL purge utility."""
from argparse import Namespace
from unittest.mock import MagicMock, Mock, patch
@ -17,6 +18,8 @@ from purge_ttl import (
class TestParseArgsList:
"""Tests for the parse_args_list function."""
def test_empty_string(self) -> None:
"""Empty string returns an empty list."""
assert parse_args_list("") == []
@ -39,6 +42,8 @@ class TestParseArgsList:
class TestAddConditions:
"""Tests for the add_conditions function."""
def test_no_conditions(self) -> None:
"""Empty collection_ids leaves the query and params unchanged."""
args = Namespace(collection_ids=[])
@ -85,6 +90,8 @@ class TestAddConditions:
class TestGetExpiryCondition:
"""Tests for the get_expiry_condition function."""
def test_expiry_mode_now(self) -> None:
"""'now' mode compares expiry against the current timestamp."""
args = Namespace(expiry_mode="now")
@ -105,6 +112,8 @@ class TestGetExpiryCondition:
class TestGetDbEngine:
"""Tests for the get_db_engine function."""
@patch("purge_ttl.sqlalchemy.create_engine")
def test_postgresql_url(self, mock_create_engine: MagicMock) -> None:
"""A 'postgresql://' URL is passed through to create_engine unchanged."""
@ -129,6 +138,8 @@ class TestGetDbEngine:
class TestExecDelete:
"""Tests for the exec_delete function."""
@patch("purge_ttl.statsd")
def test_dryrun(self, mock_statsd: MagicMock) -> None:
"""In dryrun mode the engine is never contacted."""
@ -187,6 +198,8 @@ class TestExecDelete:
class TestIntegration:
"""Integration tests for the purge_ttl module."""
def test_full_query(self) -> None:
"""get_expiry_condition and add_conditions compose correctly for a single ID."""
args = Namespace(collection_ids=["8"], expiry_mode="now")

View File

@ -0,0 +1 @@
"""Spanner utilities package for syncstorage-rs."""

View File

@ -4,13 +4,14 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Count expired rows in the Spanner database tables."""
import sys
import logging
from typing import Any
from statsd.defaults.env import statsd
from google.cloud import spanner # type: ignore[attr-defined]
from google.cloud import spanner
from tools.spanner.utils import ids_from_env
# set up logger
@ -25,9 +26,9 @@ client: Any = spanner.Client()
def spanner_read_data(query: str, table: str) -> None:
"""
Executes a query on the specified Spanner table to count expired rows,
logs the result, and sends metrics to statsd.
"""Execute a query on the specified Spanner table to count expired rows.
Log the result and send metrics to statsd.
Args:
query (str): The SQL query to execute.
@ -54,7 +55,7 @@ if __name__ == "__main__":
logging.info("Starting count_expired_rows.py")
for table in ["batches", "bsos"]:
query = f"SELECT COUNT(*) FROM {table} WHERE expiry < CURRENT_TIMESTAMP()"
query = f"SELECT COUNT(*) FROM {table} WHERE expiry < CURRENT_TIMESTAMP()" # nosec B608
spanner_read_data(query, table)
logging.info("Completed count_expired_rows.py")

View File

@ -4,6 +4,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Count distinct users in the Spanner database."""
import sys
import logging
@ -24,12 +25,11 @@ client = spanner.Client()
def spanner_read_data() -> None:
"""
Reads data from a Google Cloud Spanner database to count the number of distinct users.
"""Read data from Spanner to count the number of distinct users.
This function connects to a Spanner instance and database using environment variables,
executes a SQL query to count the number of distinct `fxa_uid` entries in the `user_collections` table,
and logs the result. It also records the duration of the operation and the user count using statsd metrics.
Connect to a Spanner instance and database using environment variables,
execute a SQL query to count distinct `fxa_uid` entries in the
`user_collections` table, and log the result with statsd metrics.
Args:
None

View File

@ -188,125 +188,141 @@ pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
[[package]]
name = "charset-normalizer"
version = "3.4.5"
version = "3.4.6"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "charset_normalizer-3.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4167a621a9a1a986c73777dbc15d4b5eac8ac5c10393374109a343d4013ec765"},
{file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f64c6bf8f32f9133b668c7f7a7cbdbc453412bc95ecdbd157f3b1e377a92990"},
{file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:568e3c34b58422075a1b49575a6abc616d9751b4d61b23f712e12ebb78fe47b2"},
{file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:036c079aa08a6a592b82487f97c60b439428320ed1b2ea0b3912e99d30c77765"},
{file = "charset_normalizer-3.4.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340810d34ef83af92148e96e3e44cb2d3f910d2bf95e5618a5c467d9f102231d"},
{file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:cd2d0f0ec9aa977a27731a3209ebbcacebebaf41f902bd453a928bfd281cf7f8"},
{file = "charset_normalizer-3.4.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b362bcd27819f9c07cbf23db4e0e8cd4b44c5ecd900c2ff907b2b92274a7412"},
{file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:77be992288f720306ab4108fe5c74797de327f3248368dfc7e1a916d6ed9e5a2"},
{file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:8b78d8a609a4b82c273257ee9d631ded7fac0d875bdcdccc109f3ee8328cfcb1"},
{file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ba20bdf69bd127f66d0174d6f2a93e69045e0b4036dc1ca78e091bcc765830c4"},
{file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:76a9d0de4d0eab387822e7b35d8f89367dd237c72e82ab42b9f7bf5e15ada00f"},
{file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:8fff79bf5978c693c9b1a4d71e4a94fddfb5fe744eb062a318e15f4a2f63a550"},
{file = "charset_normalizer-3.4.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c7e84e0c0005e3bdc1a9211cd4e62c78ba80bc37b2365ef4410cd2007a9047f2"},
{file = "charset_normalizer-3.4.5-cp310-cp310-win32.whl", hash = "sha256:58ad8270cfa5d4bef1bc85bd387217e14ff154d6630e976c6f56f9a040757475"},
{file = "charset_normalizer-3.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:02a9d1b01c1e12c27883b0c9349e0bcd9ae92e727ff1a277207e1a262b1cbf05"},
{file = "charset_normalizer-3.4.5-cp310-cp310-win_arm64.whl", hash = "sha256:039215608ac7b358c4da0191d10fc76868567fbf276d54c14721bdedeb6de064"},
{file = "charset_normalizer-3.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:610f72c0ee565dfb8ae1241b666119582fdbfe7c0975c175be719f940e110694"},
{file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60d68e820af339df4ae8358c7a2e7596badeb61e544438e489035f9fbf3246a5"},
{file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:10b473fc8dca1c3ad8559985794815f06ca3fc71942c969129070f2c3cdf7281"},
{file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d4eb8ac7469b2a5d64b5b8c04f84d8bf3ad340f4514b98523805cbf46e3b3923"},
{file = "charset_normalizer-3.4.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5bcb3227c3d9aaf73eaaab1db7ccd80a8995c509ee9941e2aae060ca6e4e5d81"},
{file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:75ee9c1cce2911581a70a3c0919d8bccf5b1cbc9b0e5171400ec736b4b569497"},
{file = "charset_normalizer-3.4.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d1401945cb77787dbd3af2446ff2d75912327c4c3a1526ab7955ecf8600687c"},
{file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a45e504f5e1be0bd385935a8e1507c442349ca36f511a47057a71c9d1d6ea9e"},
{file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e09f671a54ce70b79a1fc1dc6da3072b7ef7251fadb894ed92d9aa8218465a5f"},
{file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:d01de5e768328646e6a3fa9e562706f8f6641708c115c62588aef2b941a4f88e"},
{file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:131716d6786ad5e3dc542f5cc6f397ba3339dc0fb87f87ac30e550e8987756af"},
{file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:1a374cc0b88aa710e8865dc1bd6edb3743c59f27830f0293ab101e4cf3ce9f85"},
{file = "charset_normalizer-3.4.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d31f0d1671e1534e395f9eb84a68e0fb670e1edb1fe819a9d7f564ae3bc4e53f"},
{file = "charset_normalizer-3.4.5-cp311-cp311-win32.whl", hash = "sha256:cace89841c0599d736d3d74a27bc5821288bb47c5441923277afc6059d7fbcb4"},
{file = "charset_normalizer-3.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:f8102ae93c0bc863b1d41ea0f4499c20a83229f52ed870850892df555187154a"},
{file = "charset_normalizer-3.4.5-cp311-cp311-win_arm64.whl", hash = "sha256:ed98364e1c262cf5f9363c3eca8c2df37024f52a8fa1180a3610014f26eac51c"},
{file = "charset_normalizer-3.4.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed97c282ee4f994ef814042423a529df9497e3c666dca19be1d4cd1129dc7ade"},
{file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0294916d6ccf2d069727d65973c3a1ca477d68708db25fd758dd28b0827cff54"},
{file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dc57a0baa3eeedd99fafaef7511b5a6ef4581494e8168ee086031744e2679467"},
{file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ed1a9a204f317ef879b32f9af507d47e49cd5e7f8e8d5d96358c98373314fc60"},
{file = "charset_normalizer-3.4.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ad83b8f9379176c841f8865884f3514d905bcd2a9a3b210eaa446e7d2223e4d"},
{file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:a118e2e0b5ae6b0120d5efa5f866e58f2bb826067a646431da4d6a2bdae7950e"},
{file = "charset_normalizer-3.4.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:754f96058e61a5e22e91483f823e07df16416ce76afa4ebf306f8e1d1296d43f"},
{file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0c300cefd9b0970381a46394902cd18eaf2aa00163f999590ace991989dcd0fc"},
{file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c108f8619e504140569ee7de3f97d234f0fbae338a7f9f360455071ef9855a95"},
{file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d1028de43596a315e2720a9849ee79007ab742c06ad8b45a50db8cdb7ed4a82a"},
{file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:19092dde50335accf365cce21998a1c6dd8eafd42c7b226eb54b2747cdce2fac"},
{file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4354e401eb6dab9aed3c7b4030514328a6c748d05e1c3e19175008ca7de84fb1"},
{file = "charset_normalizer-3.4.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a68766a3c58fde7f9aaa22b3786276f62ab2f594efb02d0a1421b6282e852e98"},
{file = "charset_normalizer-3.4.5-cp312-cp312-win32.whl", hash = "sha256:1827734a5b308b65ac54e86a618de66f935a4f63a8a462ff1e19a6788d6c2262"},
{file = "charset_normalizer-3.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:728c6a963dfab66ef865f49286e45239384249672cd598576765acc2a640a636"},
{file = "charset_normalizer-3.4.5-cp312-cp312-win_arm64.whl", hash = "sha256:75dfd1afe0b1647449e852f4fb428195a7ed0588947218f7ba929f6538487f02"},
{file = "charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23"},
{file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8"},
{file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d"},
{file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce"},
{file = "charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819"},
{file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d"},
{file = "charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763"},
{file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9"},
{file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c"},
{file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67"},
{file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3"},
{file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf"},
{file = "charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6"},
{file = "charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f"},
{file = "charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7"},
{file = "charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36"},
{file = "charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873"},
{file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f"},
{file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4"},
{file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee"},
{file = "charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66"},
{file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362"},
{file = "charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7"},
{file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d"},
{file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6"},
{file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39"},
{file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6"},
{file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94"},
{file = "charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e"},
{file = "charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2"},
{file = "charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa"},
{file = "charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4"},
{file = "charset_normalizer-3.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e22d1059b951e7ae7c20ef6b06afd10fb95e3c41bf3c4fbc874dba113321c193"},
{file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:afca7f78067dd27c2b848f1b234623d26b87529296c6c5652168cc1954f2f3b2"},
{file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec56a2266f32bc06ed3c3e2a8f58417ce02f7e0356edc89786e52db13c593c98"},
{file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2b970382e4a36bed897c19f310f31d7d13489c11b4f468ddfba42d41cddfb918"},
{file = "charset_normalizer-3.4.5-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573ef5814c4b7c0d59a7710aa920eaaaef383bd71626aa420fba27b5cab92e8d"},
{file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:50bcbca6603c06a1dcc7b056ed45c37715fb5d2768feb3bcd37d2313c587a5b9"},
{file = "charset_normalizer-3.4.5-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1f2da5cbb9becfcd607757a169e38fb82aa5fd86fae6653dea716e7b613fe2cf"},
{file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:fc1c64934b8faf7584924143eb9db4770bbdb16659626e1a1a4d9efbcb68d947"},
{file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ae8b03427410731469c4033934cf473426faff3e04b69d2dfb64a4281a3719f8"},
{file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:b3e71afc578b98512bfe7bdb822dd6bc57d4b0093b4b6e5487c1e96ad4ace242"},
{file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:4b8551b6e6531e156db71193771c93bda78ffc4d1e6372517fe58ad3b91e4659"},
{file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:65b3c403a5b6b8034b655e7385de4f72b7b244869a22b32d4030b99a60593eca"},
{file = "charset_normalizer-3.4.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:8ce11cd4d62d11166f2b441e30ace226c19a3899a7cf0796f668fba49a9fb123"},
{file = "charset_normalizer-3.4.5-cp38-cp38-win32.whl", hash = "sha256:66dee73039277eb35380d1b82cccc69cc82b13a66f9f4a18da32d573acf02b7c"},
{file = "charset_normalizer-3.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:d29dd9c016f2078b43d0c357511e87eee5b05108f3dd603423cb389b89813969"},
{file = "charset_normalizer-3.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:259cd1ca995ad525f638e131dbcc2353a586564c038fc548a3fe450a91882139"},
{file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a28afb04baa55abf26df544e3e5c6534245d3daa5178bc4a8eeb48202060d0e"},
{file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ff95a9283de8a457e6b12989de3f9f5193430f375d64297d323a615ea52cbdb3"},
{file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:708c7acde173eedd4bfa4028484426ba689d2103b28588c513b9db2cd5ecde9c"},
{file = "charset_normalizer-3.4.5-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa92ec1102eaff840ccd1021478af176a831f1bccb08e526ce844b7ddda85c22"},
{file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:5fea359734b140d0d6741189fea5478c6091b54ffc69d7ce119e0a05637d8c99"},
{file = "charset_normalizer-3.4.5-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e545b51da9f9af5c67815ca0eb40676c0f016d0b0381c86f20451e35696c5f95"},
{file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:30987f4a8ed169983f93e1be8ffeea5214a779e27ed0b059835c7afe96550ad7"},
{file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:149ec69866c3d6c2fb6f758dbc014ecb09f30b35a5ca90b6a8a2d4e54e18fdfe"},
{file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:530beedcec9b6e027e7a4b6ce26eed36678aa39e17da85e6e03d7bd9e8e9d7c9"},
{file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:14498a429321de554b140013142abe7608f9d8ccc04d7baf2ad60498374aefa2"},
{file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2820a98460c83663dd8ec015d9ddfd1e4879f12e06bb7d0500f044fb477d2770"},
{file = "charset_normalizer-3.4.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:aa2f963b4da26daf46231d9b9e0e2c9408a751f8f0d0f44d2de56d3caf51d294"},
{file = "charset_normalizer-3.4.5-cp39-cp39-win32.whl", hash = "sha256:82cc7c2ad42faec8b574351f8bc2a0c049043893853317bd9bb309f5aba6cb5a"},
{file = "charset_normalizer-3.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:92263f7eca2f4af326cd20de8d16728d2602f7cfea02e790dcde9d83c365d7cc"},
{file = "charset_normalizer-3.4.5-cp39-cp39-win_arm64.whl", hash = "sha256:014837af6fabf57121b6254fa8ade10dceabc3528b27b721a64bbc7b8b1d4eb4"},
{file = "charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0"},
{file = "charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644"},
{file = "charset_normalizer-3.4.6-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2e1d8ca8611099001949d1cdfaefc510cf0f212484fe7c565f735b68c78c3c95"},
{file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e25369dc110d58ddf29b949377a93e0716d72a24f62bad72b2b39f155949c1fd"},
{file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:259695e2ccc253feb2a016303543d691825e920917e31f894ca1a687982b1de4"},
{file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:dda86aba335c902b6149a02a55b38e96287157e609200811837678214ba2b1db"},
{file = "charset_normalizer-3.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51fb3c322c81d20567019778cb5a4a6f2dc1c200b886bc0d636238e364848c89"},
{file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:4482481cb0572180b6fd976a4d5c72a30263e98564da68b86ec91f0fe35e8565"},
{file = "charset_normalizer-3.4.6-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:39f5068d35621da2881271e5c3205125cc456f54e9030d3f723288c873a71bf9"},
{file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8bea55c4eef25b0b19a0337dc4e3f9a15b00d569c77211fa8cde38684f234fb7"},
{file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:f0cdaecd4c953bfae0b6bb64910aaaca5a424ad9c72d85cb88417bb9814f7550"},
{file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:150b8ce8e830eb7ccb029ec9ca36022f756986aaaa7956aad6d9ec90089338c0"},
{file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e68c14b04827dd76dcbd1aeea9e604e3e4b78322d8faf2f8132c7138efa340a8"},
{file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:3778fd7d7cd04ae8f54651f4a7a0bd6e39a0cf20f801720a4c21d80e9b7ad6b0"},
{file = "charset_normalizer-3.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dad6e0f2e481fffdcf776d10ebee25e0ef89f16d691f1e5dee4b586375fdc64b"},
{file = "charset_normalizer-3.4.6-cp310-cp310-win32.whl", hash = "sha256:74a2e659c7ecbc73562e2a15e05039f1e22c75b7c7618b4b574a3ea9118d1557"},
{file = "charset_normalizer-3.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:aa9cccf4a44b9b62d8ba8b4dd06c649ba683e4bf04eea606d2e94cfc2d6ff4d6"},
{file = "charset_normalizer-3.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:e985a16ff513596f217cee86c21371b8cd011c0f6f056d0920aa2d926c544058"},
{file = "charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e"},
{file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9"},
{file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d"},
{file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de"},
{file = "charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73"},
{file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c"},
{file = "charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc"},
{file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f"},
{file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef"},
{file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398"},
{file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e"},
{file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed"},
{file = "charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021"},
{file = "charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e"},
{file = "charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4"},
{file = "charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316"},
{file = "charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab"},
{file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21"},
{file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2"},
{file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff"},
{file = "charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5"},
{file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0"},
{file = "charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a"},
{file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2"},
{file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5"},
{file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6"},
{file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d"},
{file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2"},
{file = "charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923"},
{file = "charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4"},
{file = "charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb"},
{file = "charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4"},
{file = "charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f"},
{file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843"},
{file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf"},
{file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8"},
{file = "charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9"},
{file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88"},
{file = "charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84"},
{file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd"},
{file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c"},
{file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194"},
{file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc"},
{file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f"},
{file = "charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2"},
{file = "charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d"},
{file = "charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389"},
{file = "charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f"},
{file = "charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8"},
{file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421"},
{file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2"},
{file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30"},
{file = "charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db"},
{file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8"},
{file = "charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815"},
{file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a"},
{file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43"},
{file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0"},
{file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1"},
{file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f"},
{file = "charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815"},
{file = "charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d"},
{file = "charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f"},
{file = "charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4"},
{file = "charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c"},
{file = "charset_normalizer-3.4.6-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:659a1e1b500fac8f2779dd9e1570464e012f43e580371470b45277a27baa7532"},
{file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f61aa92e4aad0be58eb6eb4e0c21acf32cf8065f4b2cae5665da756c4ceef982"},
{file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f50498891691e0864dc3da965f340fada0771f6142a378083dc4608f4ea513e2"},
{file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf625105bb9eef28a56a943fec8c8a98aeb80e7d7db99bd3c388137e6eb2d237"},
{file = "charset_normalizer-3.4.6-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bd9d128ef93637a5d7a6af25363cf5dec3fa21cf80e68055aad627f280e8afa"},
{file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_armv7l.whl", hash = "sha256:d08ec48f0a1c48d75d0356cea971921848fb620fdeba805b28f937e90691209f"},
{file = "charset_normalizer-3.4.6-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1ed80ff870ca6de33f4d953fda4d55654b9a2b340ff39ab32fa3adbcd718f264"},
{file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f98059e4fcd3e3e4e2d632b7cf81c2faae96c43c60b569e9c621468082f1d104"},
{file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:ab30e5e3e706e3063bc6de96b118688cb10396b70bb9864a430f67df98c61ecc"},
{file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:d5f5d1e9def3405f60e3ca8232d56f35c98fb7bf581efcc60051ebf53cb8b611"},
{file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:461598cd852bfa5a61b09cae2b1c02e2efcd166ee5516e243d540ac24bfa68a7"},
{file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:71be7e0e01753a89cf024abf7ecb6bca2c81738ead80d43004d9b5e3f1244e64"},
{file = "charset_normalizer-3.4.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df01808ee470038c3f8dc4f48620df7225c49c2d6639e38f96e6d6ac6e6f7b0e"},
{file = "charset_normalizer-3.4.6-cp38-cp38-win32.whl", hash = "sha256:69dd852c2f0ad631b8b60cfbe25a28c0058a894de5abb566619c205ce0550eae"},
{file = "charset_normalizer-3.4.6-cp38-cp38-win_amd64.whl", hash = "sha256:517ad0e93394ac532745129ceabdf2696b609ec9f87863d337140317ebce1c14"},
{file = "charset_normalizer-3.4.6-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31215157227939b4fb3d740cd23fe27be0439afef67b785a1eb78a3ae69cba9e"},
{file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecbbd45615a6885fe3240eb9db73b9e62518b611850fdf8ab08bd56de7ad2b17"},
{file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c45a03a4c69820a399f1dda9e1d8fbf3562eda46e7720458180302021b08f778"},
{file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e8aeb10fcbe92767f0fa69ad5a72deca50d0dca07fbde97848997d778a50c9fe"},
{file = "charset_normalizer-3.4.6-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:54fae94be3d75f3e573c9a1b5402dc593de19377013c9a0e4285e3d402dd3a2a"},
{file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:2f7fdd9b6e6c529d6a2501a2d36b240109e78a8ceaef5687cfcfa2bbe671d297"},
{file = "charset_normalizer-3.4.6-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d1d02209e06550bdaef34af58e041ad71b88e624f5d825519da3a3308e22687"},
{file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8bc5f0687d796c05b1e28ab0d38a50e6309906ee09375dd3aff6a9c09dd6e8f4"},
{file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:ee4ec14bc1680d6b0afab9aea2ef27e26d2024f18b24a2d7155a52b60da7e833"},
{file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:d1a2ee9c1499fc8f86f4521f27a973c914b211ffa87322f4ee33bb35392da2c5"},
{file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:48696db7f18afb80a068821504296eb0787d9ce239b91ca15059d1d3eaacf13b"},
{file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4f41da960b196ea355357285ad1316a00099f22d0929fe168343b99b254729c9"},
{file = "charset_normalizer-3.4.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:802168e03fba8bbc5ce0d866d589e4b1ca751d06edee69f7f3a19c5a9fe6b597"},
{file = "charset_normalizer-3.4.6-cp39-cp39-win32.whl", hash = "sha256:8761ac29b6c81574724322a554605608a9960769ea83d2c73e396f3df896ad54"},
{file = "charset_normalizer-3.4.6-cp39-cp39-win_amd64.whl", hash = "sha256:1cf0a70018692f85172348fe06d3a4b63f94ecb055e13a00c644d368eb82e5b8"},
{file = "charset_normalizer-3.4.6-cp39-cp39-win_arm64.whl", hash = "sha256:3516bbb8d42169de9e61b8520cbeeeb716f12f4ecfe3fd30a9919aa16c806ca8"},
{file = "charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69"},
{file = "charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6"},
]
[[package]]
@ -339,61 +355,61 @@ files = [
[[package]]
name = "cryptography"
version = "46.0.5"
version = "46.0.6"
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
optional = false
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
groups = ["main"]
files = [
{file = "cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0"},
{file = "cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731"},
{file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82"},
{file = "cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1"},
{file = "cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48"},
{file = "cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4"},
{file = "cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0"},
{file = "cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663"},
{file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826"},
{file = "cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d"},
{file = "cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a"},
{file = "cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4"},
{file = "cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d"},
{file = "cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c"},
{file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4"},
{file = "cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9"},
{file = "cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72"},
{file = "cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595"},
{file = "cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c"},
{file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a"},
{file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356"},
{file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da"},
{file = "cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257"},
{file = "cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7"},
{file = "cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d"},
{file = "cryptography-46.0.6-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:64235194bad039a10bb6d2d930ab3323baaec67e2ce36215fd0952fad0930ca8"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:26031f1e5ca62fcb9d1fcb34b2b60b390d1aacaa15dc8b895a9ed00968b97b30"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9a693028b9cbe51b5a1136232ee8f2bc242e4e19d456ded3fa7c86e43c713b4a"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:67177e8a9f421aa2d3a170c3e56eca4e0128883cf52a071a7cbf53297f18b175"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d9528b535a6c4f8ff37847144b8986a9a143585f0540fbcb1a98115b543aa463"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:22259338084d6ae497a19bae5d4c66b7ca1387d3264d1c2c0e72d9e9b6a77b97"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:760997a4b950ff00d418398ad73fbc91aa2894b5c1db7ccb45b4f68b42a63b3c"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3dfa6567f2e9e4c5dceb8ccb5a708158a2a871052fa75c8b78cb0977063f1507"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:cdcd3edcbc5d55757e5f5f3d330dd00007ae463a7e7aa5bf132d1f22a4b62b19"},
{file = "cryptography-46.0.6-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d4e4aadb7fc1f88687f47ca20bb7227981b03afaae69287029da08096853b738"},
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2b417edbe8877cda9022dde3a008e2deb50be9c407eef034aeeb3a8b11d9db3c"},
{file = "cryptography-46.0.6-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:380343e0653b1c9d7e1f55b52aaa2dbb2fdf2730088d48c43ca1c7c0abb7cc2f"},
{file = "cryptography-46.0.6-cp311-abi3-win32.whl", hash = "sha256:bcb87663e1f7b075e48c3be3ecb5f0b46c8fc50b50a97cf264e7f60242dca3f2"},
{file = "cryptography-46.0.6-cp311-abi3-win_amd64.whl", hash = "sha256:6739d56300662c468fddb0e5e291f9b4d084bead381667b9e654c7dd81705124"},
{file = "cryptography-46.0.6-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:2ef9e69886cbb137c2aef9772c2e7138dc581fad4fcbcf13cc181eb5a3ab6275"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7f417f034f91dcec1cb6c5c35b07cdbb2ef262557f701b4ecd803ee8cefed4f4"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d24c13369e856b94892a89ddf70b332e0b70ad4a5c43cf3e9cb71d6d7ffa1f7b"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:aad75154a7ac9039936d50cf431719a2f8d4ed3d3c277ac03f3339ded1a5e707"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3c21d92ed15e9cfc6eb64c1f5a0326db22ca9c2566ca46d845119b45b4400361"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:4668298aef7cddeaf5c6ecc244c2302a2b8e40f384255505c22875eebb47888b"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8ce35b77aaf02f3b59c90b2c8a05c73bac12cea5b4e8f3fbece1f5fddea5f0ca"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c89eb37fae9216985d8734c1afd172ba4927f5a05cfd9bf0e4863c6d5465b013"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:ed418c37d095aeddf5336898a132fba01091f0ac5844e3e8018506f014b6d2c4"},
{file = "cryptography-46.0.6-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:69cf0056d6947edc6e6760e5f17afe4bea06b56a9ac8a06de9d2bd6b532d4f3a"},
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e7304c4f4e9490e11efe56af6713983460ee0780f16c63f219984dab3af9d2d"},
{file = "cryptography-46.0.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b928a3ca837c77a10e81a814a693f2295200adb3352395fad024559b7be7a736"},
{file = "cryptography-46.0.6-cp314-cp314t-win32.whl", hash = "sha256:97c8115b27e19e592a05c45d0dd89c57f81f841cc9880e353e0d3bf25b2139ed"},
{file = "cryptography-46.0.6-cp314-cp314t-win_amd64.whl", hash = "sha256:c797e2517cb7880f8297e2c0f43bb910e91381339336f75d2c1c2cbf811b70b4"},
{file = "cryptography-46.0.6-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:12cae594e9473bca1a7aceb90536060643128bb274fcea0fc459ab90f7d1ae7a"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:639301950939d844a9e1c4464d7e07f902fe9a7f6b215bb0d4f28584729935d8"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed3775295fb91f70b4027aeba878d79b3e55c0b3e97eaa4de71f8f23a9f2eb77"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8927ccfbe967c7df312ade694f987e7e9e22b2425976ddbf28271d7e58845290"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:b12c6b1e1651e42ab5de8b1e00dc3b6354fdfd778e7fa60541ddacc27cd21410"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:063b67749f338ca9c5a0b7fe438a52c25f9526b851e24e6c9310e7195aad3b4d"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:02fad249cb0e090b574e30b276a3da6a149e04ee2f049725b1f69e7b8351ec70"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:7e6142674f2a9291463e5e150090b95a8519b2fb6e6aaec8917dd8d094ce750d"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:456b3215172aeefb9284550b162801d62f5f264a081049a3e94307fe20792cfa"},
{file = "cryptography-46.0.6-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:341359d6c9e68834e204ceaf25936dffeafea3829ab80e9503860dcc4f4dac58"},
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9a9c42a2723999a710445bc0d974e345c32adfd8d2fac6d8a251fa829ad31cfb"},
{file = "cryptography-46.0.6-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6617f67b1606dfd9fe4dbfa354a9508d4a6d37afe30306fe6c101b7ce3274b72"},
{file = "cryptography-46.0.6-cp38-abi3-win32.whl", hash = "sha256:7f6690b6c55e9c5332c0b59b9c8a3fb232ebf059094c17f9019a51e9827df91c"},
{file = "cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:2ea0f37e9a9cf0df2952893ad145fd9627d326a59daec9b0802480fa3bcd2ead"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a3e84d5ec9ba01f8fd03802b2147ba77f0c8f2617b2aff254cedd551844209c8"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:12f0fa16cc247b13c43d56d7b35287ff1569b5b1f4c5e87e92cc4fcc00cd10c0"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:50575a76e2951fe7dbd1f56d181f8c5ceeeb075e9ff88e7ad997d2f42af06e7b"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:90e5f0a7b3be5f40c3a0a0eafb32c681d8d2c181fc2a1bdabe9b3f611d9f6b1a"},
{file = "cryptography-46.0.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6728c49e3b2c180ef26f8e9f0a883a2c585638db64cf265b49c9ba10652d430e"},
{file = "cryptography-46.0.6.tar.gz", hash = "sha256:27550628a518c5c6c903d84f637fbecf287f6cb9ced3804838a1295dc1fd0759"},
]
[package.dependencies]
@ -406,7 +422,7 @@ nox = ["nox[uv] (>=2024.4.15)"]
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
sdist = ["build (>=1.0.0)"]
ssh = ["bcrypt (>=3.1.5)"]
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.5)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.6)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
test-randomorder = ["pytest-randomly"]
[[package]]
@ -492,18 +508,18 @@ grpc = ["grpcio (>=1.38.0,<2.0.0) ; python_version < \"3.14\"", "grpcio (>=1.75.
[[package]]
name = "google-cloud-monitoring"
version = "2.29.1"
version = "2.30.0"
description = "Google Cloud Monitoring API client library"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "google_cloud_monitoring-2.29.1-py3-none-any.whl", hash = "sha256:944a57031f20da38617d184d5658c1f938e019e8061f27fd944584831a1b9d5a"},
{file = "google_cloud_monitoring-2.29.1.tar.gz", hash = "sha256:86cac55cdd2608561819d19544fb3c129bbb7dcecc445d8de426e34cd6fa8e49"},
{file = "google_cloud_monitoring-2.30.0-py3-none-any.whl", hash = "sha256:2729f3b88a4798b7757b1d9d31b6cb562bb3544e8173765e4e5cd44d8685b1ed"},
{file = "google_cloud_monitoring-2.30.0.tar.gz", hash = "sha256:a9530aa9aa246c490810dfa7be32d67e8340d19108acc99cbc02d1ed494fba76"},
]
[package.dependencies]
google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras = ["grpc"]}
google-api-core = {version = ">=2.11.0,<3.0.0", extras = ["grpc"]}
google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0"
grpcio = [
{version = ">=1.75.1,<2.0.0", markers = "python_version >= \"3.14\""},
@ -513,10 +529,10 @@ proto-plus = [
{version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""},
{version = ">=1.22.3,<2.0.0"},
]
protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
protobuf = ">=4.25.8,<8.0.0"
[package.extras]
pandas = ["pandas (>=0.23.2)"]
pandas = ["pandas (>=1.1.3)"]
[[package]]
name = "google-cloud-spanner"
@ -550,19 +566,19 @@ libcst = ["libcst (>=0.2.5)"]
[[package]]
name = "googleapis-common-protos"
version = "1.73.0"
version = "1.73.1"
description = "Common protobufs used in Google APIs"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "googleapis_common_protos-1.73.0-py3-none-any.whl", hash = "sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8"},
{file = "googleapis_common_protos-1.73.0.tar.gz", hash = "sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a"},
{file = "googleapis_common_protos-1.73.1-py3-none-any.whl", hash = "sha256:e51f09eb0a43a8602f5a915870972e6b4a394088415c79d79605a46d8e826ee8"},
{file = "googleapis_common_protos-1.73.1.tar.gz", hash = "sha256:13114f0e9d2391756a0194c3a8131974ed7bffb06086569ba193364af59163b6"},
]
[package.dependencies]
grpcio = {version = ">=1.44.0,<2.0.0", optional = true, markers = "extra == \"grpc\""}
protobuf = ">=3.20.2,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0"
protobuf = ">=4.25.8,<8.0.0"
[package.extras]
grpc = ["grpcio (>=1.44.0,<2.0.0)"]
@ -604,96 +620,96 @@ testing = ["protobuf (>=4.21.9)"]
[[package]]
name = "grpcio"
version = "1.78.0"
version = "1.80.0"
description = "HTTP/2-based RPC framework"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "grpcio-1.78.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:7cc47943d524ee0096f973e1081cb8f4f17a4615f2116882a5f1416e4cfe92b5"},
{file = "grpcio-1.78.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:c3f293fdc675ccba4db5a561048cca627b5e7bd1c8a6973ffedabe7d116e22e2"},
{file = "grpcio-1.78.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:10a9a644b5dd5aec3b82b5b0b90d41c0fa94c85ef42cb42cf78a23291ddb5e7d"},
{file = "grpcio-1.78.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4c5533d03a6cbd7f56acfc9cfb44ea64f63d29091e40e44010d34178d392d7eb"},
{file = "grpcio-1.78.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff870aebe9a93a85283837801d35cd5f8814fe2ad01e606861a7fb47c762a2b7"},
{file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:391e93548644e6b2726f1bb84ed60048d4bcc424ce5e4af0843d28ca0b754fec"},
{file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:df2c8f3141f7cbd112a6ebbd760290b5849cda01884554f7c67acc14e7b1758a"},
{file = "grpcio-1.78.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bd8cb8026e5f5b50498a3c4f196f57f9db344dad829ffae16b82e4fdbaea2813"},
{file = "grpcio-1.78.0-cp310-cp310-win32.whl", hash = "sha256:f8dff3d9777e5d2703a962ee5c286c239bf0ba173877cc68dc02c17d042e29de"},
{file = "grpcio-1.78.0-cp310-cp310-win_amd64.whl", hash = "sha256:94f95cf5d532d0e717eed4fc1810e8e6eded04621342ec54c89a7c2f14b581bf"},
{file = "grpcio-1.78.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6"},
{file = "grpcio-1.78.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e"},
{file = "grpcio-1.78.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911"},
{file = "grpcio-1.78.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e"},
{file = "grpcio-1.78.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303"},
{file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04"},
{file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec"},
{file = "grpcio-1.78.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074"},
{file = "grpcio-1.78.0-cp311-cp311-win32.whl", hash = "sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856"},
{file = "grpcio-1.78.0-cp311-cp311-win_amd64.whl", hash = "sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558"},
{file = "grpcio-1.78.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97"},
{file = "grpcio-1.78.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e"},
{file = "grpcio-1.78.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996"},
{file = "grpcio-1.78.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7"},
{file = "grpcio-1.78.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9"},
{file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383"},
{file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6"},
{file = "grpcio-1.78.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce"},
{file = "grpcio-1.78.0-cp312-cp312-win32.whl", hash = "sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68"},
{file = "grpcio-1.78.0-cp312-cp312-win_amd64.whl", hash = "sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e"},
{file = "grpcio-1.78.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b"},
{file = "grpcio-1.78.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a"},
{file = "grpcio-1.78.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84"},
{file = "grpcio-1.78.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb"},
{file = "grpcio-1.78.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5"},
{file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9"},
{file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702"},
{file = "grpcio-1.78.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20"},
{file = "grpcio-1.78.0-cp313-cp313-win32.whl", hash = "sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670"},
{file = "grpcio-1.78.0-cp313-cp313-win_amd64.whl", hash = "sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4"},
{file = "grpcio-1.78.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e"},
{file = "grpcio-1.78.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f"},
{file = "grpcio-1.78.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724"},
{file = "grpcio-1.78.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b"},
{file = "grpcio-1.78.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7"},
{file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452"},
{file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127"},
{file = "grpcio-1.78.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65"},
{file = "grpcio-1.78.0-cp314-cp314-win32.whl", hash = "sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c"},
{file = "grpcio-1.78.0-cp314-cp314-win_amd64.whl", hash = "sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb"},
{file = "grpcio-1.78.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:86f85dd7c947baa707078a236288a289044836d4b640962018ceb9cd1f899af5"},
{file = "grpcio-1.78.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:de8cb00d1483a412a06394b8303feec5dcb3b55f81d83aa216dbb6a0b86a94f5"},
{file = "grpcio-1.78.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e888474dee2f59ff68130f8a397792d8cb8e17e6b3434339657ba4ee90845a8c"},
{file = "grpcio-1.78.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:86ce2371bfd7f212cf60d8517e5e854475c2c43ce14aa910e136ace72c6db6c1"},
{file = "grpcio-1.78.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0c689c02947d636bc7fab3e30cc3a3445cca99c834dfb77cd4a6cabfc1c5597"},
{file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ce7599575eeb25c0f4dc1be59cada6219f3b56176f799627f44088b21381a28a"},
{file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:684083fd383e9dc04c794adb838d4faea08b291ce81f64ecd08e4577c7398adf"},
{file = "grpcio-1.78.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:ab399ef5e3cd2a721b1038a0f3021001f19c5ab279f145e1146bb0b9f1b2b12c"},
{file = "grpcio-1.78.0-cp39-cp39-win32.whl", hash = "sha256:f3d6379493e18ad4d39537a82371c5281e153e963cecb13f953ebac155756525"},
{file = "grpcio-1.78.0-cp39-cp39-win_amd64.whl", hash = "sha256:5361a0630a7fdb58a6a97638ab70e1dae2893c4d08d7aba64ded28bb9e7a29df"},
{file = "grpcio-1.78.0.tar.gz", hash = "sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5"},
{file = "grpcio-1.80.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:886457a7768e408cdce226ad1ca67d2958917d306523a0e21e1a2fdaa75c9c9c"},
{file = "grpcio-1.80.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:7b641fc3f1dc647bfd80bd713addc68f6d145956f64677e56d9ebafc0bd72388"},
{file = "grpcio-1.80.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:33eb763f18f006dc7fee1e69831d38d23f5eccd15b2e0f92a13ee1d9242e5e02"},
{file = "grpcio-1.80.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:52d143637e3872633fc7dd7c3c6a1c84e396b359f3a72e215f8bf69fd82084fc"},
{file = "grpcio-1.80.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c51bf8ac4575af2e0678bccfb07e47321fc7acb5049b4482832c5c195e04e13a"},
{file = "grpcio-1.80.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:50a9871536d71c4fba24ee856abc03a87764570f0c457dd8db0b4018f379fed9"},
{file = "grpcio-1.80.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:a72d84ad0514db063e21887fbacd1fd7acb4d494a564cae22227cd45c7fbf199"},
{file = "grpcio-1.80.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f7691a6788ad9196872f95716df5bc643ebba13c97140b7a5ee5c8e75d1dea81"},
{file = "grpcio-1.80.0-cp310-cp310-win32.whl", hash = "sha256:46c2390b59d67f84e882694d489f5b45707c657832d7934859ceb8c33f467069"},
{file = "grpcio-1.80.0-cp310-cp310-win_amd64.whl", hash = "sha256:dc053420fc75749c961e2a4c906398d7c15725d36ccc04ae6d16093167223b58"},
{file = "grpcio-1.80.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:dfab85db094068ff42e2a3563f60ab3dddcc9d6488a35abf0132daec13209c8a"},
{file = "grpcio-1.80.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5c07e82e822e1161354e32da2662f741a4944ea955f9f580ec8fb409dd6f6060"},
{file = "grpcio-1.80.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba0915d51fd4ced2db5ff719f84e270afe0e2d4c45a7bdb1e8d036e4502928c2"},
{file = "grpcio-1.80.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3cb8130ba457d2aa09fa6b7c3ed6b6e4e6a2685fce63cb803d479576c4d80e21"},
{file = "grpcio-1.80.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:09e5e478b3d14afd23f12e49e8b44c8684ac3c5f08561c43a5b9691c54d136ab"},
{file = "grpcio-1.80.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:00168469238b022500e486c1c33916acf2f2a9b2c022202cf8a1885d2e3073c1"},
{file = "grpcio-1.80.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8502122a3cc1714038e39a0b071acb1207ca7844208d5ea0d091317555ee7106"},
{file = "grpcio-1.80.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ce1794f4ea6cc3ca29463f42d665c32ba1b964b48958a66497917fe9069f26e6"},
{file = "grpcio-1.80.0-cp311-cp311-win32.whl", hash = "sha256:51b4a7189b0bef2aa30adce3c78f09c83526cf3dddb24c6a96555e3b97340440"},
{file = "grpcio-1.80.0-cp311-cp311-win_amd64.whl", hash = "sha256:02e64bb0bb2da14d947a49e6f120a75e947250aebe65f9629b62bb1f5c14e6e9"},
{file = "grpcio-1.80.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:c624cc9f1008361014378c9d776de7182b11fe8b2e5a81bc69f23a295f2a1ad0"},
{file = "grpcio-1.80.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f49eddcac43c3bf350c0385366a58f36bed8cc2c0ec35ef7b74b49e56552c0c2"},
{file = "grpcio-1.80.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d334591df610ab94714048e0d5b4f3dd5ad1bee74dfec11eee344220077a79de"},
{file = "grpcio-1.80.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:0cb517eb1d0d0aaf1d87af7cc5b801d686557c1d88b2619f5e31fab3c2315921"},
{file = "grpcio-1.80.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4e78c4ac0d97dc2e569b2f4bcbbb447491167cb358d1a389fc4af71ab6f70411"},
{file = "grpcio-1.80.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2ed770b4c06984f3b47eb0517b1c69ad0b84ef3f40128f51448433be904634cd"},
{file = "grpcio-1.80.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:256507e2f524092f1473071a05e65a5b10d84b82e3ff24c5b571513cfaa61e2f"},
{file = "grpcio-1.80.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9a6284a5d907c37db53350645567c522be314bac859a64a7a5ca63b77bb7958f"},
{file = "grpcio-1.80.0-cp312-cp312-win32.whl", hash = "sha256:c71309cfce2f22be26aa4a847357c502db6c621f1a49825ae98aa0907595b193"},
{file = "grpcio-1.80.0-cp312-cp312-win_amd64.whl", hash = "sha256:9fe648599c0e37594c4809d81a9e77bd138cc82eb8baa71b6a86af65426723ff"},
{file = "grpcio-1.80.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:e9e408fc016dffd20661f0126c53d8a31c2821b5c13c5d67a0f5ed5de93319ad"},
{file = "grpcio-1.80.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:92d787312e613754d4d8b9ca6d3297e69994a7912a32fa38c4c4e01c272974b0"},
{file = "grpcio-1.80.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8ac393b58aa16991a2f1144ec578084d544038c12242da3a215966b512904d0f"},
{file = "grpcio-1.80.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:68e5851ac4b9afe07e7f84483803ad167852570d65326b34d54ca560bfa53fb6"},
{file = "grpcio-1.80.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:873ff5d17d68992ef6605330127425d2fc4e77e612fa3c3e0ed4e668685e3140"},
{file = "grpcio-1.80.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2bea16af2750fd0a899bf1abd9022244418b55d1f37da2202249ba4ba673838d"},
{file = "grpcio-1.80.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ba0db34f7e1d803a878284cd70e4c63cb6ae2510ba51937bf8f45ba997cefcf7"},
{file = "grpcio-1.80.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8eb613f02d34721f1acf3626dfdb3545bd3c8505b0e52bf8b5710a28d02e8aa7"},
{file = "grpcio-1.80.0-cp313-cp313-win32.whl", hash = "sha256:93b6f823810720912fd131f561f91f5fed0fda372b6b7028a2681b8194d5d294"},
{file = "grpcio-1.80.0-cp313-cp313-win_amd64.whl", hash = "sha256:e172cf795a3ba5246d3529e4d34c53db70e888fa582a8ffebd2e6e48bc0cba50"},
{file = "grpcio-1.80.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:3d4147a97c8344d065d01bbf8b6acec2cf86fb0400d40696c8bdad34a64ffc0e"},
{file = "grpcio-1.80.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d8e11f167935b3eb089ac9038e1a063e6d7dbe995c0bb4a661e614583352e76f"},
{file = "grpcio-1.80.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f14b618fc30de822681ee986cfdcc2d9327229dc4c98aed16896761cacd468b9"},
{file = "grpcio-1.80.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4ed39fbdcf9b87370f6e8df4e39ca7b38b3e5e9d1b0013c7b6be9639d6578d14"},
{file = "grpcio-1.80.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2dcc70e9f0ba987526e8e8603a610fb4f460e42899e74e7a518bf3c68fe1bf05"},
{file = "grpcio-1.80.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:448c884b668b868562b1bda833c5fce6272d26e1926ec46747cda05741d302c1"},
{file = "grpcio-1.80.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a1dc80fe55685b4a543555e6eef975303b36c8db1023b1599b094b92aa77965f"},
{file = "grpcio-1.80.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:31b9ac4ad1aa28ffee5503821fafd09e4da0a261ce1c1281c6c8da0423c83b6e"},
{file = "grpcio-1.80.0-cp314-cp314-win32.whl", hash = "sha256:367ce30ba67d05e0592470428f0ec1c31714cab9ef19b8f2e37be1f4c7d32fae"},
{file = "grpcio-1.80.0-cp314-cp314-win_amd64.whl", hash = "sha256:3b01e1f5464c583d2f567b2e46ff0d516ef979978f72091fd81f5ab7fa6e2e7f"},
{file = "grpcio-1.80.0-cp39-cp39-linux_armv7l.whl", hash = "sha256:aacdfb4ed3eb919ca997504d27e03d5dba403c85130b8ed450308590a738f7a4"},
{file = "grpcio-1.80.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:a361c20ec1ccd3c3953d20fb6d7b4125093bdd10dff44c5e2bbb39e58917cedc"},
{file = "grpcio-1.80.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:43168871f170d1e4ed16ae03d10cd21efa29f190e710a624cee7e5ae07da6f4f"},
{file = "grpcio-1.80.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1b97cd29a8eda100b559b455331c487a80915b6ea6bd91cf3e89836c4ee8d957"},
{file = "grpcio-1.80.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bac1d573dfa84ce59a5547073e28fa7326d53352adda6912e362da0b917fcef4"},
{file = "grpcio-1.80.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4560cf0e86514595dbbd330cd65b7afad4b5c4b8c4905c041cfffa138d45e6fd"},
{file = "grpcio-1.80.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec0a592e926071b4abad50c1495cd0d0d513324b3ff5e7267067c33ba27506e4"},
{file = "grpcio-1.80.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:deb10a1528473c11f72a0939eed36d83e847d7cbb63e8cc5611fb7a912d38614"},
{file = "grpcio-1.80.0-cp39-cp39-win32.whl", hash = "sha256:627fb7312171cdc52828bd6fac8d7028ff2a64b89f1957b6f3416caa2218d141"},
{file = "grpcio-1.80.0-cp39-cp39-win_amd64.whl", hash = "sha256:05d55e1798756282cddd52d56c896b3e7d673e3a8798c2f1cd05ba249a3bb4de"},
{file = "grpcio-1.80.0.tar.gz", hash = "sha256:29aca15edd0688c22ba01d7cc01cb000d72b2033f4a3c72a81a19b56fd143257"},
]
[package.dependencies]
typing-extensions = ">=4.12,<5.0"
[package.extras]
protobuf = ["grpcio-tools (>=1.78.0)"]
protobuf = ["grpcio-tools (>=1.80.0)"]
[[package]]
name = "grpcio-status"
version = "1.78.0"
version = "1.80.0"
description = "Status proto mapping for gRPC"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "grpcio_status-1.78.0-py3-none-any.whl", hash = "sha256:b492b693d4bf27b47a6c32590701724f1d3b9444b36491878fb71f6208857f34"},
{file = "grpcio_status-1.78.0.tar.gz", hash = "sha256:a34cfd28101bfea84b5aa0f936b4b423019e9213882907166af6b3bddc59e189"},
{file = "grpcio_status-1.80.0-py3-none-any.whl", hash = "sha256:4b56990363af50dbf2c2ebb80f1967185c07d87aa25aa2bea45ddb75fc181dbe"},
{file = "grpcio_status-1.80.0.tar.gz", hash = "sha256:df73802a4c89a3ea88aa2aff971e886fccce162bc2e6511408b3d67a144381cd"},
]
[package.dependencies]
googleapis-common-protos = ">=1.5.5"
grpcio = ">=1.78.0"
grpcio = ">=1.80.0"
protobuf = ">=6.31.1,<7.0.0"
[[package]]
@ -1224,52 +1240,52 @@ testing = ["coverage", "pytest", "pytest-benchmark"]
[[package]]
name = "proto-plus"
version = "1.27.1"
version = "1.27.2"
description = "Beautiful, Pythonic protocol buffers"
optional = false
python-versions = ">=3.7"
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "proto_plus-1.27.1-py3-none-any.whl", hash = "sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc"},
{file = "proto_plus-1.27.1.tar.gz", hash = "sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147"},
{file = "proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718"},
{file = "proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24"},
]
[package.dependencies]
protobuf = ">=3.19.0,<7.0.0"
protobuf = ">=4.25.8,<8.0.0"
[package.extras]
testing = ["google-api-core (>=1.31.5)"]
[[package]]
name = "protobuf"
version = "6.33.5"
version = "6.33.6"
description = ""
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b"},
{file = "protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c"},
{file = "protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5"},
{file = "protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190"},
{file = "protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd"},
{file = "protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0"},
{file = "protobuf-6.33.5-cp39-cp39-win32.whl", hash = "sha256:a3157e62729aafb8df6da2c03aa5c0937c7266c626ce11a278b6eb7963c4e37c"},
{file = "protobuf-6.33.5-cp39-cp39-win_amd64.whl", hash = "sha256:8f04fa32763dcdb4973d537d6b54e615cc61108c7cb38fe59310c3192d29510a"},
{file = "protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02"},
{file = "protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c"},
{file = "protobuf-6.33.6-cp310-abi3-win32.whl", hash = "sha256:7d29d9b65f8afef196f8334e80d6bc1d5d4adedb449971fefd3723824e6e77d3"},
{file = "protobuf-6.33.6-cp310-abi3-win_amd64.whl", hash = "sha256:0cd27b587afca21b7cfa59a74dcbd48a50f0a6400cfb59391340ad729d91d326"},
{file = "protobuf-6.33.6-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:9720e6961b251bde64edfdab7d500725a2af5280f3f4c87e57c0208376aa8c3a"},
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:e2afbae9b8e1825e3529f88d514754e094278bb95eadc0e199751cdd9a2e82a2"},
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:c96c37eec15086b79762ed265d59ab204dabc53056e3443e702d2681f4b39ce3"},
{file = "protobuf-6.33.6-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:e9db7e292e0ab79dd108d7f1a94fe31601ce1ee3f7b79e0692043423020b0593"},
{file = "protobuf-6.33.6-cp39-cp39-win32.whl", hash = "sha256:bd56799fb262994b2c2faa1799693c95cc2e22c62f56fb43af311cae45d26f0e"},
{file = "protobuf-6.33.6-cp39-cp39-win_amd64.whl", hash = "sha256:f443a394af5ed23672bc6c486be138628fbe5c651ccbc536873d7da23d1868cf"},
{file = "protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901"},
{file = "protobuf-6.33.6.tar.gz", hash = "sha256:a6768d25248312c297558af96a9f9c929e8c4cee0659cb07e780731095f38135"},
]
[[package]]
name = "pyasn1"
version = "0.6.2"
version = "0.6.3"
description = "Pure-Python implementation of ASN.1 types and DER/BER/CER codecs (X.208)"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf"},
{file = "pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b"},
{file = "pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde"},
{file = "pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf"},
]
[[package]]
@ -1320,14 +1336,14 @@ toml = ["tomli (>=1.2.3) ; python_version < \"3.11\""]
[[package]]
name = "pygments"
version = "2.19.2"
version = "2.20.0"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
{file = "pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"},
{file = "pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f"},
]
[package.extras]
@ -1495,25 +1511,25 @@ files = [
[[package]]
name = "requests"
version = "2.32.5"
version = "2.33.1"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.9"
python-versions = ">=3.10"
groups = ["main"]
files = [
{file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"},
{file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"},
{file = "requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a"},
{file = "requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517"},
]
[package.dependencies]
certifi = ">=2017.4.17"
certifi = ">=2023.5.7"
charset_normalizer = ">=2,<4"
idna = ">=2.5,<4"
urllib3 = ">=1.21.1,<3"
urllib3 = ">=1.26,<3"
[package.extras]
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
use-chardet-on-py3 = ["chardet (>=3.0.2,<8)"]
[[package]]
name = "rich"
@ -1536,30 +1552,30 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.15.6"
version = "0.15.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
groups = ["dev"]
files = [
{file = "ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff"},
{file = "ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3"},
{file = "ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab"},
{file = "ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e"},
{file = "ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb"},
{file = "ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0"},
{file = "ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c"},
{file = "ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406"},
{file = "ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837"},
{file = "ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4"},
{file = "ruff-0.15.8-py3-none-linux_armv6l.whl", hash = "sha256:cbe05adeba76d58162762d6b239c9056f1a15a55bd4b346cfd21e26cd6ad7bc7"},
{file = "ruff-0.15.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d3e3d0b6ba8dca1b7ef9ab80a28e840a20070c4b62e56d675c24f366ef330570"},
{file = "ruff-0.15.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6ee3ae5c65a42f273f126686353f2e08ff29927b7b7e203b711514370d500de3"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdce027ada77baa448077ccc6ebb2fa9c3c62fd110d8659d601cf2f475858d94"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12e617fc01a95e5821648a6df341d80456bd627bfab8a829f7cfc26a14a4b4a3"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:432701303b26416d22ba696c39f2c6f12499b89093b61360abc34bcc9bf07762"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d910ae974b7a06a33a057cb87d2a10792a3b2b3b35e33d2699fdf63ec8f6b17a"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2033f963c43949d51e6fdccd3946633c6b37c484f5f98c3035f49c27395a8ab8"},
{file = "ruff-0.15.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f29b989a55572fb885b77464cf24af05500806ab4edf9a0fd8977f9759d85b1"},
{file = "ruff-0.15.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:ac51d486bf457cdc985a412fb1801b2dfd1bd8838372fc55de64b1510eff4bec"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:c9861eb959edab053c10ad62c278835ee69ca527b6dcd72b47d5c1e5648964f6"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8d9a5b8ea13f26ae90838afc33f91b547e61b794865374f114f349e9036835fb"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c2a33a529fb3cbc23a7124b5c6ff121e4d6228029cba374777bd7649cc8598b8"},
{file = "ruff-0.15.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:75e5cd06b1cf3f47a3996cfc999226b19aa92e7cce682dcd62f80d7035f98f49"},
{file = "ruff-0.15.8-py3-none-win32.whl", hash = "sha256:bc1f0a51254ba21767bfa9a8b5013ca8149dcf38092e6a9eb704d876de94dc34"},
{file = "ruff-0.15.8-py3-none-win_amd64.whl", hash = "sha256:04f79eff02a72db209d47d665ba7ebcad609d8918a134f86cb13dd132159fc89"},
{file = "ruff-0.15.8-py3-none-win_arm64.whl", hash = "sha256:cf891fa8e3bb430c0e7fac93851a5978fc99c8fa2c053b57b118972866f8e5f2"},
{file = "ruff-0.15.8.tar.gz", hash = "sha256:995f11f63597ee362130d1d5a327a87cb6f3f5eae3094c620bcc632329a4d26e"},
]
[[package]]

View File

@ -3,6 +3,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Script to purge expired TTL rows from the Spanner database."""
import argparse
import logging
@ -12,7 +13,7 @@ from datetime import datetime
from typing import Any
from google.cloud import spanner # type: ignore[attr-defined]
from google.cloud import spanner
from google.cloud.spanner_v1 import param_types as param_types
from statsd.defaults.env import statsd
@ -134,7 +135,7 @@ def spanner_purge(args: argparse.Namespace) -> None:
# IN PARENT batches ON DELETE CASCADE)
(batch_query, params, types) = add_conditions(
args,
f"DELETE FROM batches WHERE {expiry_condition}",
f"DELETE FROM batches WHERE {expiry_condition}", # nosec B608
prefix,
)
deleter(
@ -150,7 +151,9 @@ def spanner_purge(args: argparse.Namespace) -> None:
if args.mode in ["bsos", "both"]:
# Delete BSOs
(bso_query, params, types) = add_conditions(
args, f"DELETE FROM bsos WHERE {expiry_condition}", prefix
args,
f"DELETE FROM bsos WHERE {expiry_condition}", # nosec B608
prefix,
)
deleter(
database,

View File

@ -1,3 +1,5 @@
"""Tests for the Spanner count_expired_rows module."""
import logging
from unittest.mock import MagicMock

View File

@ -1,3 +1,5 @@
"""Tests for the Spanner TTL purge utility."""
import argparse
from types import SimpleNamespace
from unittest import mock

View File

@ -1,3 +1,5 @@
"""Tests for the Spanner utilities module."""
import pytest
from tools.spanner.utils import ids_from_env

View File

@ -3,6 +3,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Utility functions and types for Spanner CLI scripts."""
import os
from enum import auto, Enum
@ -17,6 +18,8 @@ In this context, should always point to spanner for these scripts.
class Mode(Enum):
"""Enumeration of DSN resolution modes."""
URL = auto()
ENV_VAR = auto()

View File

@ -8,6 +8,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
"""Script to preload batch data into the Spanner database for optimization."""
import random
import string
@ -79,6 +80,7 @@ PAYLOAD = "".join(
def load(instance, db, coll_id, name):
"""Load a batch of test records into Spanner for the given user and collection."""
fxa_uid = "DEADBEEF" + uuid.uuid4().hex[8:]
fxa_kid = "{:013d}-{}".format(22, fxa_uid)
print(f"{name} -> Loading {fxa_uid} {fxa_kid}")
@ -177,6 +179,7 @@ def load(instance, db, coll_id, name):
def loader():
"""Load records into Spanner for the current thread's user."""
# Prefix uaids for easy filtering later
# Each loader thread gets it's own fake user to prevent some hotspot
# issues.
@ -187,6 +190,7 @@ def loader():
def main():
"""Start all loader threads to populate the Spanner database."""
for c in range(THREAD_COUNT):
print(f"Starting thread {c}")
t = threading.Thread(name=f"loader_{c}", target=loader)

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python3
"""
Cleanup orphaned accounts from interrupted tests.
"""Cleanup orphaned accounts from interrupted tests.
Usage:
python cleanup_orphaned_accounts.py
@ -8,7 +7,7 @@ Usage:
import json
import os
from typing import Any
from typing import Any, cast
from fxa.core import Client
from fxa.errors import ClientError, ServerError
@ -29,7 +28,7 @@ def load_tracked_accounts() -> list[dict[str, Any]]:
try:
with open(ACCT_TRACKING_FILE, "r") as f:
return json.load(f)
return cast(list[dict[str, Any]], json.load(f))
except (json.JSONDecodeError, IOError) as e:
print(f"Warning: Could not load tracking file: {e}")
return []

View File

@ -1,3 +1,5 @@
"""Print a public RSA key as a JSON Web Key (JWK)."""
import sys
from authlib.jose import JsonWebKey

File diff suppressed because it is too large Load Diff

View File

@ -78,7 +78,7 @@ def _track_account_creation(email: str, password: str, fxa_uid: str) -> None:
with open(_ACCT_TRACKING_FILE, "w") as f:
json.dump(accounts, f, indent=2)
except Exception:
except Exception: # nosec B110
# continue with tests
pass
@ -105,7 +105,7 @@ def _remove_account_from_tracking(email: str) -> None:
with open(_ACCT_TRACKING_FILE, "w") as f:
json.dump(accounts, f, indent=2)
except Exception:
except Exception: # nosec B110
pass
@ -157,7 +157,7 @@ def _create_self_signed_jwt(
# sign JWT
oauth_token = jwt.encode(
payload,
private_key,
private_key, # type: ignore[arg-type]
algorithm=OAUTH_JWT_ALGORITHM,
headers={"typ": "application/at+jwt"},
)
@ -189,7 +189,7 @@ def _is_oauth_token_expired(oauth_token: str, buffer_seconds: int = 300) -> bool
return False
current_time = time.time()
return current_time >= (exp - buffer_seconds)
return bool(current_time >= (exp - buffer_seconds))
except Exception:
return True

View File

@ -65,7 +65,7 @@ class StorageClient(object):
url = self.endpoint_url + path
if params is not None:
url += "?" + urlencode(params)
return url
return str(url)
def __repr__(self):
"""Return string representation of the client.

View File

@ -0,0 +1 @@
"""Tokenserver tools package for syncstorage-rs."""

View File

@ -2,11 +2,7 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Script to add a new node to the system.
"""
"""Script to add a new node to the system."""
import logging
import optparse
@ -33,9 +29,9 @@ def add_node(node, capacity, **kwds):
def main(args=None):
"""Main entry-point for running this script.
"""Run the add_node script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the add_node() function.
"""
usage = "usage: %prog [options] node_name capacity"

View File

@ -1,15 +1,12 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
"""Script to allocate a specific user to a node.
Script to allocate a specific user to a node.
This script allocates the specified user to a node. A particular node
Allocate the specified user to a node. A particular node
may be specified, or the best available node used by default.
The allocated node is printed to stdout.
"""
import logging
@ -23,6 +20,7 @@ logger = logging.getLogger("tokenserver.scripts.allocate_user")
def allocate_user(email, node=None):
"""Allocate a node for the given user, or update the user's existing node."""
logger.info("Allocating node for user %s", email)
try:
database = Database()
@ -40,9 +38,9 @@ def allocate_user(email, node=None):
def main(args=None):
"""Main entry-point for running this script.
"""Run the allocate_user script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the allocate_user() function.
"""
usage = "usage: %prog [options] email [node_name]"

View File

@ -1,3 +1,5 @@
"""Pytest configuration for tokenserver tests, setting up the module path."""
import sys
import os

View File

@ -1,11 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Script to emit total-user-count metrics for exec dashboard.
"""
"""Script to emit total-user-count metrics for exec dashboard."""
import json
import logging
@ -25,13 +21,18 @@ ZERO = timedelta(0)
class UTC(tzinfo):
"""UTC timezone implementation."""
def utcoffset(self, dt):
"""Return the UTC offset, which is zero."""
return ZERO
def tzname(self, dt):
"""Return the timezone name string."""
return "UTC"
def dst(self, dt):
"""Return the DST offset, which is zero."""
return ZERO
@ -39,6 +40,7 @@ utc = UTC()
def count_users(outfile, timestamp=None):
"""Count total users and write a JSON metrics record to outfile."""
if timestamp is None:
ts = time.gmtime()
midnight = (ts[0], ts[1], ts[2], 0, 0, 0, ts[6], ts[7], ts[8])
@ -62,10 +64,10 @@ def count_users(outfile, timestamp=None):
def main(args=None):
"""Main entry-point for running this script.
"""Run the count_users script with the given arguments.
This function parses command-line arguments and passes them on
to the add_node() function.
Parse command-line arguments and pass them on
to the count_users() function.
"""
usage = "usage: %prog [options]"
descr = "Count total users in the tokenserver database"

View File

@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Database access layer for the tokenserver."""
import math
import os
@ -258,6 +259,8 @@ SERVICE_NAME = "sync-1.5"
class Database:
"""Tokenserver database access class."""
def __init__(self):
# Formatted for rust diesel with a "postgres" dialect, whereas
# sqlalchemy uses "postgresql" instead
@ -277,9 +280,11 @@ class Database:
return self.database.execute(*args, **kwds)
def close(self):
"""Close the database connection."""
self.database.close()
def get_user(self, email):
"""Return the current user record for the given email address."""
params = {"service": self._get_service_id(SERVICE_NAME), "email": email}
res = self._execute_sql(_GET_USER_RECORDS, **params)
try:
@ -340,6 +345,7 @@ class Database:
node=None,
timestamp=None,
):
"""Allocate a node for a new user and return the user record."""
if timestamp is None:
timestamp = get_timestamp()
if node is None:
@ -384,6 +390,7 @@ class Database:
def update_user(
self, user, generation=None, client_state=None, keys_changed_at=None, node=None
):
"""Update an existing user record with new generation, state, or node."""
if client_state is None and node is None:
# No need for a node-reassignment, just update the row in place.
# Note that if we're changing keys_changed_at without changing
@ -465,6 +472,7 @@ class Database:
self.replace_user_records(user["email"], now)
def retire_user(self, email):
"""Retire a user by setting generation to MAX_GENERATION."""
now = get_timestamp()
params = {"email": email, "timestamp": now, "generation": MAX_GENERATION}
# Pass through explicit engine to help with sharded implementation,
@ -473,6 +481,7 @@ class Database:
res.close()
def count_users(self, timestamp=None):
"""Return the count of users created before the given timestamp."""
if timestamp is None:
timestamp = get_timestamp()
res = self._execute_sql(_COUNT_USER_RECORDS, timestamp=timestamp)
@ -633,10 +642,9 @@ class Database:
if "nodeid" in kwds:
cols.append("id")
args.append(":nodeid")
query = """
insert into nodes ({cols})
values ({args})
""".format(cols=", ".join(cols), args=", ".join(args))
query = "insert into nodes ({cols}) values ({args})".format( # nosec B608
cols=", ".join(cols), args=", ".join(args)
)
res = self._execute_sql(
sqltext(query),
nodeid=kwds.get("nodeid"),
@ -651,7 +659,7 @@ class Database:
res.close()
def update_node(self, node, **kwds):
"""Updates node fields in the db."""
"""Update node fields in the database."""
values = {}
cols = NODE_FIELDS & kwds.keys()
for col in NODE_FIELDS:
@ -723,8 +731,9 @@ class Database:
res.close()
def get_best_node(self):
"""Returns the 'least loaded' node currently available, increments the
active count on that node, and decrements the slots currently available
"""Return the least loaded node, incrementing its active count.
Decrement the slots currently available on the chosen node.
"""
# The spanner node is the best node.
if self.spanner_node:
@ -775,6 +784,7 @@ class Database:
return nodeid, node
def get_node(self, node):
"""Return the node record for the given node URL."""
if node is None:
raise Exception("NONE node")
res = self._execute_sql(
@ -788,6 +798,7 @@ class Database:
# somewhat simplified version that just gets the one Spanner node.
def get_spanner_node(self, node):
"""Return the node URL for the given Spanner node ID."""
res = self._execute_sql(_GET_SPANNER_NODE, id=node)
row = res.fetchone()
res.close()

View File

@ -0,0 +1 @@
"""Load tests package for the tokenserver."""

View File

@ -1,3 +1,5 @@
"""Print a public RSA key as a JSON Web Key (JWK)."""
import sys
from authlib.jose import JsonWebKey

View File

@ -1,3 +1,5 @@
"""Locust load test file for the tokenserver OAuth endpoints."""
import binascii
import os
from base64 import urlsafe_b64encode as b64encode
@ -33,6 +35,8 @@ VALID_OAUTH_PRIVATE_KEY = private_key = serialization.load_pem_private_key(
class TokenserverTestUser(HttpUser):
"""Simulate a single Tokenserver user making sporadic requests during a load test."""
# An instance of this class represents a single Tokenserver user. Instances
# will live for the entire duration of the load test. Based on the
# `wait_time` class variable and the `@task` decorators, each user will
@ -41,6 +45,7 @@ class TokenserverTestUser(HttpUser):
wait_time = between(1, 5)
def __init__(self, *args, **kwargs):
"""Initialize the test user with a unique FxA UID and client state."""
super().__init__(*args, **kwargs)
# Keep track of this user's generation number.
self.generation_counter = 0
@ -54,18 +59,21 @@ class TokenserverTestUser(HttpUser):
@task(3000)
def test_oauth_success(self):
"""Test a successful OAuth token exchange."""
token = self._make_oauth_token(self.email)
self._do_token_exchange_via_oauth(token)
@task(100)
def test_invalid_oauth(self):
"""Test that an invalid OAuth token returns a 401 error."""
token = self._make_oauth_token(self.email, key=INVALID_OAUTH_PRIVATE_KEY)
self._do_token_exchange_via_oauth(token, status=401)
@task(100)
def test_invalid_oauth_scope(self):
"""Test that an OAuth token with an invalid scope returns a 401 error."""
token = self._make_oauth_token(
self.email,
scope="unrelated scopes",
@ -75,6 +83,7 @@ class TokenserverTestUser(HttpUser):
@task(20)
def test_encryption_key_change(self):
"""Test token exchange after an encryption key change."""
# When a user's encryption keys change, the generation number and
# keys_changed_at for the user both increase.
self.generation_counter += 1
@ -87,6 +96,7 @@ class TokenserverTestUser(HttpUser):
@task(20)
def test_password_change(self):
"""Test token exchange after a password change."""
# When a user's password changes, the generation number increases.
self.generation_counter += 1
token = self._make_oauth_token(self.email)
@ -94,9 +104,11 @@ class TokenserverTestUser(HttpUser):
self._do_token_exchange_via_oauth(token)
def _make_oauth_token(self, email, key=VALID_OAUTH_PRIVATE_KEY, **fields):
# For mock oauth tokens, we bundle the desired status code
# and response body into a JSON blob for the mock verifier
# to echo back to us.
"""Create OAuth Token.
For mock oauth tokens, we bundle the desired status code
and response body into a JSON blob for the mock verifier
to echo back to us.
"""
body = {}
if "scope" not in fields:
fields["scope"] = DEFAULT_OAUTH_SCOPE
@ -113,9 +125,11 @@ class TokenserverTestUser(HttpUser):
)
def _make_x_key_id_header(self):
# In practice, the generation number and keys_changed_at may not be
# the same, but for our purposes, making this assumption is sufficient:
# the accuracy of the load test is unaffected.
"""Generate the x-key-id header.
In practice, the generation number and keys_changed_at may not be
the same, but for our purposes, making this assumption is sufficient:
the accuracy of the load test is unaffected.
"""
keys_changed_at = self.generation_counter
raw_client_state = binascii.unhexlify(self.client_state)
client_state = b64encode(raw_client_state).strip(b"=").decode("utf-8")

View File

@ -1,5 +1,6 @@
#! /usr/bin/env python
# script to populate the database with records
"""Script to populate the tokenserver database with test user records."""
import random
import time
@ -56,6 +57,8 @@ _SERVICE_NAME = "sync-1.5"
# :param user_range: the number of users to create
# :param host: the hostname to use when generating users
class PopulateDatabase:
"""Create test users associated with the sync-1.5 service for load testing."""
def __init__(self, sqluri, nodes, user_range, host="loadtest.local"):
engine = create_engine(sqluri)
self.database = engine.execution_options(isolation_level="AUTOCOMMIT").connect()
@ -83,6 +86,7 @@ class PopulateDatabase:
return row.id
def run(self):
"""Populate the database by assigning each user in the range to a random node."""
params = {
"service": self.service_id,
"timestamp": int(time.time() * 1000),
@ -98,13 +102,14 @@ class PopulateDatabase:
def main():
# Read the arguments from the command line and pass them to the
# PopulateDb class.
#
# Example use:
#
# python3 populate-db.py sqlite:////tmp/tokenserver\
# node1,node2,node3,node4,node5,node6 100
"""Run the populate_db script, reading args from the command line.
Read the arguments from the command line and pass them to the
PopulateDb class.
Example use:
python3 populate-db.py sqlite:////tmp/tokenserver\
node1,node2,node3,node4,node5,node6 100
"""
import sys
if len(sys.argv) < 4:

File diff suppressed because it is too large Load Diff

View File

@ -1,11 +1,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
"""Script to process account-related events from an SQS queue.
Script to process account-related events from an SQS queue.
This script polls an SQS queue for events indicating activity on an upstream
Poll an SQS queue for events indicating activity on an upstream
account, as documented here:
https://github.com/mozilla/fxa-auth-server/blob/master/docs/service_notifications.md
@ -165,9 +163,9 @@ def update_generation_number(database, email, generation, metrics=None):
def main(args=None):
"""Main entry-point for running this script.
"""Run the process_account_events script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the process_account_events() function.
"""
usage = "usage: %prog [options] queue_name"

View File

@ -1,18 +1,15 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
"""Script to purge user records that have been replaced.
Script to purge user records that have been replaced.
This script purges any obsolete user records from the database.
Purge any obsolete user records from the database.
Obsolete records are those that have been replaced by a newer record for
the same user.
Note that this is a purely optional administrative task, since replaced records
are handled internally by the assignment backend. But it should help reduce
overheads, improve performance etc if run regularly.
"""
import backoff
@ -21,7 +18,7 @@ import hawkauthlib
import logging
import optparse
import random
import requests
import requests # type: ignore[import-untyped]
import time
import tokenlib
@ -210,20 +207,22 @@ def delete_service_data(
def retry_giveup(e):
"""Return True if the HTTP error is a permanent server error that should stop retrying."""
return 500 <= e.response.status_code < 505
@backoff.on_exception(backoff.expo, requests.HTTPError, giveup=retry_giveup)
def retryable(fn, *args, **kwargs):
"""Call fn with args, retrying on HTTP errors with exponential backoff."""
fn(*args, **kwargs)
def points_to_active(database, replaced_at_row, override_node, metrics=None):
"""Determine if a `replaced_at` user record has the same
generation/client_state as their active record.
"""Determine if a replaced_at user record has the same state as the active record.
In which case issuing a `force`/`override_node` delete (to their current
node) would delete their active data, which should be avoided
Return True if the generation/client_state matches the active record,
in which case issuing a force/override_node delete would delete their
active data and should be avoided.
"""
if override_node and replaced_at_row.node != override_node:
# NOTE: Users who never connected after being migrated could be
@ -256,18 +255,20 @@ class HawkAuth(requests.auth.AuthBase):
"""Hawk-signing auth helper class."""
def __init__(self, token, secret):
"""Initialize with the Hawk token and signing secret."""
self.token = token
self.secret = secret
def __call__(self, req):
"""Sign the request using Hawk authentication."""
hawkauthlib.sign_request(req, self.token, self.secret)
return req
def main(args=None):
"""Main entry-point for running this script.
"""Run the purge_old_records script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the purge_old_records() function.
"""
usage = "usage: %prog [options] secret"

View File

@ -16,7 +16,7 @@ package-mode = false
[tool.poetry.dependencies]
boto = "2.49.0"
hawkauthlib = "2.0.0"
mysqlclient = "2.1.1"
mysqlclient = "^2.1.8"
pyramid = "^1.10.8"
sqlalchemy = "^1.4.46"
testfixtures = "^8.3.0"

View File

@ -1,14 +1,11 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
"""Script to remove a node from the system.
Script to remove a node from the system.
This script nukes any references to the named node - it is removed from
Nuke any references to the named node - it is removed from
the "nodes" table and any users currently assigned to that node have their
assignments cleared.
"""
import logging
@ -45,9 +42,9 @@ def remove_node(node):
def main(args=None):
"""Main entry-point for running this script.
"""Run the remove_node script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the remove_node() function.
"""
usage = "usage: %prog [options] node_name"

View File

@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Tests for the tokenserver database module."""
import time
import unittest
@ -11,7 +12,10 @@ from util import get_timestamp
class TestDatabase(unittest.TestCase):
"""Tests for the tokenserver Database class."""
def setUp(self):
"""Set up test fixtures."""
super(TestDatabase, self).setUp()
self.database = Database()
# Start each test with a blank slate.
@ -28,6 +32,7 @@ class TestDatabase(unittest.TestCase):
self.database.add_node("https://phx12", 100)
def tearDown(self):
"""Tear down test fixtures."""
super(TestDatabase, self).tearDown()
# And clean up at the end, for good measure.
cursor = self.database._execute_sql(("DELETE FROM users"), ())
@ -42,6 +47,7 @@ class TestDatabase(unittest.TestCase):
self.database.close()
def test_node_allocation(self):
"""Test node allocation."""
user = self.database.get_user("test1@example.com")
self.assertEqual(user, None)
@ -53,22 +59,26 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(user["node"], wanted)
def test_allocation_to_least_loaded_node(self):
"""Test allocation to least loaded node."""
self.database.add_node("https://phx13", 100)
user1 = self.database.allocate_user("test1@mozilla.com")
user2 = self.database.allocate_user("test2@mozilla.com")
self.assertNotEqual(user1["node"], user2["node"])
def test_allocation_is_not_allowed_to_downed_nodes(self):
"""Test allocation is not allowed to downed nodes."""
self.database.update_node("https://phx12", downed=True)
with self.assertRaises(Exception):
self.database.allocate_user("test1@mozilla.com")
def test_allocation_is_not_allowed_to_backoff_nodes(self):
"""Test allocation is not allowed to backoff nodes."""
self.database.update_node("https://phx12", backoff=True)
with self.assertRaises(Exception):
self.database.allocate_user("test1@mozilla.com")
def test_update_generation_number(self):
"""Test update generation number."""
user = self.database.allocate_user("test1@example.com")
self.assertEqual(user["generation"], 0)
self.assertEqual(user["client_state"], "")
@ -102,6 +112,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(user["client_state"], "")
def test_update_client_state(self):
"""Test update client state."""
user = self.database.allocate_user("test1@example.com")
self.assertEqual(user["generation"], 0)
self.assertEqual(user["client_state"], "")
@ -154,6 +165,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(set(user["old_client_states"]), set(("", "aaaa")))
def test_user_retirement(self):
"""Test user retirement."""
self.database.allocate_user("test@mozilla.com")
user1 = self.database.get_user("test@mozilla.com")
self.database.retire_user("test@mozilla.com")
@ -161,6 +173,7 @@ class TestDatabase(unittest.TestCase):
self.assertTrue(user2["generation"] > user1["generation"])
def test_cleanup_of_old_records(self):
"""Test cleanup of old records."""
# Create 6 user records for the first user.
# Do a sleep halfway through so we can test use of grace period.
email1 = "test1@mozilla.com"
@ -215,6 +228,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(len(old_records), 4)
def test_node_reassignment_when_records_are_replaced(self):
"""Test node reassignment when records are replaced."""
self.database.allocate_user(
"test@mozilla.com", generation=42, keys_changed_at=12, client_state="aaaa"
)
@ -229,6 +243,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(user2["client_state"], user1["client_state"])
def test_node_reassignment_not_done_for_retired_users(self):
"""Test node reassignment not done for retired users."""
self.database.allocate_user(
"test@mozilla.com", generation=42, client_state="aaaa"
)
@ -240,6 +255,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(user2["client_state"], user2["client_state"])
def test_recovery_from_racy_record_creation(self):
"""Test recovery from racy record creation."""
timestamp = get_timestamp()
# Simulate race for forcing creation of two rows with same timestamp.
user1 = self.database.allocate_user("test@mozilla.com", timestamp=timestamp)
@ -254,6 +270,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(len(old_records), 1)
def test_that_race_recovery_respects_generation_number_monotonicity(self):
"""Test that race recovery respects generation number monotonicity."""
timestamp = get_timestamp()
# Simulate race between clients with different generation numbers,
# in which the out-of-date client gets a higher timestamp.
@ -273,6 +290,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(len(old_records), 1)
def test_node_reassignment_and_removal(self):
"""Test node reassignment and removal."""
NODE1 = "https://phx12"
NODE2 = "https://phx13"
# note that NODE1 is created by default for all tests.
@ -315,6 +333,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(null_node_count, 3)
def test_that_race_recovery_respects_generation_after_reassignment(self):
"""Test that race recovery respects generation after reassignment."""
timestamp = get_timestamp()
# Simulate race between clients with different generation numbers,
# in which the out-of-date client gets a higher timestamp.
@ -335,6 +354,7 @@ class TestDatabase(unittest.TestCase):
self.assertNotEqual(user["uid"], user2["uid"])
def test_that_we_can_allocate_users_to_a_specific_node(self):
"""Test that we can allocate users to a specific node."""
node = "https://phx13"
self.database.add_node(node, 50)
# The new node is not selected by default, because of lower capacity.
@ -345,6 +365,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(user["node"], node)
def test_that_we_can_move_users_to_a_specific_node(self):
"""Test that we can move users to a specific node."""
node = "https://phx13"
self.database.add_node(node, 50)
# The new node is not selected by default, because of lower capacity.
@ -374,6 +395,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(sorted(user["old_client_states"]), ["", "XXX"])
def test_that_record_cleanup_frees_slots_on_the_node(self):
"""Test that record cleanup frees slots on the node."""
node = "https://phx12"
self.database.update_node(node, capacity=10, available=1, current_load=9)
# We should only be able to allocate one more user to that node.
@ -388,6 +410,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(user["node"], node)
def test_gradual_release_of_node_capacity(self):
"""Test gradual release of node capacity."""
node1 = "https://phx12"
self.database.update_node(node1, capacity=8, available=1, current_load=4)
node2 = "https://phx13"
@ -413,6 +436,7 @@ class TestDatabase(unittest.TestCase):
self.database.allocate_user("test7@mozilla.com")
def test_count_users(self):
"""Test count users."""
user = self.database.allocate_user("test1@example.com")
self.assertEqual(self.database.count_users(), 1)
old_timestamp = get_timestamp()
@ -430,6 +454,7 @@ class TestDatabase(unittest.TestCase):
self.assertEqual(self.database.count_users(), 1)
def test_first_seen_at(self):
"""Test first seen at."""
EMAIL = "test1@example.com"
user0 = self.database.allocate_user(EMAIL)
user1 = self.database.get_user(EMAIL)
@ -448,6 +473,7 @@ class TestDatabase(unittest.TestCase):
self.assertNotEqual(user3["first_seen_at"], user2["first_seen_at"])
def test_build_old_range(self):
"""Test build old range."""
params = dict()
sql = self.database._build_old_user_query(None, params)
self.assertTrue(sql.text.find("uid > :start") < 0)

View File

@ -1,6 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Tests for the tokenserver process_account_events module."""
import json
import os
@ -20,20 +21,26 @@ ISS = "example.com"
def message_body(**kwds):
"""Build a JSON-encoded SNS message body from the given keyword arguments."""
return json.dumps({"Message": json.dumps(kwds)})
class ProcessAccountEventsTestCase(unittest.TestCase):
"""Base test case for processing account events."""
def get_ini(self):
"""Return the path to the test INI configuration file."""
return os.path.join(os.path.dirname(__file__), "test_sql.ini")
def setUp(self):
"""Set up test fixtures."""
self.database = Database()
self.database.add_service("sync-1.5", r"{node}/1.5/{uid}")
self.database.add_node("https://phx12", 100)
self.logs = LogCapture()
def tearDown(self):
"""Tear down test fixtures."""
self.logs.uninstall()
testing.tearDown()
@ -55,14 +62,19 @@ class ProcessAccountEventsTestCase(unittest.TestCase):
assert False, "message %r was not logged" % (msg,)
def clearLogs(self):
"""Clear all captured log records."""
del self.logs.records[:]
def process_account_event(self, body):
"""Process the given account event body against the test database."""
process_account_event(self.database, body)
class TestProcessAccountEvents(ProcessAccountEventsTestCase):
"""Tests for processing account events from SNS messages."""
def test_delete_user(self):
"""Test delete user."""
self.database.allocate_user(EMAIL)
user = self.database.get_user(EMAIL)
self.database.update_user(user, client_state="abcdef")
@ -84,6 +96,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertTrue(row["replaced_at"] is not None)
def test_delete_user_by_legacy_uid_format(self):
"""Test delete user by legacy uid format."""
self.database.allocate_user(EMAIL)
user = self.database.get_user(EMAIL)
self.database.update_user(user, client_state="abcdef")
@ -104,6 +117,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertTrue(row["replaced_at"] is not None)
def test_delete_user_who_is_not_in_the_db(self):
"""Test delete user who is not in the db."""
records = list(self.database.get_user_records(EMAIL))
self.assertEqual(len(records), 0)
@ -113,6 +127,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(len(records), 0)
def test_reset_user(self):
"""Test reset user."""
self.database.allocate_user(EMAIL, generation=12)
self.process_account_event(
@ -128,6 +143,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(user["generation"], 42)
def test_reset_user_by_legacy_uid_format(self):
"""Test reset user by legacy uid format."""
self.database.allocate_user(EMAIL, generation=12)
self.process_account_event(
@ -142,6 +158,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(user["generation"], 42)
def test_reset_user_who_is_not_in_the_db(self):
"""Test reset user who is not in the db."""
records = list(self.database.get_user_records(EMAIL))
self.assertEqual(len(records), 0)
@ -158,6 +175,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(len(records), 0)
def test_password_change(self):
"""Test password change."""
self.database.allocate_user(EMAIL, generation=12)
self.process_account_event(
@ -173,6 +191,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(user["generation"], 42)
def test_password_change_user_not_in_db(self):
"""Test password change user not in db."""
records = list(self.database.get_user_records(EMAIL))
self.assertEqual(len(records), 0)
@ -189,6 +208,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(len(records), 0)
def test_malformed_events(self):
"""Test malformed events."""
# Unknown event type.
self.process_account_event(
message_body(
@ -270,6 +290,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.clearLogs()
def test_update_with_no_keys_changed_at(self):
"""Test update with no keys changed at."""
user = self.database.allocate_user(EMAIL, generation=12, keys_changed_at=None)
# These update_user calls previously failed (SYNC-3633)
@ -291,6 +312,7 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
self.assertEqual(user["generation"], 42)
def test_update_with_no_keys_changed_at2(self):
"""Test update with no keys changed at2."""
user = self.database.allocate_user(EMAIL, generation=12, keys_changed_at=None)
# Mark the current record as replaced. This can probably only occur
# during a race condition in row creation
@ -310,11 +332,15 @@ class TestProcessAccountEvents(ProcessAccountEventsTestCase):
class TestProcessAccountEventsForceSpanner(ProcessAccountEventsTestCase):
"""Tests for processing account events with forced Spanner routing."""
def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.database.spanner_node_id = self.database.get_node_id("https://phx12")
def test_delete_user_force_spanner(self):
"""Test delete user force spanner."""
self.database.allocate_user(EMAIL)
user = self.database.get_user(EMAIL)
self.database.update_user(user, client_state="abcdef")

View File

@ -1,6 +1,8 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Tests for the tokenserver purge_old_records script."""
import pytest
import hawkauthlib
@ -15,8 +17,11 @@ from purge_old_records import purge_old_records
class PurgeOldRecordsTestCase(unittest.TestCase):
"""Test case for the purge_old_records functionality."""
@classmethod
def setUpClass(cls):
"""Set up class-level test fixtures."""
cls.service_requests = []
cls.service = make_server("localhost", 0, cls._service_app)
host, port = cls.service.server_address
@ -31,6 +36,7 @@ class PurgeOldRecordsTestCase(unittest.TestCase):
cls.service.RequestHandlerClass.log_request = lambda *a: None
def setUp(self):
"""Set up test fixtures."""
super().setUp()
# Configure the node-assignment backend to talk to our test service.
@ -39,6 +45,7 @@ class PurgeOldRecordsTestCase(unittest.TestCase):
self.database.add_node(self.service_node, 100)
def tearDown(self):
"""Tear down test fixtures."""
cursor = self.database._execute_sql("DELETE FROM users")
cursor.close()
@ -52,6 +59,7 @@ class PurgeOldRecordsTestCase(unittest.TestCase):
@classmethod
def tearDownClass(cls):
"""Tear down class-level test fixtures."""
cls.service.shutdown()
cls.service_thread.join()
@ -71,6 +79,7 @@ class TestPurgeOldRecords(PurgeOldRecordsTestCase):
"""
def test_purging_of_old_user_records(self):
"""Test purging of old user records."""
# Make some old user records.
email = "test@mozilla.com"
user = self.database.allocate_user(email, client_state="aa", generation=123)
@ -119,6 +128,7 @@ class TestPurgeOldRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(user["old_client_states"]), 0)
def test_purging_is_not_done_on_downed_nodes(self):
"""Test purging is not done on downed nodes."""
# Make some old user records.
node_secret = "SECRET"
email = "test@mozilla.com"
@ -142,6 +152,7 @@ class TestPurgeOldRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(self.service_requests), 1)
def test_force(self):
"""Test force."""
# Make some old user records.
node_secret = "SECRET"
email = "test@mozilla.com"
@ -160,6 +171,7 @@ class TestPurgeOldRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(self.service_requests), 1)
def test_dry_run(self):
"""Test dry run."""
# Make some old user records.
node_secret = "SECRET"
email = "test@mozilla.com"
@ -180,12 +192,11 @@ class TestPurgeOldRecords(PurgeOldRecordsTestCase):
@pytest.mark.migration_records
class TestMigrationRecords(PurgeOldRecordsTestCase):
"""Test user records that were migrated from the old MySQL cluster of
syncstorage nodes to a single Spanner node
"""
"""Test user records migrated from old MySQL syncstorage nodes to Spanner."""
@classmethod
def setUpClass(cls):
"""Set up class-level test fixtures."""
super().setUpClass()
cls.spanner_service = make_server("localhost", 0, cls._service_app)
host, port = cls.spanner_service.server_address
@ -196,16 +207,19 @@ class TestMigrationRecords(PurgeOldRecordsTestCase):
@classmethod
def tearDownClass(cls):
"""Tear down class-level test fixtures."""
super().tearDownClass()
cls.spanner_service.shutdown()
cls.spanner_thread.join()
def setUp(self):
"""Set up test fixtures."""
super().setUp()
self.database.add_node(self.downed_node, 100, downed=True)
self.database.add_node(self.spanner_node, 100)
def test_purging_replaced_at(self):
"""Test purging replaced at."""
node_secret = "SECRET"
email = "test@mozilla.com"
user = self.database.allocate_user(email, client_state="aa")
@ -217,6 +231,7 @@ class TestMigrationRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(self.service_requests), 1)
def test_purging_no_override(self):
"""Test purging no override."""
node_secret = "SECRET"
email = "test@mozilla.com"
user = self.database.allocate_user(email, client_state="aa")
@ -231,6 +246,7 @@ class TestMigrationRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(self.service_requests), 1)
def test_purging_override_with_migrated(self):
"""Test purging override with migrated."""
node_secret = "SECRET"
email = "test@mozilla.com"
@ -267,6 +283,7 @@ class TestMigrationRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(self.service_requests), 0)
def test_purging_override_with_migrated_password_change(self):
"""Test purging override with migrated password change."""
node_secret = "SECRET"
email = "test@mozilla.com"
@ -293,6 +310,7 @@ class TestMigrationRecords(PurgeOldRecordsTestCase):
self.assertEqual(len(self.service_requests), 2)
def test_purging_override_null_keys_changed_at(self):
"""Test purging override null keys changed at."""
# Same as test_purging_override_with_migrated but with a null
# keys_changed_at
node_secret = "SECRET"

View File

@ -1,11 +1,12 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""Tests for the tokenserver CLI management scripts."""
import json
import os
import tempfile
import unittest
import uuid
from add_node import main as add_node_script
from allocate_user import main as allocate_user_script
@ -18,10 +19,13 @@ from util import get_timestamp
class TestScripts(unittest.TestCase):
"""Tests for the tokenserver management scripts."""
NODE_ID = 800
NODE_URL = "https://node1"
def setUp(self):
"""Set up test fixtures."""
self.database = Database()
# Start each test with a blank slate.
@ -41,6 +45,7 @@ class TestScripts(unittest.TestCase):
self.database.add_node(self.NODE_URL, 100, id=self.NODE_ID)
def tearDown(self):
"""Tear down test fixtures."""
# And clean up at the end, for good measure.
cursor = self.database._execute_sql("DELETE FROM users")
cursor.close()
@ -54,6 +59,7 @@ class TestScripts(unittest.TestCase):
self.database.close()
def test_add_node(self):
"""Test add node."""
add_node_script(args=["--current-load", "9", "test_node", "100"])
res = self.database.get_node("test_node")
# The node should have the expected attributes
@ -65,6 +71,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(res.service, self.database.service_id)
def test_add_node_with_explicit_available(self):
"""Test add node with explicit available."""
args = ["--current-load", "9", "--available", "5", "test_node", "100"]
add_node_script(args=args)
res = self.database.get_node("test_node")
@ -77,6 +84,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(res.service, self.database.service_id)
def test_add_downed_node(self):
"""Test add downed node."""
add_node_script(args=["--downed", "test_node", "100"])
res = self.database.get_node("test_node")
# The node should have the expected attributes
@ -88,6 +96,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(res.service, self.database.service_id)
def test_add_backoff_node(self):
"""Test add backoff node."""
add_node_script(args=["--backoff", "test_node", "100"])
res = self.database.get_node("test_node")
# The node should have the expected attributes
@ -99,6 +108,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(res.service, self.database.service_id)
def test_allocate_user_user_already_exists(self):
"""Test allocate user user already exists."""
email = "test@test.com"
self.database.allocate_user(email)
node = "https://node2"
@ -112,6 +122,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(count, 1)
def test_allocate_user_given_node(self):
"""Test allocate user given node."""
email = "test@test.com"
node = "https://node2"
self.database.add_node(node, 100)
@ -121,6 +132,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(user["node"], node)
def test_allocate_user_not_given_node(self):
"""Test allocate user not given node."""
email = "test@test.com"
self.database.add_node("https://node2", 100, current_load=10)
self.database.add_node("https://node3", 100, current_load=20)
@ -131,12 +143,14 @@ class TestScripts(unittest.TestCase):
self.assertEqual(user["node"], "https://node1")
def test_count_users(self):
"""Test count users."""
self.database.allocate_user("test1@test.com")
self.database.allocate_user("test2@test.com")
self.database.allocate_user("test3@test.com")
timestamp = get_timestamp()
filename = "/tmp/" + str(uuid.uuid4())
fd, filename = tempfile.mkstemp()
os.close(fd)
try:
count_users_script(
args=["--output", filename, "--timestamp", str(timestamp)]
@ -149,7 +163,8 @@ class TestScripts(unittest.TestCase):
finally:
os.remove(filename)
filename = "/tmp/" + str(uuid.uuid4())
fd, filename = tempfile.mkstemp()
os.close(fd)
try:
args = ["--output", filename, "--timestamp", str(timestamp - 10000)]
count_users_script(args=args)
@ -162,6 +177,7 @@ class TestScripts(unittest.TestCase):
os.remove(filename)
def test_remove_node(self):
"""Test remove node."""
self.database.add_node("https://node2", 100)
self.database.allocate_user("test1@test.com", node="https://node2")
self.database.allocate_user("test2@test.com", node=self.NODE_URL)
@ -182,6 +198,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(user["node"], self.NODE_URL)
def test_unassign_node(self):
"""Test unassign node."""
self.database.add_node("https://node2", 100)
self.database.allocate_user("test1@test.com", node="https://node2")
self.database.allocate_user("test2@test.com", node="https://node2")
@ -198,6 +215,7 @@ class TestScripts(unittest.TestCase):
self.assertEqual(user["node"], self.NODE_URL)
def test_update_node(self):
"""Test update node."""
self.database.add_node("https://node2", 100)
update_node_script(
args=[

View File

@ -1,12 +1,9 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Script to remove a node from the system.
"""Script to remove a node from the system.
This script clears any assignments to the named node.
"""
import logging
@ -44,9 +41,9 @@ def unassign_node(node):
def main(args=None):
"""Main entry-point for running this script.
"""Run the unassign_node script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the unassign_node() function.
"""
usage = "usage: %prog [options] node_name"

View File

@ -1,11 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Script to update node status in the db.
"""
"""Script to update node status in the database."""
import logging
import optparse
@ -33,9 +29,9 @@ def update_node(node, **kwds):
def main(args=None):
"""Main entry-point for running this script.
"""Run the update_node script with the given arguments.
This function parses command-line arguments and passes them on
Parse command-line arguments and pass them on
to the update_node() function.
"""
usage = "usage: %prog [options] node_name"

View File

@ -1,11 +1,7 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
"""
Admin/managment scripts for TokenServer.
"""
"""Admin/management scripts for TokenServer."""
import sys
import time
@ -20,11 +16,12 @@ from datadog import initialize, statsd
def encode_bytes_b64(value):
"""Encode bytes to a URL-safe base64 string without padding."""
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
def run_script(main):
"""Simple wrapper for running scripts in __main__ section."""
"""Run a script's main function and exit with the returned code."""
try:
exitcode = main()
except KeyboardInterrupt:
@ -39,7 +36,6 @@ def configure_script_logging(opts=None, logger_name=""):
formatting that's more for human readability than machine parsing.
It also takes care of the --verbosity command-line option.
"""
verbosity = (opts and getattr(opts, "verbosity", logging.NOTSET)) or logging.NOTSET
logger = logging.getLogger(logger_name)
level = (
@ -72,7 +68,10 @@ def configure_script_logging(opts=None, logger_name=""):
# This includes "escaping" the message as well as converting the timestamp
# into a parsable format.
class GCP_JSON_Formatter(logging.Formatter):
"""JSON log formatter compatible with Google Cloud Platform logging."""
def format(self, record):
"""Format a log record as a GCP-compatible JSON string."""
return json.dumps(
{
"severity": record.levelname,
@ -98,6 +97,8 @@ def get_timestamp():
class Metrics:
"""Wrapper for sending statsd metrics with a configured namespace."""
def __init__(self, opts, namespace=""):
options = dict(
namespace=namespace,
@ -109,6 +110,7 @@ class Metrics:
initialize(**options)
def incr(self, label, tags=None):
"""Increment a statsd counter with the given label and optional tags."""
statsd.increment(label, tags=tags)